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")