From 60bd37e8c2e1bf99d1d1c959c7c098a5db3aeb0c Mon Sep 17 00:00:00 2001 From: sbcrumb Date: Fri, 10 Oct 2025 14:16:05 -0400 Subject: [PATCH 1/3] Fix rename webhook targeting using episodeId lookup Found that rename events contain episodeId field which can be used to fetch specific episode details. Replaced complex episode data extraction with direct API call to /api/v3/episode/{episodeId} to get season/episode numbers for targeted processing. This should resolve rename webhooks processing entire series instead of single episodes. --- VERSION | 2 +- api/routes.py | 75 +++++++++++++++++++++------------------------------ 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/VERSION b/VERSION index c945ef1..ed2af5c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.18 +2.0.19 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: -- 2.39.5 From b10055397367af7b7353d69003686620b069df9d Mon Sep 17 00:00:00 2001 From: sbcrumb Date: Fri, 10 Oct 2025 15:27:06 -0400 Subject: [PATCH 2/3] Optimize TV processor performance with database-first episode lookup Major performance improvement for manual scans: - Check NFOGuard database before making Sonarr API calls - Only query Sonarr history for episodes missing from database - Filter Sonarr episode processing to only needed episodes - Add detailed logging for cache hits vs API calls Expected improvement: 172 API calls -> 0-10 API calls for typical scans --- VERSION | 2 +- processors/tv_processor.py | 100 +++++++++++++++++++++++++------------ 2 files changed, 69 insertions(+), 33 deletions(-) diff --git a/VERSION b/VERSION index ed2af5c..d17d1f3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.19 +2.0.20 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: -- 2.39.5 From a82652cbb9cc8ceb28291d7b64ee073c0f2f1015 Mon Sep 17 00:00:00 2001 From: sbcrumb Date: Fri, 10 Oct 2025 15:28:45 -0400 Subject: [PATCH 3/3] Bump version to 2.0.21 to resolve merge conflict with dev branch --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index d17d1f3..7d5624f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.20 +2.0.21 -- 2.39.5