Ampel / ampel (push) Successful in 54s
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>
117 lines
4.1 KiB
Python
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:*")
|