From 03bcd445650255de7bbee98fed9b74052b5de664 Mon Sep 17 00:00:00 2001 From: SBCrumb Date: Mon, 27 Oct 2025 09:25:47 -0400 Subject: [PATCH] improvments: updates to scans etc --- api/routes.py | 25 ++++++++++++---- clients/sonarr_client.py | 4 +++ core/nfo_manager.py | 64 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/api/routes.py b/api/routes.py index 259c8cc..602432b 100644 --- a/api/routes.py +++ b/api/routes.py @@ -804,7 +804,9 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N if path and scan_path.name.lower().startswith('season'): # Single season processing series_path = scan_path.parent - if nfo_manager.parse_imdb_from_path(series_path): + tv_processor_obj = dependencies.get("tv_processor") + sonarr_client = tv_processor_obj.sonarr if tv_processor_obj and hasattr(tv_processor_obj, 'sonarr') else None + if nfo_manager.parse_imdb_from_path_with_nfo_fallback(series_path, sonarr_client): print(f"INFO: Processing single season: {scan_path}") try: tv_processor.process_season(series_path, scan_path) @@ -814,15 +816,19 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N # Single episode processing season_path = scan_path.parent series_path = season_path.parent - if nfo_manager.parse_imdb_from_path(series_path): + tv_processor_obj = dependencies.get("tv_processor") + sonarr_client = tv_processor_obj.sonarr if tv_processor_obj and hasattr(tv_processor_obj, 'sonarr') else None + if nfo_manager.parse_imdb_from_path_with_nfo_fallback(series_path, sonarr_client): print(f"INFO: Processing single episode: {scan_path}") try: tv_processor.process_episode_file(series_path, season_path, scan_path) except Exception as e: print(f"ERROR: Failed processing episode {scan_path}: {e}") else: - # Check if this path itself is a series (has IMDb ID in the directory name) - if nfo_manager.parse_imdb_from_path(scan_path): + # Check if this path itself is a series (has IMDb ID in directory name or NFO files) + tv_processor_obj = dependencies.get("tv_processor") + sonarr_client = tv_processor_obj.sonarr if tv_processor_obj and hasattr(tv_processor_obj, 'sonarr') else None + if nfo_manager.parse_imdb_from_path_with_nfo_fallback(scan_path, sonarr_client): try: # Determine force_scan based on scan mode force_scan = (scan_mode == "full") @@ -846,8 +852,10 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N not item.name.lower().startswith('season') and not re.match(r'^season\s+\d+$', item.name, re.IGNORECASE)): - # Check for IMDb ID - imdb_id = nfo_manager.parse_imdb_from_path(item) + # Check for IMDb ID (enhanced with NFO fallback) + tv_processor_obj = dependencies.get("tv_processor") + sonarr_client = tv_processor_obj.sonarr if tv_processor_obj and hasattr(tv_processor_obj, 'sonarr') else None + imdb_id = nfo_manager.parse_imdb_from_path_with_nfo_fallback(item, sonarr_client) if imdb_id: tv_series_list.append(item) else: @@ -2201,6 +2209,11 @@ def register_routes(app, dependencies: dict): @app.get("/health") async def _health() -> HealthResponse: return await health(dependencies) + + @app.get("/health/simple") + async def _health_simple(): + """Simple health check for Docker without external dependencies""" + return {"status": "healthy", "service": "nfoguard-core"} @app.get("/stats") async def _get_stats(): diff --git a/clients/sonarr_client.py b/clients/sonarr_client.py index 8b037aa..b149eed 100644 --- a/clients/sonarr_client.py +++ b/clients/sonarr_client.py @@ -150,6 +150,10 @@ class SonarrClient: _log("WARNING", f"No series found with IMDb ID via direct lookup: {imdb_id}") return None + def get_series_by_id(self, series_id: int) -> Optional[Dict[str, Any]]: + """Get series information by Sonarr series ID""" + return self._get(f"/series/{series_id}") + def episodes_for_series(self, series_id: int) -> List[Dict[str, Any]]: """Get all episodes for a series""" return self._get("/episode", {"seriesId": series_id}) or [] diff --git a/core/nfo_manager.py b/core/nfo_manager.py index 23b98fe..972582a 100644 --- a/core/nfo_manager.py +++ b/core/nfo_manager.py @@ -50,6 +50,58 @@ class NFOManager: return None + def parse_imdb_from_path_with_nfo_fallback(self, path: Path, sonarr_client=None) -> Optional[str]: + """ + Enhanced IMDb detection that fallback to Sonarr ID lookup from NFO files + + 1. First try to parse IMDb ID from directory path/name + 2. If not found, scan NFO files in directory for Sonarr series ID + 3. Use Sonarr API to lookup series and extract IMDb ID + """ + # Primary: Try directory name first + imdb_id = self.parse_imdb_from_path(path) + if imdb_id: + return imdb_id + + # Fallback: Check NFO files for Sonarr series ID + if not sonarr_client or not sonarr_client.enabled: + return None + + try: + # Look for episode NFO files in the directory (and subdirectories) + nfo_files = [] + if path.is_dir(): + # Check current directory and season subdirectories + nfo_files.extend(list(path.glob("*.nfo"))) + for subdir in path.iterdir(): + if subdir.is_dir() and subdir.name.lower().startswith('season'): + nfo_files.extend(list(subdir.glob("*.nfo"))) + + # Extract Sonarr series ID from any NFO file + for nfo_file in nfo_files: + try: + tree = ET.parse(nfo_file) + root = tree.getroot() + + # Look for Sonarr series ID + for uniqueid in root.findall('.//uniqueid[@type="sonarr"]'): + sonarr_id = uniqueid.text + if sonarr_id and sonarr_id.isdigit(): + # Look up series in Sonarr to get IMDb ID + series_data = sonarr_client.get_series_by_id(int(sonarr_id)) + if series_data: + imdb_id = series_data.get('imdbId') + if imdb_id: + return imdb_id.replace('tt', '') if imdb_id.startswith('tt') else imdb_id + + except Exception as e: + continue # Skip this NFO file and try the next one + + except Exception as e: + pass # Fallback failed, return None + + return None + def parse_imdb_from_nfo(self, nfo_path: Path) -> Optional[str]: """Extract IMDb ID from NFO file content""" if not nfo_path.exists(): @@ -154,8 +206,10 @@ class NFOManager: aired_elem = root.find('.//aired') # For TV episodes lockdata_elem = root.find('.//lockdata') - # Consider it NFOGuard-managed if it has lockdata=true (with or without dateadded) - if lockdata_elem is not None and lockdata_elem.text == "true": + # Consider it NFOGuard-managed ONLY if it has BOTH lockdata=true AND dateadded + # This prevents incomplete NFO files from being marked as "complete" + if (lockdata_elem is not None and lockdata_elem.text == "true" and + dateadded_elem is not None and dateadded_elem.text): # Extract original source from NFOGuard comment, default to nfo_file_existing source = "nfo_file_existing" @@ -206,8 +260,10 @@ class NFOManager: aired_elem = root.find('.//aired') lockdata_elem = root.find('.//lockdata') - # Consider it NFOGuard-managed if it has lockdata=true (with or without dateadded) - if lockdata_elem is not None and lockdata_elem.text == "true": + # Consider it NFOGuard-managed ONLY if it has BOTH lockdata=true AND dateadded + # This prevents incomplete episode NFO files from being marked as "complete" + if (lockdata_elem is not None and lockdata_elem.text == "true" and + dateadded_elem is not None and dateadded_elem.text): # Extract original source from NFOGuard comment, default to episode_nfo_existing source = "episode_nfo_existing"