Files
rippy/docker/api/clients/thetvdb.py
HitonabiandClaude Opus 5 878b24c43b
Ampel / ampel (push) Successful in 54s
fix: die Metadaten-Suche war seit V2-1 tot — auf Windows UND auf der VM
Der Commander: "TMDB und OMDB Key sind hinterlegt, das laufwerk wird aber
auch nicht korrekt ausgelesen. Eigentlich sollte er direkt bei TMDB oder OMDB
oder JIKAN anfragen nach metadaten."

Er hat das Symptom gemeldet. Darunter lagen VIER Fehler.

## 1. Ein Phantom-Modul (der schwerste)

    clients/tmdb.py:27     from db import get_settings
    clients/omdb.py:36     from db import get_settings
    clients/thetvdb.py:16  from db import get_settings

`docker/api/db.py` gibt es seit der Zusammenlegung in V2-1 (dd1d0b7) nicht
mehr; main.py schreibt seitdem `from rippy import store as db`. Diese drei
Module haben es nie mitbekommen.

Damit starb JEDE Metadaten-Abfrage schon beim Erzeugen des Clients mit
ModuleNotFoundError -- und `_auto_prescan` verschluckte das in seinem
`except Exception`. Die Disc blieb namenlos, und niemand sah warum.

**Das betrifft die VM genauso.** Sie steht auf 05ab655, also NACH dd1d0b7 --
dort laeuft seit V2-1 dieselbe tote Metadaten-Suche.

## 2. Redis war Pflicht statt Beschleunigung

`cache/cache.py` sprach `redis:6379` an -- den Dienstnamen aus
docker-compose.yml. Auf einem Windows-PC gibt es kein Redis:

    redis.exceptions.ConnectionError: Error 11001 connecting to redis:6379

Das riss den Pre-Scan mit. Ein Zwischenspeicher ist eine Beschleunigung,
keine Voraussetzung: Jetzt faengt jeder Zugriff den Verbindungsfehler ab und
meldet "nicht vorhanden" -- was der Wahrheit entspricht. Gemeldet wird der
Ausfall EINMAL, nicht bei jeder Anfrage.

## 3. Der Klartext-Titel war unter Windows nicht erreichbar

`read_disc_title_via_mount` machte fest `mount -t udf` -- also Linux. Unter
Windows haengt Windows die Disc selbst ein. An der echten Disc gemessen:

    Volume-Label (UDF)          BD_EVG_D2      <- damit findet keine API etwas
    BDMV/META/DL/bdmt_deu.xml   Evangelion 2.22

`disc_wurzel()` liefert jetzt je Plattform einen lesbaren Einstieg, und
`titel_aus_bdmt()` ist davon getrennt (und damit ohne Disc pruefbar).

## 4. Modell und Seriennummer fehlten

Im UI stand "Modell: unbekannt, Seriennummer: -". Der Linux-Treiber liest
beides aus /sys; der Windows-Treiber lieferte leere Felder. Die Entsprechung
ist IOCTL_STORAGE_QUERY_PROPERTY. An echter Hardware gemessen:

    hersteller     'HL-DT-ST'
    modell         'BD-RE BU40N'
    fassung        '1.03'
    seriennummer   '0025114C0149'

Gemerkt statt jedes Mal abgefragt: Die Disc-Wache ruft device_info alle drei
Sekunden, und die Angaben eines Laufwerks aendern sich nicht.

## Ergebnis, an der eingelegten Disc gemessen

    title        'Neon Genesis Evangelion'
    year         1995
    disc_type    'Blu-ray'
    confidence   0.8
    fingerprint  'BD_EVG_D2|48149364736'

(Jikan antwortete waehrend der Messung mit 504 -- deren Ausfall, nicht
unserer. Der Treffer kam von TMDB.)

Zwei Waechter, beide beim Zurueckdrehen rot gesehen: Die Clients muessen sich
OHNE Datenbank und OHNE Redis bauen lassen, und ein `from db import` in
clients/ faellt mechanisch auf.

Ampel lokal: 769 gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 15:59:47 +02:00

117 lines
4.1 KiB
Python

"""TheTVDB API Client mit Caching."""
import requests
from typing import Dict, List, Optional
from config import settings
from cache import get, set, delete
THETVDB_BASE_URL = "https://api.thetvdb.com"
class TheTVDBClient:
def __init__(self):
# DB-Einstellung (Settings-UI/Wizard) gewinnt gegen die Env-Variable
# ⚠️ rippy.store, NICHT db (Befund 28.08.2026). docker/api/db.py
# gibt es seit V2-1 (dd1d0b7) nicht mehr — main.py schreibt seitdem
# "from rippy import store as db", dieses Modul hat es nie
# mitbekommen. Jede Abfrage starb beim Erzeugen des Clients, und
# _auto_prescan verschluckte es. Siehe clients/tmdb.py.
from rippy.store import get_settings
self.api_key = get_settings(bei_fehler_leer=True).get("tvdbApiKey") or settings.thetvdb_api_key
self.base_url = THETVDB_BASE_URL
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
})
self._cached_token = None
def _get_token(self) -> str:
"""Hole API-Token (gecachted)."""
if self._cached_token:
return self._cached_token
try:
response = self.session.post(
f"{self.base_url}/login",
json={"apikey": self.api_key},
timeout=10
)
response.raise_for_status()
data = response.json()
if "token" in data:
self._cached_token = data["token"]
return self._cached_token
except Exception as e:
print(f"TheTVDB Login Error: {e}")
return ""
def _request(self, endpoint: str, params: Dict = None) -> Optional[Dict]:
"""Mache API-Request mit Caching."""
cache_key = f"thetvdb:{endpoint}:{params or {}}"
cached = get(cache_key)
if cached:
return cached
try:
self.session.headers.update({
"Authorization": f"Bearer {self._get_token()}"
})
response = self.session.get(
f"{self.base_url}/{endpoint}",
params=params,
timeout=10
)
response.raise_for_status()
data = response.json()
set(cache_key, data)
return data
except Exception as e:
print(f"TheTVDB API Error: {e}")
return None
def search_series(self, name: str) -> List[Dict]:
"""Suche Serien."""
params = {"name": name}
result = self._request("search/series", params)
if result and "data" in result:
return result["data"]
return []
def get_series_details(self, series_id: int) -> Optional[Dict]:
"""Hole Serien-Details."""
return self._request(f"series/{series_id}")
def get_episode_details(self, series_id: int, episode_id: int) -> Optional[Dict]:
"""Hole Episode-Details."""
return self._request(f"series/{series_id}/episodes/{episode_id}")
def get_series_images(self, series_id: int) -> Dict[str, str]:
"""Hole Serien-Poster/Fanart URLs."""
params = {"type": "poster"}
result = self._request(f"series/{series_id}/images/query", params)
if result and "data" in result:
images = result["data"]
# Finde Poster und Fanart
poster = next((img for img in images if img.get("subKey") == "poster"), None)
fanart = next((img for img in images if img.get("subKey") == "fanart"), None)
return {
"poster": f"{self.base_url}/images/series/{series_id}_{poster['fileName']}" if poster else "",
"fanart": f"{self.base_url}/images/series/{series_id}_{fanart['fileName']}" if fanart else ""
}
return {}
def clear_cache(self) -> None:
"""Leere TheTVDB-Cache."""
delete("thetvdb:*")