Initial commit.
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Book Selector
|
||||
|
||||
Interactive file browser for selecting book files.
|
||||
Supports navigation and filtering by supported formats.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
|
||||
|
||||
class BookSelector:
|
||||
"""Book file selection interface"""
|
||||
|
||||
def __init__(self, startDir=None, supportedFormats=None, speechEngine=None):
|
||||
"""
|
||||
Initialize book selector
|
||||
|
||||
Args:
|
||||
startDir: Starting directory (default: home)
|
||||
supportedFormats: List of supported file extensions (default: ['.zip', '.epub'])
|
||||
speechEngine: SpeechEngine instance for accessibility
|
||||
"""
|
||||
if startDir is None:
|
||||
startDir = Path.home()
|
||||
|
||||
if supportedFormats is None:
|
||||
supportedFormats = ['.zip', '.epub']
|
||||
|
||||
self.currentDir = Path(startDir).resolve()
|
||||
self.supportedFormats = supportedFormats
|
||||
self.speechEngine = speechEngine
|
||||
self.currentSelection = 0
|
||||
self.inBrowser = False
|
||||
self.items = []
|
||||
|
||||
def select_book_interactive(self):
|
||||
"""
|
||||
Interactive book selection with directory navigation
|
||||
|
||||
Returns:
|
||||
Selected book path or None if cancelled
|
||||
"""
|
||||
while True:
|
||||
print(f"\nCurrent directory: {self.currentDir}")
|
||||
print("-" * 60)
|
||||
|
||||
# List directories and supported files
|
||||
items = self._list_items()
|
||||
|
||||
if not items:
|
||||
print("No books or directories found")
|
||||
print("\nCommands:")
|
||||
print(" .. - Go to parent directory")
|
||||
print(" q - Cancel")
|
||||
print()
|
||||
|
||||
choice = input("Select> ").strip()
|
||||
if choice == 'q':
|
||||
return None
|
||||
elif choice == '..':
|
||||
self._go_parent()
|
||||
continue
|
||||
|
||||
# Display items
|
||||
for idx, item in enumerate(items):
|
||||
prefix = "[DIR]" if item['isDir'] else "[BOOK]"
|
||||
print(f"{idx + 1}. {prefix} {item['name']}")
|
||||
|
||||
print("-" * 60)
|
||||
print("\nCommands:")
|
||||
print(" <number> - Select item")
|
||||
print(" .. - Go to parent directory")
|
||||
print(" q - Cancel")
|
||||
print()
|
||||
|
||||
try:
|
||||
choice = input("Select> ").strip()
|
||||
|
||||
if choice == 'q':
|
||||
return None
|
||||
|
||||
elif choice == '..':
|
||||
self._go_parent()
|
||||
|
||||
else:
|
||||
# Select item by number
|
||||
try:
|
||||
itemNum = int(choice)
|
||||
if 1 <= itemNum <= len(items):
|
||||
selectedItem = items[itemNum - 1]
|
||||
|
||||
if selectedItem['isDir']:
|
||||
# Navigate into directory
|
||||
self.currentDir = selectedItem['path']
|
||||
else:
|
||||
# Return selected book
|
||||
return str(selectedItem['path'])
|
||||
else:
|
||||
print(f"Invalid number. Choose 1-{len(items)}")
|
||||
|
||||
except ValueError:
|
||||
print("Invalid input. Enter a number, '..' for parent, or 'q' to cancel")
|
||||
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
print("\nCancelled")
|
||||
return None
|
||||
|
||||
def _list_items(self):
|
||||
"""
|
||||
List directories and supported book files in current directory
|
||||
|
||||
Returns:
|
||||
List of item dictionaries
|
||||
"""
|
||||
items = []
|
||||
|
||||
try:
|
||||
# Add directories (excluding hidden)
|
||||
for item in sorted(self.currentDir.iterdir()):
|
||||
if item.name.startswith('.'):
|
||||
continue
|
||||
|
||||
if item.is_dir():
|
||||
items.append({
|
||||
'name': item.name,
|
||||
'path': item,
|
||||
'isDir': True
|
||||
})
|
||||
|
||||
# Add supported book files
|
||||
for item in sorted(self.currentDir.iterdir()):
|
||||
if item.name.startswith('.'):
|
||||
continue
|
||||
|
||||
if item.is_file() and item.suffix.lower() in self.supportedFormats:
|
||||
# For zip files, validate that they're actually DAISY books
|
||||
if item.suffix.lower() == '.zip':
|
||||
if not self._is_daisy_zip(item):
|
||||
continue # Skip non-DAISY zip files
|
||||
|
||||
items.append({
|
||||
'name': item.name,
|
||||
'path': item,
|
||||
'isDir': False
|
||||
})
|
||||
|
||||
except PermissionError:
|
||||
print(f"Permission denied: {self.currentDir}")
|
||||
|
||||
return items
|
||||
|
||||
def _go_parent(self):
|
||||
"""Navigate to parent directory"""
|
||||
parent = self.currentDir.parent
|
||||
if parent != self.currentDir: # Not at root
|
||||
self.currentDir = parent
|
||||
else:
|
||||
print("Already at root directory")
|
||||
|
||||
def _is_daisy_zip(self, zipPath):
|
||||
"""
|
||||
Check if a zip file contains a DAISY book
|
||||
|
||||
Args:
|
||||
zipPath: Path to zip file
|
||||
|
||||
Returns:
|
||||
True if zip contains DAISY book markers, False otherwise
|
||||
"""
|
||||
try:
|
||||
with zipfile.ZipFile(zipPath, 'r') as zf:
|
||||
fileList = zf.namelist()
|
||||
|
||||
# Check for DAISY 2.02 marker (ncc.html)
|
||||
if 'ncc.html' in fileList:
|
||||
return True
|
||||
|
||||
# Check for DAISY 3 marker (.ncx file)
|
||||
for filename in fileList:
|
||||
if filename.lower().endswith('.ncx'):
|
||||
return True
|
||||
|
||||
return False
|
||||
except (zipfile.BadZipFile, PermissionError, OSError):
|
||||
# If we can't read it, don't show it
|
||||
return False
|
||||
|
||||
def reset_to_directory(self, directory):
|
||||
"""
|
||||
Reset browser to a specific directory
|
||||
|
||||
Args:
|
||||
directory: Path to directory to reset to
|
||||
"""
|
||||
dirPath = Path(directory).resolve()
|
||||
if dirPath.exists() and dirPath.is_dir():
|
||||
self.currentDir = dirPath
|
||||
|
||||
def get_current_directory(self):
|
||||
"""
|
||||
Get current directory path
|
||||
|
||||
Returns:
|
||||
Path object of current directory
|
||||
"""
|
||||
return self.currentDir
|
||||
|
||||
def enter_browser(self):
|
||||
"""Enter the book browser"""
|
||||
self.inBrowser = True
|
||||
self.currentSelection = 0
|
||||
self.items = self._list_items()
|
||||
|
||||
if self.speechEngine:
|
||||
self.speechEngine.speak("Book browser. Use arrow keys to navigate, Enter to select, Backspace for parent directory, L to set library, Escape to cancel.")
|
||||
|
||||
# Speak current directory and first item
|
||||
if self.speechEngine:
|
||||
dirName = self.currentDir.name if self.currentDir.name else str(self.currentDir)
|
||||
self.speechEngine.speak(f"Directory: {dirName}")
|
||||
|
||||
if self.items:
|
||||
self._speak_current_item()
|
||||
elif self.speechEngine:
|
||||
self.speechEngine.speak("Empty directory")
|
||||
|
||||
def navigate_browser(self, direction):
|
||||
"""Navigate browser up or down"""
|
||||
if not self.items:
|
||||
return
|
||||
|
||||
if direction == 'up':
|
||||
self.currentSelection = (self.currentSelection - 1) % len(self.items)
|
||||
elif direction == 'down':
|
||||
self.currentSelection = (self.currentSelection + 1) % len(self.items)
|
||||
|
||||
self._speak_current_item()
|
||||
|
||||
def _speak_current_item(self):
|
||||
"""Speak current item with name first, then type"""
|
||||
if not self.items or not self.speechEngine:
|
||||
return
|
||||
|
||||
item = self.items[self.currentSelection]
|
||||
if item['isDir']:
|
||||
text = f"{item['name']}, directory"
|
||||
else:
|
||||
text = f"{item['name']}, book"
|
||||
|
||||
self.speechEngine.speak(text)
|
||||
|
||||
def activate_current_item(self):
|
||||
"""
|
||||
Activate current item (enter directory or return book path)
|
||||
|
||||
Returns:
|
||||
Book path if selected, None if navigating or empty
|
||||
"""
|
||||
if not self.items:
|
||||
if self.speechEngine:
|
||||
self.speechEngine.speak("No items")
|
||||
return None
|
||||
|
||||
item = self.items[self.currentSelection]
|
||||
|
||||
if item['isDir']:
|
||||
# Navigate into directory
|
||||
self.currentDir = item['path']
|
||||
self.currentSelection = 0
|
||||
self.items = self._list_items()
|
||||
|
||||
if self.speechEngine:
|
||||
dirName = self.currentDir.name if self.currentDir.name else str(self.currentDir)
|
||||
self.speechEngine.speak(f"Entered directory: {dirName}")
|
||||
|
||||
if self.items:
|
||||
self._speak_current_item()
|
||||
elif self.speechEngine:
|
||||
self.speechEngine.speak("Empty directory")
|
||||
|
||||
return None
|
||||
else:
|
||||
# Book selected
|
||||
if self.speechEngine:
|
||||
self.speechEngine.speak(f"Loading: {item['name']}")
|
||||
return str(item['path'])
|
||||
|
||||
def go_parent_directory(self):
|
||||
"""Go to parent directory"""
|
||||
parent = self.currentDir.parent
|
||||
if parent != self.currentDir:
|
||||
self.currentDir = parent
|
||||
self.currentSelection = 0
|
||||
self.items = self._list_items()
|
||||
|
||||
if self.speechEngine:
|
||||
dirName = self.currentDir.name if self.currentDir.name else str(self.currentDir)
|
||||
self.speechEngine.speak(f"Parent directory: {dirName}")
|
||||
|
||||
if self.items:
|
||||
self._speak_current_item()
|
||||
elif self.speechEngine:
|
||||
self.speechEngine.speak("Empty directory")
|
||||
else:
|
||||
if self.speechEngine:
|
||||
self.speechEngine.speak("Already at root")
|
||||
|
||||
def is_in_browser(self):
|
||||
"""Check if currently in browser"""
|
||||
return self.inBrowser
|
||||
|
||||
def exit_browser(self):
|
||||
"""Exit the browser"""
|
||||
self.inBrowser = False
|
||||
if self.speechEngine:
|
||||
self.speechEngine.speak("Cancelled")
|
||||
Reference in New Issue
Block a user