From 2e6d8a35e777e14c9c0859ba3a1a6f87e0402fb8 Mon Sep 17 00:00:00 2001 From: SBCrumb Date: Fri, 26 Sep 2025 13:56:57 -0400 Subject: [PATCH] feat: Add external API fallback for TV episode airdates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Sonarr lookup fails to find a series or episode data, the system now falls back to external APIs (TMDB, OMDb) to get episode air dates. This ensures episodes still get proper dateadded values even when not found in Sonarr. Changes: - Add ExternalClientManager integration to TVSeriesProcessor - Implement _get_episode_airdate_from_external_apis method - Support TMDB TV episode lookups with IMDb->TMDB ID conversion - Support OMDb episode season lookups with date parsing - Version bump to 2.0.3-external-fallback 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- VERSION | 2 +- processors/tv_series_processor.py | 65 +++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 6a66477..9e69e54 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.2-clean +2.0.3-external-fallback diff --git a/processors/tv_series_processor.py b/processors/tv_series_processor.py index 6488e06..a068244 100644 --- a/processors/tv_series_processor.py +++ b/processors/tv_series_processor.py @@ -13,6 +13,7 @@ from core.episode_nfo_manager import EpisodeNFOManager from core.nfo_manager import NFOManager from core.path_mapper import PathMapper from clients.sonarr_client import SonarrClient +from clients.external_clients import ExternalClientManager from core.logging import _log, convert_utc_to_local @@ -26,6 +27,7 @@ class TVSeriesProcessor: self.episode_nfo_manager = EpisodeNFOManager() self.path_mapper = path_mapper self.sonarr = sonarr_client + self.external_manager = ExternalClientManager() def process_series_manual_scan(self, series_path: Path) -> bool: """Process a TV series during manual scan""" @@ -228,6 +230,13 @@ class TVSeriesProcessor: except Exception as e: _log("ERROR", f"Sonarr API error for S{season_num:02d}E{episode_num:02d}: {e}") + # Try external APIs for episode airdate + _log("INFO", f"Trying external APIs for episode S{season_num:02d}E{episode_num:02d} airdate") + aired, source = self._get_episode_airdate_from_external_apis(imdb_id, season_num, episode_num) + if aired: + dateadded = convert_utc_to_local(aired) + return aired, dateadded, source + _log("ERROR", f"Could not get any date information for S{season_num:02d}E{episode_num:02d}") return aired, dateadded, source @@ -251,6 +260,62 @@ class TVSeriesProcessor: return None, None + def _get_episode_airdate_from_external_apis(self, imdb_id: str, season_num: int, episode_num: int) -> Tuple[Optional[str], str]: + """Get episode airdate from external APIs (TMDB, OMDb) as fallback""" + # Try TMDB first + if self.external_manager.tmdb.enabled: + try: + _log("DEBUG", f"Trying TMDB for episode S{season_num:02d}E{episode_num:02d} airdate") + + # First convert IMDb to TMDB TV ID + tv_search = self.external_manager.tmdb._get(f"/find/{imdb_id}", {"external_source": "imdb_id"}) + if tv_search and tv_search.get("tv_results"): + tv_id = tv_search["tv_results"][0].get("id") + if tv_id: + _log("DEBUG", f"Found TMDB TV ID {tv_id} for {imdb_id}") + + # Get episode details + episode_data = self.external_manager.tmdb._get(f"/tv/{tv_id}/season/{season_num}/episode/{episode_num}") + if episode_data and episode_data.get("air_date"): + airdate = episode_data["air_date"] + # Convert to ISO format with UTC timezone + iso_airdate = f"{airdate}T00:00:00Z" + _log("INFO", f"Found TMDB airdate for S{season_num:02d}E{episode_num:02d}: {iso_airdate}") + return iso_airdate, "tmdb:episode.air_date" + except Exception as e: + _log("WARNING", f"TMDB episode lookup failed for S{season_num:02d}E{episode_num:02d}: {e}") + + # Try OMDb as fallback + if self.external_manager.omdb.enabled: + try: + _log("DEBUG", f"Trying OMDb for episode S{season_num:02d}E{episode_num:02d} airdate") + episode_dates = self.external_manager.omdb.get_tv_season_episodes(imdb_id, season_num) + if episode_num in episode_dates: + airdate = episode_dates[episode_num] + # Convert to ISO format + from datetime import datetime, timezone + try: + # Try to parse OMDb date format (usually DD MMM YYYY) + dt = datetime.strptime(airdate, "%d %b %Y").replace(tzinfo=timezone.utc) + iso_airdate = dt.isoformat(timespec="seconds") + _log("INFO", f"Found OMDb airdate for S{season_num:02d}E{episode_num:02d}: {iso_airdate}") + return iso_airdate, "omdb:episode.released" + except ValueError: + # Try other common formats + for fmt in ["%Y-%m-%d", "%d %B %Y"]: + try: + dt = datetime.strptime(airdate, fmt).replace(tzinfo=timezone.utc) + iso_airdate = dt.isoformat(timespec="seconds") + _log("INFO", f"Found OMDb airdate for S{season_num:02d}E{episode_num:02d}: {iso_airdate}") + return iso_airdate, "omdb:episode.released" + except ValueError: + continue + except Exception as e: + _log("WARNING", f"OMDb episode lookup failed for S{season_num:02d}E{episode_num:02d}: {e}") + + _log("WARNING", f"No external API airdate found for S{season_num:02d}E{episode_num:02d}") + return None, "no_external_data" + def _create_series_nfos(self, series_path: Path, imdb_id: str): """Create tvshow.nfo and season.nfo files""" # Create tvshow.nfo