updates
This commit is contained in:
@@ -26,3 +26,8 @@ SONARR_API_KEY=your_sonarr_api_key
|
|||||||
|
|
||||||
# Jellyseerr API key (optional)
|
# Jellyseerr API key (optional)
|
||||||
JELLYSEERR_API_KEY=your_jellyseerr_api_key
|
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
|
||||||
+5
-2
@@ -3,9 +3,12 @@ FROM python:3.11-slim
|
|||||||
# Set working directory
|
# Set working directory
|
||||||
WORKDIR /app
|
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 \
|
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/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& groupadd -g 1000 appuser \
|
&& groupadd -g 1000 appuser \
|
||||||
|
|||||||
@@ -42,6 +42,64 @@ def _parse_date_to_iso(date_str: str) -> Optional[str]:
|
|||||||
return None
|
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:
|
class TMDBClient:
|
||||||
"""The Movie Database API client"""
|
"""The Movie Database API client"""
|
||||||
|
|
||||||
@@ -346,6 +404,7 @@ class ExternalClientManager:
|
|||||||
self.tmdb = TMDBClient(primary_country=tmdb_country)
|
self.tmdb = TMDBClient(primary_country=tmdb_country)
|
||||||
self.omdb = OMDbClient()
|
self.omdb = OMDbClient()
|
||||||
self.jellyseerr = JellyseerrClient()
|
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]]:
|
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"""
|
"""Get release date using configurable priority order with smart date validation"""
|
||||||
@@ -462,6 +521,14 @@ class ExternalClientManager:
|
|||||||
candidates = self.get_digital_release_candidates(imdb_id)
|
candidates = self.get_digital_release_candidates(imdb_id)
|
||||||
return candidates[0] if candidates else None
|
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__":
|
if __name__ == "__main__":
|
||||||
# Test the clients
|
# Test the clients
|
||||||
|
|||||||
+16
-4
@@ -310,21 +310,33 @@ class NFOManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
_log("WARNING", f"Could not write season.nfo in {season_dir}: {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"""
|
"""Create tvshow.nfo if it doesn't exist"""
|
||||||
tvshow_nfo = series_dir / "tvshow.nfo"
|
tvshow_nfo = series_dir / "tvshow.nfo"
|
||||||
if tvshow_nfo.exists():
|
if tvshow_nfo.exists():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Build uniqueid elements - prioritize TVDB for Emby compatibility
|
||||||
|
uniqueid_elements = []
|
||||||
|
if tvdb_id:
|
||||||
|
uniqueid_elements.append(f' <uniqueid type="tvdb" default="true">{tvdb_id}</uniqueid>')
|
||||||
|
if imdb_id:
|
||||||
|
uniqueid_elements.append(f' <uniqueid type="imdb"{"" if tvdb_id else " default=\"true\"""}>{imdb_id}</uniqueid>')
|
||||||
|
|
||||||
|
# Fallback to IMDB if no TVDB ID available
|
||||||
|
main_id = tvdb_id if tvdb_id else imdb_id
|
||||||
|
|
||||||
xml_content = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
xml_content = f"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<tvshow>
|
<tvshow>
|
||||||
<id>{imdb_id}</id>
|
<id>{main_id}</id>
|
||||||
<uniqueid type="imdb" default="true">{imdb_id}</uniqueid>
|
{chr(10).join(uniqueid_elements)}
|
||||||
|
<!-- NFOGuard: TVDB ID required for proper Emby metadata loading -->
|
||||||
|
<!-- If missing TVDB ID, use: Dashboard > Series > Edit Metadata > External IDs -->
|
||||||
</tvshow>
|
</tvshow>
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
tvshow_nfo.write_text(xml_content, encoding="utf-8")
|
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:
|
except Exception as e:
|
||||||
_log("WARNING", f"Could not create tvshow.nfo in {series_dir}: {e}")
|
_log("WARNING", f"Could not create tvshow.nfo in {series_dir}: {e}")
|
||||||
|
|
||||||
|
|||||||
+9
-3
@@ -299,7 +299,9 @@ class TVProcessor:
|
|||||||
self.nfo_manager.create_season_nfo(season_dir, season)
|
self.nfo_manager.create_season_nfo(season_dir, season)
|
||||||
seasons_processed.add(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}")
|
_log("INFO", f"Completed processing TV series: {series_path.name}")
|
||||||
|
|
||||||
@@ -481,7 +483,9 @@ class TVProcessor:
|
|||||||
# Create season NFO
|
# Create season NFO
|
||||||
if config.manage_nfo:
|
if config.manage_nfo:
|
||||||
self.nfo_manager.create_season_nfo(season_path, season_num)
|
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}")
|
_log("INFO", f"Completed processing season {season_num}")
|
||||||
|
|
||||||
@@ -729,7 +733,9 @@ class TVProcessor:
|
|||||||
self.nfo_manager.create_season_nfo(season_dir, season_num)
|
self.nfo_manager.create_season_nfo(season_dir, season_num)
|
||||||
seasons_processed.add(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")
|
_log("INFO", f"Completed targeted processing: {episodes_processed}/{len(webhook_episodes)} episodes processed")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user