""" Utilities for managing book library, scanning EPUBs, and extracting metadata. """ from pathlib import Path from typing import List, Dict, Optional from dreader import create_ebook_reader import base64 from io import BytesIO from PIL import Image import ebooklib from ebooklib import epub def scan_book_directory(directory: Path) -> List[Dict[str, str]]: """ Scan a directory for EPUB files and extract metadata. Args: directory: Path to directory containing EPUB files Returns: List of book dictionaries with metadata """ books = [] epub_files = list(directory.glob('*.epub')) for epub_path in epub_files: metadata = extract_book_metadata(epub_path) if metadata: books.append(metadata) return sorted(books, key=lambda b: b['title'].lower()) def extract_book_metadata(epub_path: Path, include_cover: bool = True) -> Optional[Dict]: """ Extract metadata from an EPUB file. Args: epub_path: Path to EPUB file include_cover: Whether to extract and include cover image as base64 Returns: Dictionary with book metadata or None if extraction fails """ try: # Create temporary reader to extract metadata reader = create_ebook_reader(page_size=(400, 600)) reader.load_epub(str(epub_path)) metadata = { 'filename': epub_path.name, 'path': str(epub_path), 'title': reader.book_title or epub_path.stem, 'author': reader.book_author or 'Unknown Author', } # Extract cover image if requested - use direct EPUB extraction if include_cover: cover_data = extract_cover_from_epub(epub_path) metadata['cover_data'] = cover_data return metadata except Exception as e: print(f"Error extracting metadata from {epub_path}: {e}") return { 'filename': epub_path.name, 'path': str(epub_path), 'title': epub_path.stem, 'author': 'Unknown', 'cover_data': None } def extract_cover_as_base64(reader, max_width: int = 300, max_height: int = 450) -> Optional[str]: """ Extract cover image from reader and return as base64 encoded string. This function is kept for backward compatibility but now uses extract_cover_from_epub internally if the reader has an epub_path attribute. Args: reader: EbookReader instance with loaded book max_width: Maximum width for cover image max_height: Maximum height for cover image Returns: Base64 encoded PNG image string or None """ try: # If the reader has an epub path, try to extract actual cover if hasattr(reader, '_epub_path') and reader._epub_path: return extract_cover_from_epub(reader._epub_path, max_width, max_height) # Fallback to first page as cover cover_image = reader.get_current_page() # Resize if needed if cover_image.width > max_width or cover_image.height > max_height: cover_image.thumbnail((max_width, max_height)) # Convert to base64 buffer = BytesIO() cover_image.save(buffer, format='PNG') img_bytes = buffer.getvalue() img_base64 = base64.b64encode(img_bytes).decode('utf-8') return img_base64 except Exception as e: print(f"Error extracting cover image: {e}") return None def extract_cover_from_epub(epub_path: Path, max_width: int = 300, max_height: int = 450) -> Optional[str]: """ Extract the actual cover image from an EPUB file. Args: epub_path: Path to EPUB file max_width: Maximum width for cover image max_height: Maximum height for cover image Returns: Base64 encoded PNG image string or None """ try: # Read the EPUB book = epub.read_epub(str(epub_path)) # Look for cover image cover_image = None # First, try to find item marked as cover for item in book.get_items(): if item.get_type() == ebooklib.ITEM_COVER: cover_image = Image.open(BytesIO(item.get_content())) break # If not found, look for files with 'cover' in the name if not cover_image: for item in book.get_items(): if item.get_type() == ebooklib.ITEM_IMAGE: name = item.get_name().lower() if 'cover' in name: cover_image = Image.open(BytesIO(item.get_content())) break # If still not found, get the first image if not cover_image: for item in book.get_items(): if item.get_type() == ebooklib.ITEM_IMAGE: try: cover_image = Image.open(BytesIO(item.get_content())) break except: continue if not cover_image: return None # Resize if needed (maintain aspect ratio) if cover_image.width > max_width or cover_image.height > max_height: cover_image.thumbnail((max_width, max_height), Image.Resampling.LANCZOS) # Convert to base64 buffer = BytesIO() cover_image.save(buffer, format='PNG') img_bytes = buffer.getvalue() img_base64 = base64.b64encode(img_bytes).decode('utf-8') return img_base64 except Exception as e: print(f"Error extracting cover from EPUB {epub_path}: {e}") return None def get_chapter_list(reader) -> List[Dict]: """ Get formatted chapter list from reader. Args: reader: EbookReader instance with loaded book Returns: List of chapter dictionaries with index and title """ try: chapters = reader.get_chapters() result = [] for i, chapter in enumerate(chapters): # Handle different chapter formats if isinstance(chapter, str): title = chapter elif isinstance(chapter, dict): title = chapter.get('title', f'Chapter {i+1}') elif isinstance(chapter, tuple) and len(chapter) >= 2: # Tuple format: (title, ...) title = chapter[0] if chapter[0] else f'Chapter {i+1}' else: title = f'Chapter {i+1}' result.append({ 'index': i, 'title': title }) return result except Exception as e: print(f"Error getting chapters: {e}") return [] def get_bookmark_list(reader) -> List[Dict]: """ Get formatted bookmark list from reader. Args: reader: EbookReader instance with loaded book Returns: List of bookmark dictionaries """ try: bookmarks = reader.list_saved_positions() return [ { 'name': bookmark, 'position': '' # Could be enhanced to show chapter/page info } for bookmark in bookmarks ] except Exception as e: print(f"Error getting bookmarks: {e}") return [] def page_image_to_base64(page_image) -> str: """ Convert PIL Image to base64 encoded string. Args: page_image: PIL Image object Returns: Base64 encoded PNG string """ buffer = BytesIO() page_image.save(buffer, format='PNG') img_bytes = buffer.getvalue() return base64.b64encode(img_bytes).decode('utf-8')