From b1598114d9ebffeefd895d43654d72918c9e360d Mon Sep 17 00:00:00 2001 From: SBCrumb Date: Tue, 28 Oct 2025 17:16:15 -0400 Subject: [PATCH] update: scan to check .nfo files first --- api/routes.py | 6 +- processors/tv_processor.py | 138 +++++++++++++++++++++++++++++++++++-- 2 files changed, 137 insertions(+), 7 deletions(-) diff --git a/api/routes.py b/api/routes.py index fc78370..2753d88 100644 --- a/api/routes.py +++ b/api/routes.py @@ -862,8 +862,8 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N try: # Determine force_scan based on scan mode force_scan = (scan_mode == "full") - print(f"DEBUG: Processing series {scan_path} with force_scan={force_scan}") - result = tv_processor.process_series(scan_path, force_scan=force_scan) + print(f"DEBUG: Processing series {scan_path} with force_scan={force_scan}, scan_mode={scan_mode}") + result = tv_processor.process_series(scan_path, force_scan=force_scan, scan_mode=scan_mode) tv_series_total += 1 if result == "skipped": tv_series_skipped += 1 @@ -928,7 +928,7 @@ async def manual_scan(background_tasks: BackgroundTasks, path: Optional[str] = N try: # Determine force_scan based on scan mode force_scan = (scan_mode == "full") - result = tv_processor.process_series(item, force_scan=force_scan) + result = tv_processor.process_series(item, force_scan=force_scan, scan_mode=scan_mode) tv_series_total += 1 if result == "skipped": tv_series_skipped += 1 diff --git a/processors/tv_processor.py b/processors/tv_processor.py index cb68f3a..47d8fc9 100644 --- a/processors/tv_processor.py +++ b/processors/tv_processor.py @@ -145,7 +145,7 @@ class TVProcessor: _log("ERROR", f"Error checking series completion for {imdb_id}: {e}") return False, f"Error checking completion: {e}" - def process_series(self, series_path: Path, force_scan: bool = False) -> str: + def process_series(self, series_path: Path, force_scan: bool = False, scan_mode: str = "smart") -> str: """Process a TV series directory""" imdb_id = self.nfo_manager.parse_imdb_from_path(series_path) if not imdb_id: @@ -226,12 +226,17 @@ class TVProcessor: return extract_title_from_directory_name(series_path.name) - def _gather_episode_dates(self, series_path: Path, imdb_id: str, disk_episodes: Dict[Tuple[int, int], List[Path]]) -> Dict[Tuple[int, int], Tuple[Optional[str], Optional[str], str]]: - """Gather episode air dates and date added information with database-first optimization""" - _log("INFO", f"🎯 GATHERING EPISODE DATES for {imdb_id}: {len(disk_episodes)} episodes on disk") + def _gather_episode_dates(self, series_path: Path, imdb_id: str, disk_episodes: Dict[Tuple[int, int], List[Path]], scan_mode: str = "smart") -> Dict[Tuple[int, int], Tuple[Optional[str], Optional[str], str]]: + """Gather episode air dates and date added information with optimization based on scan mode""" + _log("INFO", f"🎯 GATHERING EPISODE DATES for {imdb_id}: {len(disk_episodes)} episodes on disk (mode: {scan_mode})") episode_dates = {} episodes_needing_lookup = [] + # For incomplete mode: Start with NFO check to find missing dateadded elements + if scan_mode == "incomplete": + return self._gather_episode_dates_nfo_first(series_path, imdb_id, disk_episodes) + + # For smart/full modes: Use database-first optimization # TIER 1: Check database first for existing dates (fastest) _log("DEBUG", f"TIER 1 - Checking database for existing episode dates for {len(disk_episodes)} episodes") db_cache_hits = 0 @@ -378,6 +383,131 @@ class TVProcessor: return episode_dates + def _gather_episode_dates_nfo_first(self, series_path: Path, imdb_id: str, disk_episodes: Dict[Tuple[int, int], List[Path]]) -> Dict[Tuple[int, int], Tuple[Optional[str], Optional[str], str]]: + """Gather episode dates for incomplete mode: Check NFO files first for missing dateadded elements""" + _log("INFO", f"🔍 NFO-FIRST MODE: Checking {len(disk_episodes)} episodes for missing dateadded in NFO files") + episode_dates = {} + episodes_needing_db_check = [] + episodes_needing_api_lookup = [] + + # STEP 1: Check each NFO file to determine if dateadded is missing + _log("DEBUG", f"STEP 1 - Checking NFO files for missing dateadded elements") + nfo_complete_count = 0 + + for (season, episode) in disk_episodes: + season_dir = series_path / config.get_season_dir_name(season) + episode_files = disk_episodes[(season, episode)] + + nfo_has_dateadded = False + nfo_data = None + + # Look for NFO file for this episode + for episode_file in episode_files: + nfo_path = episode_file.with_suffix('.nfo') + if nfo_path.exists(): + # Extract NFOGuard data from episode NFO + nfo_data = self.nfo_manager.extract_nfoguard_dates_from_nfo(nfo_path) + if nfo_data and nfo_data.get('dateadded'): + # NFO has dateadded - this episode is complete + aired = nfo_data.get('aired') + dateadded = nfo_data.get('dateadded') + source = nfo_data.get('source', 'nfo_cache') + episode_dates[(season, episode)] = (aired, dateadded, source) + nfo_complete_count += 1 + nfo_has_dateadded = True + _log("DEBUG", f"S{season:02d}E{episode:02d}: NFO has dateadded={dateadded}, marked as complete") + break + + if not nfo_has_dateadded: + # NFO missing or missing dateadded - needs further processing + if nfo_data: + # NFO exists but missing dateadded + _log("DEBUG", f"S{season:02d}E{episode:02d}: NFO exists but missing dateadded - needs DB/API lookup") + else: + # No NFO file found + _log("DEBUG", f"S{season:02d}E{episode:02d}: No NFO file found - needs DB/API lookup") + episodes_needing_db_check.append((season, episode)) + + _log("INFO", f"NFO files with dateadded: {nfo_complete_count}/{len(disk_episodes)} episodes. Need DB check: {len(episodes_needing_db_check)}") + + # STEP 2: For episodes missing dateadded in NFO, check database + if episodes_needing_db_check: + _log("DEBUG", f"STEP 2 - Checking database for {len(episodes_needing_db_check)} episodes missing dateadded") + db_hits = 0 + + for (season, episode) in episodes_needing_db_check: + db_result = self.db.get_episode_date(imdb_id, season, episode) + + if db_result and db_result.get('dateadded'): + # Found in database - use cached data and will add to NFO later + aired = db_result.get('aired') + dateadded = db_result.get('dateadded') + source = db_result.get('source', 'database_cache') + episode_dates[(season, episode)] = (aired, dateadded, source) + db_hits += 1 + _log("DEBUG", f"S{season:02d}E{episode:02d}: Database has dateadded={dateadded} - will add to NFO") + else: + # Not in database or incomplete - needs API lookup + episodes_needing_api_lookup.append((season, episode)) + + _log("INFO", f"Database hits: {db_hits}/{len(episodes_needing_db_check)} episodes. Need API lookup: {len(episodes_needing_api_lookup)}") + + # STEP 3: For episodes still missing dateadded, query Sonarr + if episodes_needing_api_lookup: + _log("DEBUG", f"STEP 3 - Querying Sonarr for {len(episodes_needing_api_lookup)} episodes missing from NFO and database") + sonarr_episodes = self._get_sonarr_episodes(imdb_id, episodes_needing_api_lookup) + + for (season, episode) in episodes_needing_api_lookup: + aired = None + dateadded = None + source = "unknown" + + # Try Sonarr first + if (season, episode) in sonarr_episodes: + sonarr_data = sonarr_episodes[(season, episode)] + aired = sonarr_data.get('airDate') + dateadded = sonarr_data.get('dateAdded') + + if dateadded: + source = "sonarr:history.import" + _log("DEBUG", f"S{season:02d}E{episode:02d}: Got Sonarr import date: {dateadded}") + else: + source = "sonarr:no_import_date" + _log("DEBUG", f"S{season:02d}E{episode:02d}: Sonarr has data but no dateAdded (aired: {aired})") + + # Fallback to external sources if needed + if not aired: + _log("DEBUG", f"S{season:02d}E{episode:02d}: No aired date from Sonarr, trying external APIs") + external_aired = self.external_clients.get_episode_air_date(imdb_id, season, episode) + if external_aired: + aired = external_aired + if not dateadded: + source = "external" + _log("INFO", f"S{season:02d}E{episode:02d}: Found aired date from external APIs: {aired}") + + # Use air date as fallback for dateadded if no import date found + if not dateadded and aired: + dateadded = aired + if source == "sonarr:no_import_date": + source = "sonarr:aired_fallback" + elif source == "external": + source = "external:aired_fallback" + else: + source = f"{source}_fallback" if source != "unknown" else "aired_fallback" + _log("DEBUG", f"S{season:02d}E{episode:02d}: Using aired date as dateadded fallback: {dateadded}") + + # Save to database for future lookups + if dateadded or aired: + self.db.upsert_episode_date(imdb_id, season, episode, aired, dateadded, source, True) + + episode_dates[(season, episode)] = (aired, dateadded, source) + + _log("INFO", f"🔍 NFO-FIRST COMPLETE: {len(episode_dates)} episodes processed") + for (s, e), (aired, dateadded, source) in episode_dates.items(): + _log("INFO", f" S{s:02d}E{e:02d}: aired={aired}, dateadded={dateadded}, source={source}") + + return episode_dates + def _get_sonarr_episodes(self, imdb_id: str, episodes_filter: List[Tuple[int, int]] = None) -> Dict[Tuple[int, int], Dict[str, Any]]: """Get episode information from Sonarr including import history - optimized to only fetch needed episodes""" try: