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)