From 5d712065b9d3379627c32b6d990b00c905c9d7bd Mon Sep 17 00:00:00 2001 From: ponzischeme89 Date: Sun, 18 Jan 2026 21:14:14 +1300 Subject: [PATCH] 1.0.5 - regression in adding extra info (directors, etc) --- frontend/src/components/AppSidebar.svelte | 2 +- frontend/src/components/ResultsList.svelte | 4 +- server/app.py | 116 ++++++++++++--------- server/core/omdb_client.py | 70 +++++++++++++ server/core/subtitle_processor.py | 19 +++- 5 files changed, 161 insertions(+), 50 deletions(-) diff --git a/frontend/src/components/AppSidebar.svelte b/frontend/src/components/AppSidebar.svelte index 7ee39d2..2c35bfc 100644 --- a/frontend/src/components/AppSidebar.svelte +++ b/frontend/src/components/AppSidebar.svelte @@ -157,7 +157,7 @@ > {#if !collapsed} v1.0.4 Release Candiatev1.0.5 Release Candiate {:else} v diff --git a/frontend/src/components/ResultsList.svelte b/frontend/src/components/ResultsList.svelte index 0bb7bbd..711bdcb 100644 --- a/frontend/src/components/ResultsList.svelte +++ b/frontend/src/components/ResultsList.svelte @@ -178,8 +178,10 @@ } function handleAddPlotWithSearch(file, event) { + // Svelte Button dispatches a custom event; the native MouseEvent is in detail. + const nativeEvent = event?.detail ?? event; // Open dropdown for search - toggleSearchDropdown(file, event); + toggleSearchDropdown(file, nativeEvent); } function handleQuickAddPlot(file) { diff --git a/server/app.py b/server/app.py index 91f7e3b..6ac803b 100644 --- a/server/app.py +++ b/server/app.py @@ -131,19 +131,19 @@ def initialize_clients(): global omdb_client, tmdb_client, tvmaze_client, processor # Load OMDb API key - omdb_key = db.get_setting("omdb_api_key", "") or "" + omdb_key = _get_str_setting("omdb_api_key", "") if not omdb_key: # Fallback to legacy "api_key" setting - omdb_key = db.get_setting("api_key", "") or "" + omdb_key = _get_str_setting("api_key", "") if not omdb_key: omdb_key = config.get("api_key", "") # Load TMDb API key - tmdb_key = db.get_setting("tmdb_api_key", "") - omdb_enabled = db.get_setting("omdb_enabled", False) - tmdb_enabled = db.get_setting("tmdb_enabled", False) - tvmaze_enabled = db.get_setting("tvmaze_enabled", False) - preferred_source = db.get_setting("preferred_source", "omdb") + tmdb_key = _get_str_setting("tmdb_api_key", "") + omdb_enabled = _get_bool_setting("omdb_enabled", False) + tmdb_enabled = _get_bool_setting("tmdb_enabled", False) + tvmaze_enabled = _get_bool_setting("tvmaze_enabled", False) + preferred_source = _get_str_setting("preferred_source", "omdb") # Initialize clients with db_manager for usage tracking if omdb_enabled and omdb_key: @@ -196,15 +196,28 @@ def migrate_settings(): logger.error(f"Error migrating settings: {e}") +def _get_bool_setting(key: str, default: bool) -> bool: + """Fetch a boolean setting with explicit casting for type safety.""" + value = db.get_setting(key, default) + return bool(value) + +def _get_str_setting(key: str, default: str) -> str: + """Fetch a string setting with explicit casting for type safety.""" + value = db.get_setting(key, default) + if value is None: + return default + return str(value) + + def get_format_options_from_settings() -> SubtitleFormatOptions: """Load subtitle formatting options from database settings.""" return SubtitleFormatOptions( - title_bold=db.get_setting("subtitle_title_bold", True), - plot_italic=db.get_setting("subtitle_plot_italic", True), - show_director=db.get_setting("subtitle_show_director", False), - show_actors=db.get_setting("subtitle_show_actors", False), - show_released=db.get_setting("subtitle_show_released", False), - show_genre=db.get_setting("subtitle_show_genre", False), + title_bold=_get_bool_setting("subtitle_title_bold", True), + plot_italic=_get_bool_setting("subtitle_plot_italic", True), + show_director=_get_bool_setting("subtitle_show_director", False), + show_actors=_get_bool_setting("subtitle_show_actors", False), + show_released=_get_bool_setting("subtitle_show_released", False), + show_genre=_get_bool_setting("subtitle_show_genre", False), ) @@ -823,7 +836,11 @@ def search_title(): "imdb_rating": data.get("imdbRating", "N/A"), "media_type": data.get("Type"), "poster": data.get("Poster"), - "imdb_id": data.get("imdbID") + "imdb_id": data.get("imdbID"), + "director": data.get("Director", "N/A"), + "actors": data.get("Actors", "N/A"), + "released": data.get("Released", "N/A"), + "genre": data.get("Genre", "N/A") }] db.track_api_call( @@ -869,43 +886,48 @@ def search_title(): if not search_results: return [] - # Second: get details for top result only (saves API calls) - # Users can see basic info for others, click to load more if needed - top_item = search_results[0] - detail_params = { - "apikey": omdb_client.api_key, - "i": top_item.get("imdbID"), - "plot": "short" - } - + # Second: get details for each result so manual selections have full metadata detailed_results = [] - async with session.get(omdb_client.BASE_URL, params=detail_params) as detail_resp: - api_calls += 1 - if detail_resp.status == 200: - detail_data = await detail_resp.json() - if detail_data.get("Response") == "True": - detailed_results.append({ - "title": detail_data.get("Title"), - "year": detail_data.get("Year"), - "plot": detail_data.get("Plot", "No plot available"), - "runtime": detail_data.get("Runtime", "N/A"), - "imdb_rating": detail_data.get("imdbRating", "N/A"), - "media_type": detail_data.get("Type"), - "poster": detail_data.get("Poster"), - "imdb_id": detail_data.get("imdbID") - }) - - # Add remaining results with basic info (no extra API calls) - for item in search_results[1:]: + for item in search_results: + detail_params = { + "apikey": omdb_client.api_key, + "i": item.get("imdbID"), + "plot": "short" + } + async with session.get(omdb_client.BASE_URL, params=detail_params) as detail_resp: + api_calls += 1 + if detail_resp.status == 200: + detail_data = await detail_resp.json() + if detail_data.get("Response") == "True": + detailed_results.append({ + "title": detail_data.get("Title"), + "year": detail_data.get("Year"), + "plot": detail_data.get("Plot", "No plot available"), + "runtime": detail_data.get("Runtime", "N/A"), + "imdb_rating": detail_data.get("imdbRating", "N/A"), + "media_type": detail_data.get("Type"), + "poster": detail_data.get("Poster"), + "imdb_id": detail_data.get("imdbID"), + "director": detail_data.get("Director", "N/A"), + "actors": detail_data.get("Actors", "N/A"), + "released": detail_data.get("Released", "N/A"), + "genre": detail_data.get("Genre", "N/A") + }) + continue + # Fallback to basic info if detail lookup fails detailed_results.append({ "title": item.get("Title"), "year": item.get("Year"), - "plot": None, # Not fetched yet + "plot": None, "runtime": None, "imdb_rating": None, "media_type": item.get("Type"), "poster": item.get("Poster"), - "imdb_id": item.get("imdbID") + "imdb_id": item.get("imdbID"), + "director": "N/A", + "actors": "N/A", + "released": "N/A", + "genre": "N/A" }) response_time_ms = int((time.time() - start_time) * 1000) @@ -968,10 +990,10 @@ def process_files(): format_options = get_format_options_from_settings() # Load strip_keywords setting (default True for better matching) - strip_keywords = db.get_setting("strip_keywords", True) + strip_keywords = _get_bool_setting("strip_keywords", True) # Load clean_subtitle_content setting (default True for ad removal) - clean_subtitle_content = db.get_setting("clean_subtitle_content", True) + clean_subtitle_content = _get_bool_setting("clean_subtitle_content", True) # Create a processing run in database run_id = db.create_run(total_files=len(file_paths)) @@ -1106,10 +1128,10 @@ def process_batch(): format_options = get_format_options_from_settings() # Load strip_keywords setting (default True for better matching) - strip_keywords = db.get_setting("strip_keywords", True) + strip_keywords = _get_bool_setting("strip_keywords", True) # Load clean_subtitle_content setting (default True for ad removal) - clean_subtitle_content = db.get_setting("clean_subtitle_content", True) + clean_subtitle_content = _get_bool_setting("clean_subtitle_content", True) # Create a processing run run_id = db.create_run(total_files=total) diff --git a/server/core/omdb_client.py b/server/core/omdb_client.py index fd43396..e5d2bc5 100644 --- a/server/core/omdb_client.py +++ b/server/core/omdb_client.py @@ -124,6 +124,35 @@ class OMDbClient: finally: self._inflight.pop(key, None) + async def fetch_summary_by_imdb_id(self, imdb_id: str) -> Optional[dict]: + """ + Fetch summary from OMDb using a specific IMDb ID. + + Args: + imdb_id: IMDb ID (e.g., tt1234567) + """ + if not imdb_id: + return None + + key = f"imdb:{imdb_id.lower()}" + if key in self._inflight: + logger.debug("Awaiting inflight OMDb request: %s", key) + return await self._inflight[key] + + loop = asyncio.get_running_loop() + future = loop.create_future() + self._inflight[key] = future + + try: + result = await self._fetch_summary_by_imdb_id_internal(imdb_id) + future.set_result(result) + return result + except Exception as e: + future.set_exception(e) + raise + finally: + self._inflight.pop(key, None) + # ----------------------------- # Internal fetch logic # ----------------------------- @@ -202,6 +231,47 @@ class OMDbClient: logger.error("OMDb network error for '%s': %s", title, e) return None + async def _fetch_summary_by_imdb_id_internal(self, imdb_id: str) -> Optional[dict]: + logger.info("Fetching OMDb summary for IMDb ID: %s", imdb_id) + + async with self.semaphore: + await self.rate_limiter.wait() + + params = { + "apikey": self.api_key, + "i": imdb_id, + "plot": "short", + } + + session = await self._get_session() + start = time.monotonic() + + try: + async with session.get(self.BASE_URL, params=params) as resp: + elapsed_ms = int((time.monotonic() - start) * 1000) + + if resp.status != 200: + self._track(False, imdb_id, elapsed_ms) + logger.error("OMDb HTTP %s for IMDb ID '%s'", resp.status, imdb_id) + return None + + data = await resp.json() + + if data.get("Response") != "True": + self._track(False, imdb_id, elapsed_ms) + logger.warning("OMDb error for IMDb ID '%s': %s", imdb_id, data.get("Error")) + return None + + self._track(True, imdb_id, elapsed_ms) + return self._parse_response(data) + + except asyncio.TimeoutError: + logger.error("OMDb timeout for IMDb ID '%s'", imdb_id) + return None + except aiohttp.ClientError as e: + logger.error("OMDb network error for IMDb ID '%s': %s", imdb_id, e) + return None + # ----------------------------- # Helpers # ----------------------------- diff --git a/server/core/subtitle_processor.py b/server/core/subtitle_processor.py index 2d49b1c..60c3ba8 100644 --- a/server/core/subtitle_processor.py +++ b/server/core/subtitle_processor.py @@ -1206,7 +1206,24 @@ class SubtitleProcessor: # (Do metadata fetch BEFORE acquiring lock to minimize lock hold time) if title_override: logger.info("Using provided title override for %s: %s", file_path.name, title_override.get("title")) - movie = title_override + movie = dict(title_override) + + # If extra fields are missing, try to enrich from OMDb using IMDb ID. + missing_fields = ["director", "actors", "released", "genre"] + has_missing = any( + not movie.get(field) or movie.get(field) == "N/A" + for field in missing_fields + ) + imdb_id = movie.get("imdb_id") or movie.get("imdbID") + if has_missing and imdb_id and self.omdb_client: + try: + enrichment = await self.omdb_client.fetch_summary_by_imdb_id(imdb_id) + if enrichment: + for field in missing_fields: + if not movie.get(field) or movie.get(field) == "N/A": + movie[field] = enrichment.get(field, movie.get(field)) + except Exception as e: + logger.warning("Failed to enrich metadata for %s: %s", imdb_id, e) else: raw_name = file_path.stem movie_name, year = self.extract_title_and_year(raw_name, strip_keywords=strip_keywords)