#!/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(" - 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")