diff --git a/VERSION b/VERSION index c945ef1..7d5624f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.18 +2.0.21 diff --git a/api/routes.py b/api/routes.py index c944fb7..08f9711 100644 --- a/api/routes.py +++ b/api/routes.py @@ -152,54 +152,39 @@ async def sonarr_webhook(request: Request, background_tasks: BackgroundTasks, de if recent_renames: # Take the most recent rename event latest_rename = recent_renames[0] - print(f"DEBUG: Latest rename event keys: {list(latest_rename.keys())}") - print(f"DEBUG: Latest rename event full structure: {latest_rename}") + print(f"DEBUG: Processing latest rename event") - # Try different possible episode data locations - episode_info = latest_rename.get("episode", {}) - episode_file_info = latest_rename.get("episodeFile", {}) - data_info = latest_rename.get("data", {}) + # Extract episodeId directly from the rename event + episode_id = latest_rename.get("episodeId") + print(f"DEBUG: Found episodeId {episode_id} in rename event") - print(f"DEBUG: Episode info: {episode_info}") - print(f"DEBUG: EpisodeFile info: {episode_file_info}") - print(f"DEBUG: Data info: {data_info}") - - # Try to extract episode info from any available source - season_num = None - episode_num = None - episode_id = None - episode_title = None - - # Check episode object first - if episode_info: - season_num = episode_info.get("seasonNumber") - episode_num = episode_info.get("episodeNumber") - episode_id = episode_info.get("id") - episode_title = episode_info.get("title") - - # Check episodeFile object - if not season_num and episode_file_info: - season_num = episode_file_info.get("seasonNumber") - episode_num = episode_file_info.get("episodeNumber") - episode_id = episode_file_info.get("episodeId") or episode_file_info.get("id") - - # Check data object - if not season_num and data_info: - season_num = data_info.get("seasonNumber") - episode_num = data_info.get("episodeNumber") - - print(f"DEBUG: Extracted - Season: {season_num}, Episode: {episode_num}, ID: {episode_id}") - - if season_num and episode_num: - episodes_data = [{ - "seasonNumber": season_num, - "episodeNumber": episode_num, - "id": episode_id, - "title": episode_title - }] - print(f"INFO: Found recently renamed episode from history: S{season_num:02d}E{episode_num:02d}") + if episode_id: + # Fetch episode details using the episodeId + episode_detail_url = f"{config.sonarr_url}/api/v3/episode/{episode_id}" + episode_response = requests.get(episode_detail_url, headers={"X-Api-Key": os.environ.get("SONARR_API_KEY", "")}, timeout=10) + + if episode_response.status_code == 200: + episode_detail = episode_response.json() + season_num = episode_detail.get("seasonNumber") + episode_num = episode_detail.get("episodeNumber") + episode_title = episode_detail.get("title") + + print(f"DEBUG: Episode details - Season: {season_num}, Episode: {episode_num}, Title: {episode_title}") + + if season_num is not None and episode_num is not None: + episodes_data = [{ + "seasonNumber": season_num, + "episodeNumber": episode_num, + "id": episode_id, + "title": episode_title + }] + print(f"INFO: Successfully identified renamed episode: S{season_num:02d}E{episode_num:02d} - {episode_title}") + else: + print(f"DEBUG: Episode details missing season/episode numbers") + else: + print(f"DEBUG: Failed to fetch episode details: {episode_response.status_code}") else: - print(f"DEBUG: Could not extract episode data from rename event") + print(f"DEBUG: No episodeId found in rename event") else: print(f"DEBUG: No recent rename events found in last hour") else: diff --git a/processors/tv_processor.py b/processors/tv_processor.py index 0e54de8..6d11216 100644 --- a/processors/tv_processor.py +++ b/processors/tv_processor.py @@ -102,47 +102,70 @@ class TVProcessor: 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""" + """Gather episode air dates and date added information with database-first optimization""" episode_dates = {} + episodes_needing_lookup = [] - # Get data from Sonarr first if available - sonarr_episodes = self._get_sonarr_episodes(imdb_id) + # OPTIMIZATION: Check database first for existing dates + _log("DEBUG", f"Checking database for existing episode dates for {len(disk_episodes)} episodes") + db_cache_hits = 0 - # Get data from external sources for missing information for (season, episode) in disk_episodes: - aired = None - dateadded = None - source = "unknown" + # Try database first - this is much faster than API calls + db_result = self.db.get_episode_date(imdb_id, season, episode) - # 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: - # Use more specific source to indicate this came from Sonarr import history - source = "sonarr:history.import" + if db_result and db_result.get('dateadded'): + # Found in database - use cached data + 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_cache_hits += 1 + _log("DEBUG", f"Database cache hit for S{season:02d}E{episode:02d}: {dateadded}") + else: + # Not in database or incomplete - needs lookup + episodes_needing_lookup.append((season, episode)) + + _log("INFO", f"Database cache hits: {db_cache_hits}/{len(disk_episodes)} episodes. Need API lookup: {len(episodes_needing_lookup)}") + + # Only call Sonarr API for episodes not in database + if episodes_needing_lookup: + _log("DEBUG", f"Querying Sonarr for {len(episodes_needing_lookup)} episodes missing from database") + sonarr_episodes = self._get_sonarr_episodes(imdb_id, episodes_needing_lookup) - # Fallback to external sources if needed - if not aired: - external_aired = self.external_clients.get_episode_air_date(imdb_id, season, episode) - if external_aired: - aired = external_aired - if not dateadded: - source = "external" - - # ONLY use air date as fallback for dateadded if we have no Sonarr import date at all - # and if we couldn't get any import information from Sonarr history - if not dateadded and aired and source != "sonarr:history.import": - dateadded = aired - source = f"{source}_fallback" if source != "unknown" else "aired_fallback" - - episode_dates[(season, episode)] = (aired, dateadded, source) + # Process episodes that needed lookup + for (season, episode) in episodes_needing_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" + + # Fallback to external sources if needed + if not aired: + external_aired = self.external_clients.get_episode_air_date(imdb_id, season, episode) + if external_aired: + aired = external_aired + if not dateadded: + source = "external" + + # Use air date as fallback for dateadded if no import date found + if not dateadded and aired and source != "sonarr:history.import": + dateadded = aired + source = f"{source}_fallback" if source != "unknown" else "aired_fallback" + + episode_dates[(season, episode)] = (aired, dateadded, source) return episode_dates - def _get_sonarr_episodes(self, imdb_id: str) -> Dict[Tuple[int, int], Dict[str, Any]]: - """Get episode information from Sonarr including import history""" + 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: series_data = self.sonarr.series_by_imdb(imdb_id) if not series_data: @@ -154,11 +177,20 @@ class TVProcessor: episodes = self.sonarr.episodes_for_series(series_id) + # Convert episodes_filter to set for faster lookup + filter_set = set(episodes_filter) if episodes_filter else None + episode_map = {} + api_calls_made = 0 + for episode in episodes: season = episode.get('seasonNumber', 0) episode_num = episode.get('episodeNumber', 0) + # Skip episodes not in filter if filter is provided + if filter_set and (season, episode_num) not in filter_set: + continue + if season > 0 and episode_num > 0: # Get basic episode info episode_data = { @@ -170,6 +202,7 @@ class TVProcessor: episode_id = episode.get('id') if episode_id and episode.get('hasFile'): import_date = self.sonarr.get_episode_import_history(episode_id) + api_calls_made += 1 if import_date: episode_data['dateAdded'] = import_date _log("DEBUG", f"Got import date from history for S{season:02d}E{episode_num:02d}: {import_date}") @@ -183,6 +216,9 @@ class TVProcessor: episode_map[(season, episode_num)] = episode_data + if filter_set: + _log("DEBUG", f"Made {api_calls_made} Sonarr history API calls for filtered episodes (instead of all episodes)") + return episode_map except Exception as e: