feat: Add external API fallback for TV episode airdates
Local Docker Build (Dev) / build-dev (push) Successful in 19s

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 <noreply@anthropic.com>
This commit is contained in:
2025-09-26 13:56:57 -04:00
parent 12767d7350
commit 2e6d8a35e7
2 changed files with 66 additions and 1 deletions
+65
View File
@@ -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