diff --git a/.env.secrets.template b/.env.secrets.template index 8622795..3efc95a 100644 --- a/.env.secrets.template +++ b/.env.secrets.template @@ -25,4 +25,9 @@ RADARR_API_KEY=your_radarr_api_key SONARR_API_KEY=your_sonarr_api_key # Jellyseerr API key (optional) -JELLYSEERR_API_KEY=your_jellyseerr_api_key \ No newline at end of file +JELLYSEERR_API_KEY=your_jellyseerr_api_key + +# TVDB API key (RECOMMENDED for v0.6.1+ Emby TV NFO Compatibility) +# Required to convert IMDB IDs to TVDB IDs for proper Emby metadata loading +# Get free API key at: https://thetvdb.com/api-information +TVDB_API_KEY=your_tvdb_api_key \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index fd50ece..0316ffc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,9 +3,12 @@ FROM python:3.11-slim # Set working directory WORKDIR /app -# Single layer for all system setup to minimize image size +# Set timezone and install system packages +ENV TZ=America/New_York RUN apt-get update && apt-get install -y --no-install-recommends \ - sqlite3 curl gosu git \ + sqlite3 curl gosu git tzdata \ + && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ + && echo $TZ > /etc/timezone \ && rm -rf /var/lib/apt/lists/* \ && apt-get clean \ && groupadd -g 1000 appuser \ diff --git a/clients/external_clients.py b/clients/external_clients.py index c273d90..def823a 100644 --- a/clients/external_clients.py +++ b/clients/external_clients.py @@ -42,6 +42,64 @@ def _parse_date_to_iso(date_str: str) -> Optional[str]: return None +class TVDBClient: + """The TV Database API client for IMDB to TVDB ID conversion""" + + def __init__(self, api_key: str = None): + self.api_key = api_key or os.environ.get("TVDB_API_KEY", "") + self.base_url = "https://api4.thetvdb.com/v4" + self._token = None + self._token_expires = 0 + + def _get_token(self) -> Optional[str]: + """Get TVDB auth token (cached)""" + if not self.api_key: + return None + + if time.time() < self._token_expires and self._token: + return self._token + + try: + req = UrlRequest( + f"{self.base_url}/login", + data=json.dumps({"apikey": self.api_key}).encode('utf-8'), + headers={"Content-Type": "application/json"} + ) + with urlopen(req, timeout=10) as resp: + data = json.loads(resp.read().decode("utf-8")) + if data.get("status") == "success": + self._token = data["data"]["token"] + self._token_expires = time.time() + 3600 # 1 hour + return self._token + except Exception as e: + _log("WARNING", f"TVDB login failed: {e}") + return None + + def imdb_to_tvdb_series_id(self, imdb_id: str) -> Optional[str]: + """Convert IMDB ID to TVDB series ID""" + token = self._get_token() + if not token: + return None + + try: + # Search by remote ID (IMDB) + url = f"{self.base_url}/search/remoteid?remoteId={imdb_id}&type=series" + headers = {"Authorization": f"Bearer {token}"} + + data = _get_json(url, headers=headers) + if data and data.get("status") == "success" and data.get("data"): + series_list = data["data"] + if series_list and len(series_list) > 0: + tvdb_id = series_list[0].get("id") + if tvdb_id: + _log("INFO", f"TVDB: Converted {imdb_id} → {tvdb_id}") + return str(tvdb_id) + except Exception as e: + _log("WARNING", f"TVDB IMDB conversion failed for {imdb_id}: {e}") + + return None + + class TMDBClient: """The Movie Database API client""" @@ -346,6 +404,7 @@ class ExternalClientManager: self.tmdb = TMDBClient(primary_country=tmdb_country) self.omdb = OMDbClient() self.jellyseerr = JellyseerrClient() + self.tvdb = TVDBClient() def get_release_date_by_priority(self, imdb_id: str, priority_order: List[str], enable_smart_validation: bool = True) -> Optional[Tuple[str, str]]: """Get release date using configurable priority order with smart date validation""" @@ -461,6 +520,14 @@ class ExternalClientManager: """Get the earliest digital release date (legacy method)""" candidates = self.get_digital_release_candidates(imdb_id) return candidates[0] if candidates else None + + def get_tvdb_series_id(self, imdb_id: str) -> Optional[str]: + """Get TVDB series ID from IMDB ID""" + if not self.tvdb.api_key: + _log("WARNING", "TVDB API key not configured, skipping TVDB ID lookup") + return None + + return self.tvdb.imdb_to_tvdb_series_id(imdb_id) if __name__ == "__main__": diff --git a/core/nfo_manager.py b/core/nfo_manager.py index 670028f..6213cc7 100644 --- a/core/nfo_manager.py +++ b/core/nfo_manager.py @@ -310,21 +310,33 @@ class NFOManager: except Exception as e: _log("WARNING", f"Could not write season.nfo in {season_dir}: {e}") - def create_tvshow_nfo(self, series_dir: Path, imdb_id: str): + def create_tvshow_nfo(self, series_dir: Path, imdb_id: str, tvdb_id: str = None): """Create tvshow.nfo if it doesn't exist""" tvshow_nfo = series_dir / "tvshow.nfo" if tvshow_nfo.exists(): return + # Build uniqueid elements - prioritize TVDB for Emby compatibility + uniqueid_elements = [] + if tvdb_id: + uniqueid_elements.append(f' {tvdb_id}') + if imdb_id: + uniqueid_elements.append(f' {imdb_id}') + + # Fallback to IMDB if no TVDB ID available + main_id = tvdb_id if tvdb_id else imdb_id + xml_content = f""" - {imdb_id} - {imdb_id} + {main_id} +{chr(10).join(uniqueid_elements)} + + """ try: tvshow_nfo.write_text(xml_content, encoding="utf-8") - _log("DEBUG", f"Created tvshow NFO: {tvshow_nfo}") + _log("DEBUG", f"Created tvshow NFO: {tvshow_nfo} (TVDB: {tvdb_id}, IMDB: {imdb_id})") except Exception as e: _log("WARNING", f"Could not create tvshow.nfo in {series_dir}: {e}") diff --git a/nfoguard.py b/nfoguard.py index 3631f87..80f633c 100644 --- a/nfoguard.py +++ b/nfoguard.py @@ -299,7 +299,9 @@ class TVProcessor: self.nfo_manager.create_season_nfo(season_dir, season) seasons_processed.add(season) - self.nfo_manager.create_tvshow_nfo(series_path, imdb_id) + # Get TVDB ID for better Emby compatibility + tvdb_id = self.external_clients.get_tvdb_series_id(imdb_id) + self.nfo_manager.create_tvshow_nfo(series_path, imdb_id, tvdb_id) _log("INFO", f"Completed processing TV series: {series_path.name}") @@ -481,7 +483,9 @@ class TVProcessor: # Create season NFO if config.manage_nfo: self.nfo_manager.create_season_nfo(season_path, season_num) - self.nfo_manager.create_tvshow_nfo(series_path, imdb_id) + # Get TVDB ID for better Emby compatibility + tvdb_id = self.external_clients.get_tvdb_series_id(imdb_id) + self.nfo_manager.create_tvshow_nfo(series_path, imdb_id, tvdb_id) _log("INFO", f"Completed processing season {season_num}") @@ -729,7 +733,9 @@ class TVProcessor: self.nfo_manager.create_season_nfo(season_dir, season_num) seasons_processed.add(season_num) - self.nfo_manager.create_tvshow_nfo(series_path, imdb_id) + # Get TVDB ID for better Emby compatibility + tvdb_id = self.external_clients.get_tvdb_series_id(imdb_id) + self.nfo_manager.create_tvshow_nfo(series_path, imdb_id, tvdb_id) _log("INFO", f"Completed targeted processing: {episodes_processed}/{len(webhook_episodes)} episodes processed")