From c609c0b2bb8bd9907a65f4246237e80fa51b17f8 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 31 Dec 2025 13:14:51 +0000 Subject: [PATCH] Fix: Prowlarr categorisation and search fallback (#381) --- cwa_book_downloader/download/orchestrator.py | 30 ++++++++----- .../release_sources/prowlarr/handler.py | 5 ++- .../release_sources/prowlarr/settings.py | 21 +++------ .../release_sources/prowlarr/source.py | 45 +++++++++---------- 4 files changed, 48 insertions(+), 53 deletions(-) diff --git a/cwa_book_downloader/download/orchestrator.py b/cwa_book_downloader/download/orchestrator.py index ca3f2249..5332dc4c 100644 --- a/cwa_book_downloader/download/orchestrator.py +++ b/cwa_book_downloader/download/orchestrator.py @@ -111,35 +111,44 @@ def stage_file(source_path: Path, task_id: str, copy: bool = False) -> Path: return staged_path -def _get_supported_formats() -> List[str]: - """Get current supported formats from config singleton.""" - formats = config.get("SUPPORTED_FORMATS", ["epub", "mobi", "azw3", "fb2", "djvu", "cbz", "cbr"]) +def _get_supported_formats(content_type: str = None) -> List[str]: + """Get current supported formats from config singleton based on content type.""" + if content_type and content_type.lower() == "audiobook": + formats = config.get("SUPPORTED_AUDIOBOOK_FORMATS", ["m4b", "mp3"]) + else: + formats = config.get("SUPPORTED_FORMATS", ["epub", "mobi", "azw3", "fb2", "djvu", "cbz", "cbr"]) # Handle both list (from MultiSelectField) and comma-separated string (legacy/env) if isinstance(formats, str): return [fmt.strip().lower() for fmt in formats.split(",") if fmt.strip()] return [fmt.lower() for fmt in formats] -def _find_book_files_in_directory(directory: Path) -> Tuple[List[Path], List[Path]]: - """Find all book files in a directory matching SUPPORTED_FORMATS. +def _find_book_files_in_directory(directory: Path, content_type: str = None) -> Tuple[List[Path], List[Path]]: + """Find all book files in a directory matching supported formats. Args: directory: Directory to search recursively + content_type: Content type to determine format list (e.g., "audiobook") Returns: Tuple of (matching book files, rejected files with unsupported extensions) """ book_files = [] rejected_files = [] - supported_formats = _get_supported_formats() + supported_formats = _get_supported_formats(content_type) supported_exts = {f".{fmt}" for fmt in supported_formats} + is_audiobook = content_type and content_type.lower() == "audiobook" + if is_audiobook: + trackable_exts = {'.m4b', '.mp3', '.m4a', '.flac', '.ogg', '.wma', '.aac', '.wav'} + else: + trackable_exts = {'.pdf', '.epub', '.mobi', '.azw', '.azw3', '.fb2', '.djvu', '.cbz', '.cbr', '.doc', '.docx', '.rtf', '.txt'} + for file_path in directory.rglob("*"): if file_path.is_file(): if file_path.suffix.lower() in supported_exts: book_files.append(file_path) - elif file_path.suffix.lower() in {'.pdf', '.epub', '.mobi', '.azw', '.azw3', '.fb2', '.djvu', '.cbz', '.cbr', '.doc', '.docx', '.rtf', '.txt'}: - # Track ebook-like files that were rejected due to format settings + elif file_path.suffix.lower() in trackable_exts: rejected_files.append(file_path) return book_files, rejected_files @@ -164,7 +173,8 @@ def process_directory( Tuple of (list of final paths, error message if failed) """ try: - book_files, rejected_files = _find_book_files_in_directory(directory) + content_type = task.content_type + book_files, rejected_files = _find_book_files_in_directory(directory, content_type) # Find archives in directory (ZIP/RAR) archive_files = [f for f in directory.rglob("*") if f.is_file() and is_archive(f)] @@ -206,7 +216,7 @@ def process_directory( # Files were found but didn't match supported formats rejected_exts = sorted(set(f.suffix.lower() for f in rejected_files)) rejected_list = ", ".join(rejected_exts) - supported_formats = _get_supported_formats() + supported_formats = _get_supported_formats(content_type) logger.warning( f"Found {len(rejected_files)} file(s) but none match supported formats. " f"Rejected formats: {rejected_list}. Supported: {', '.join(sorted(supported_formats))}" diff --git a/cwa_book_downloader/release_sources/prowlarr/handler.py b/cwa_book_downloader/release_sources/prowlarr/handler.py index 99195f69..a7a983ce 100644 --- a/cwa_book_downloader/release_sources/prowlarr/handler.py +++ b/cwa_book_downloader/release_sources/prowlarr/handler.py @@ -117,16 +117,17 @@ class ProwlarrHandler(DownloadHandler): # No existing download - add new status_callback("resolving", f"Sending to {client.name}") try: + release_name = prowlarr_result.get("title") or task.title or "Unknown" download_id = client.add_download( url=download_url, - name=task.title or "Unknown", + name=release_name, ) except Exception as e: logger.error(f"Failed to add to {client.name}: {e}") status_callback("error", f"Failed to add to {client.name}: {e}") return None - logger.info(f"Added to {client.name}: {download_id} for '{task.title}'") + logger.info(f"Added to {client.name}: {download_id} for '{release_name}'") # Poll for progress return self._poll_and_complete( diff --git a/cwa_book_downloader/release_sources/prowlarr/settings.py b/cwa_book_downloader/release_sources/prowlarr/settings.py index 7398c030..24f61ee7 100644 --- a/cwa_book_downloader/release_sources/prowlarr/settings.py +++ b/cwa_book_downloader/release_sources/prowlarr/settings.py @@ -323,22 +323,11 @@ def prowlarr_config_settings(): default=[], show_when={"field": "PROWLARR_ENABLED", "value": True}, ), - MultiSelectField( - key="PROWLARR_SEARCH_CATEGORIES", - label="Search Categories", - description="Categories to include in search. Select multiple to broaden results. Leave empty to search all categories.", - options=[ - {"value": "7000", "label": "Books (All)"}, - {"value": "7010", "label": "Mags", "childOf": "7000"}, - {"value": "7020", "label": "Ebook", "childOf": "7000"}, - {"value": "7030", "label": "Comics", "childOf": "7000"}, - {"value": "7040", "label": "Technical", "childOf": "7000"}, - {"value": "7050", "label": "Other", "childOf": "7000"}, - {"value": "7060", "label": "Foreign", "childOf": "7000"}, - {"value": "3030", "label": "Audio/Audiobook"}, - ], - default=["7000"], - variant="dropdown", + CheckboxField( + key="PROWLARR_AUTO_EXPAND", + label="Auto-expand search on no results", + default=False, + description="Automatically retry search without category filtering if no results are found", show_when={"field": "PROWLARR_ENABLED", "value": True}, ), ] diff --git a/cwa_book_downloader/release_sources/prowlarr/source.py b/cwa_book_downloader/release_sources/prowlarr/source.py index 50432b12..dd53a9ee 100644 --- a/cwa_book_downloader/release_sources/prowlarr/source.py +++ b/cwa_book_downloader/release_sources/prowlarr/source.py @@ -199,7 +199,6 @@ class ProwlarrSource(ReleaseSource): def __init__(self): self.last_search_type: Optional[str] = None - self._category_filtered_indexers: List[int] = [] @classmethod def get_column_config(cls) -> ReleaseColumnConfig: @@ -348,48 +347,44 @@ class ProwlarrSource(ReleaseSource): return [] # Get search categories based on content type - # Audiobooks always use 3030 (Audio/Audiobook), ebooks use configured categories + # Audiobooks use 3030 (Audio/Audiobook), ebooks use 7000 (Books) if content_type == "audiobook": - search_categories = ["3030"] # Audio/Audiobook category + search_categories = [3030] # Audio/Audiobook category else: - search_categories = config.get("PROWLARR_SEARCH_CATEGORIES", ["7000"]) - # Handle both list and comma-separated string formats - if isinstance(search_categories, str): - search_categories = [c.strip() for c in search_categories.split(",") if c.strip()] + search_categories = [7000] # Books category if expand_search: - if not self._category_filtered_indexers: - logger.debug("No category-filtered indexers to expand") - return [] - indexers_to_search = self._category_filtered_indexers + # Expand search: search all indexers without category filtering categories = None self.last_search_type = "expanded" else: - indexers_to_search = indexer_ids - # Use configured categories, or None if empty (all categories) - categories = [int(c) for c in search_categories] if search_categories else None - self._category_filtered_indexers = [] - self.last_search_type = "expanded" if not search_categories else "categories" + categories = search_categories + self.last_search_type = "categories" - logger.debug(f"Searching Prowlarr: query='{query}', indexers={indexers_to_search}, categories={categories}") + logger.debug(f"Searching Prowlarr: query='{query}', indexers={indexer_ids}, categories={categories}") all_results = [] try: - for indexer_id in indexers_to_search: + for indexer_id in indexer_ids: try: raw_results = client.search(query=query, indexer_ids=[indexer_id], categories=categories) - - # Track indexers that returned no results with category filter - # so "Expand search" can retry them without the filter - if categories and not raw_results: - self._category_filtered_indexers.append(indexer_id) - if raw_results: all_results.extend(raw_results) except Exception as e: logger.warning(f"Search failed for indexer {indexer_id}: {e}") - if not expand_search and not self._category_filtered_indexers: + # Auto-expand: if no results with categories and auto-expand enabled, retry without categories + auto_expand_enabled = config.get("PROWLARR_AUTO_EXPAND", False) + logger.debug(f"Auto-expand check: no_results={not all_results}, has_categories={bool(categories)}, auto_expand_enabled={auto_expand_enabled}") + if not all_results and categories and auto_expand_enabled: + logger.info("Prowlarr: no results with category filter, auto-expanding search") + for indexer_id in indexer_ids: + try: + raw_results = client.search(query=query, indexer_ids=[indexer_id], categories=None) + if raw_results: + all_results.extend(raw_results) + except Exception as e: + logger.warning(f"Expanded search failed for indexer {indexer_id}: {e}") self.last_search_type = "expanded" results = [_prowlarr_result_to_release(r) for r in all_results]