mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-10-04 22:05:45 +01:00
Fix: Prowlarr categorisation and search fallback (#381)
This commit is contained in:
@@ -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))}"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user