Optimize TV processor performance with database-first episode lookup
Local Docker Build (Dev) / build-dev (pull_request) Successful in 4s

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
This commit is contained in:
2025-10-10 15:27:06 -04:00
parent 60bd37e8c2
commit b100553973
2 changed files with 69 additions and 33 deletions
+1 -1
View File
@@ -1 +1 @@
2.0.19 2.0.20
+66 -30
View File
@@ -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]]: 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 = {} episode_dates = {}
episodes_needing_lookup = []
# Get data from Sonarr first if available # OPTIMIZATION: Check database first for existing dates
sonarr_episodes = self._get_sonarr_episodes(imdb_id) _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: for (season, episode) in disk_episodes:
aired = None # Try database first - this is much faster than API calls
dateadded = None db_result = self.db.get_episode_date(imdb_id, season, episode)
source = "unknown"
# Try Sonarr first if db_result and db_result.get('dateadded'):
if (season, episode) in sonarr_episodes: # Found in database - use cached data
sonarr_data = sonarr_episodes[(season, episode)] aired = db_result.get('aired')
aired = sonarr_data.get('airDate') dateadded = db_result.get('dateadded')
dateadded = sonarr_data.get('dateAdded') source = db_result.get('source', 'database_cache')
if dateadded: episode_dates[(season, episode)] = (aired, dateadded, source)
# Use more specific source to indicate this came from Sonarr import history db_cache_hits += 1
source = "sonarr:history.import" _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))
# Fallback to external sources if needed _log("INFO", f"Database cache hits: {db_cache_hits}/{len(disk_episodes)} episodes. Need API lookup: {len(episodes_needing_lookup)}")
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 # Only call Sonarr API for episodes not in database
# and if we couldn't get any import information from Sonarr history if episodes_needing_lookup:
if not dateadded and aired and source != "sonarr:history.import": _log("DEBUG", f"Querying Sonarr for {len(episodes_needing_lookup)} episodes missing from database")
dateadded = aired sonarr_episodes = self._get_sonarr_episodes(imdb_id, episodes_needing_lookup)
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 return episode_dates
def _get_sonarr_episodes(self, imdb_id: str) -> Dict[Tuple[int, int], Dict[str, Any]]: 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""" """Get episode information from Sonarr including import history - optimized to only fetch needed episodes"""
try: try:
series_data = self.sonarr.series_by_imdb(imdb_id) series_data = self.sonarr.series_by_imdb(imdb_id)
if not series_data: if not series_data:
@@ -154,11 +177,20 @@ class TVProcessor:
episodes = self.sonarr.episodes_for_series(series_id) 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 = {} episode_map = {}
api_calls_made = 0
for episode in episodes: for episode in episodes:
season = episode.get('seasonNumber', 0) season = episode.get('seasonNumber', 0)
episode_num = episode.get('episodeNumber', 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: if season > 0 and episode_num > 0:
# Get basic episode info # Get basic episode info
episode_data = { episode_data = {
@@ -170,6 +202,7 @@ class TVProcessor:
episode_id = episode.get('id') episode_id = episode.get('id')
if episode_id and episode.get('hasFile'): if episode_id and episode.get('hasFile'):
import_date = self.sonarr.get_episode_import_history(episode_id) import_date = self.sonarr.get_episode_import_history(episode_id)
api_calls_made += 1
if import_date: if import_date:
episode_data['dateAdded'] = 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}") _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 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 return episode_map
except Exception as e: except Exception as e: