feat: Add external API fallback for TV episode airdates
Local Docker Build (Dev) / build-dev (push) Successful in 19s
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:
@@ -13,6 +13,7 @@ from core.episode_nfo_manager import EpisodeNFOManager
|
|||||||
from core.nfo_manager import NFOManager
|
from core.nfo_manager import NFOManager
|
||||||
from core.path_mapper import PathMapper
|
from core.path_mapper import PathMapper
|
||||||
from clients.sonarr_client import SonarrClient
|
from clients.sonarr_client import SonarrClient
|
||||||
|
from clients.external_clients import ExternalClientManager
|
||||||
from core.logging import _log, convert_utc_to_local
|
from core.logging import _log, convert_utc_to_local
|
||||||
|
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ class TVSeriesProcessor:
|
|||||||
self.episode_nfo_manager = EpisodeNFOManager()
|
self.episode_nfo_manager = EpisodeNFOManager()
|
||||||
self.path_mapper = path_mapper
|
self.path_mapper = path_mapper
|
||||||
self.sonarr = sonarr_client
|
self.sonarr = sonarr_client
|
||||||
|
self.external_manager = ExternalClientManager()
|
||||||
|
|
||||||
def process_series_manual_scan(self, series_path: Path) -> bool:
|
def process_series_manual_scan(self, series_path: Path) -> bool:
|
||||||
"""Process a TV series during manual scan"""
|
"""Process a TV series during manual scan"""
|
||||||
@@ -228,6 +230,13 @@ class TVSeriesProcessor:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
_log("ERROR", f"Sonarr API error for S{season_num:02d}E{episode_num:02d}: {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}")
|
_log("ERROR", f"Could not get any date information for S{season_num:02d}E{episode_num:02d}")
|
||||||
return aired, dateadded, source
|
return aired, dateadded, source
|
||||||
|
|
||||||
@@ -251,6 +260,62 @@ class TVSeriesProcessor:
|
|||||||
|
|
||||||
return None, None
|
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):
|
def _create_series_nfos(self, series_path: Path, imdb_id: str):
|
||||||
"""Create tvshow.nfo and season.nfo files"""
|
"""Create tvshow.nfo and season.nfo files"""
|
||||||
# Create tvshow.nfo
|
# Create tvshow.nfo
|
||||||
|
|||||||
Reference in New Issue
Block a user