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>
152 lines
5.7 KiB
Python
152 lines
5.7 KiB
Python
"""OMDb API Client — Fallback-Quelle für die Metadaten-Erkennung.
|
||
|
||
Kommt zum Zug, wenn TMDB nichts (Sicheres) findet. Kostenloser Key von
|
||
https://www.omdbapi.com/apikey.aspx (1000 Anfragen/Tag reichen fürs Heimlab).
|
||
"""
|
||
|
||
from typing import Dict, Optional
|
||
|
||
import requests
|
||
|
||
from cache import get, set as cache_set
|
||
from config import settings
|
||
|
||
OMDB_BASE_URL = "https://www.omdbapi.com/"
|
||
|
||
|
||
def parse_runtime_minutes(runtime: str) -> int:
|
||
"""OMDb liefert '142 min' als Text → Minuten als Zahl (pure Funktion, testbar)."""
|
||
if not runtime:
|
||
return 0
|
||
teil = runtime.strip().split(" ")[0]
|
||
return int(teil) if teil.isdigit() else 0
|
||
|
||
|
||
def parse_year(year: str) -> Optional[int]:
|
||
"""OMDb-Jahr kann '1999' oder '2005–2012' (Serie) sein → erstes Jahr."""
|
||
if not year:
|
||
return None
|
||
erste = year.strip()[:4]
|
||
return int(erste) if erste.isdigit() else None
|
||
|
||
|
||
class OMDbClient:
|
||
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("omdbApiKey") or settings.omdb_api_key
|
||
self.session = requests.Session()
|
||
|
||
def suche(self, title: str) -> list:
|
||
"""Roh-Kandidaten für die manuelle Korrektur (unscharfe s=-Suche)."""
|
||
if not self.api_key:
|
||
return []
|
||
try:
|
||
antwort = self.session.get(
|
||
OMDB_BASE_URL,
|
||
params={"apikey": self.api_key, "s": title, "r": "json"},
|
||
timeout=10,
|
||
).json()
|
||
except Exception:
|
||
return []
|
||
if antwort.get("Response") != "True":
|
||
return []
|
||
ergebnisse = []
|
||
for e in (antwort.get("Search") or [])[:5]:
|
||
poster = e.get("Poster", "")
|
||
ergebnisse.append({
|
||
"title": e.get("Title", ""),
|
||
"year": parse_year(e.get("Year", "")),
|
||
"poster": poster if poster != "N/A" else "",
|
||
"overview": "",
|
||
"type": "tv" if e.get("Type") == "series" else "movie",
|
||
"source": "omdb",
|
||
"id": e.get("imdbID", ""),
|
||
})
|
||
return ergebnisse
|
||
|
||
def lookup(self, title: str, year: Optional[int] = None) -> Optional[Dict]:
|
||
"""Sucht per Titel (t=), liefert normalisierte Metadaten oder None."""
|
||
if not self.api_key:
|
||
return None
|
||
|
||
cache_key = f"omdb:{title.lower()}:{year or ''}"
|
||
cached = get(cache_key)
|
||
if cached:
|
||
return cached
|
||
|
||
params = {"apikey": self.api_key, "t": title, "r": "json"}
|
||
if year:
|
||
params["y"] = str(year)
|
||
|
||
try:
|
||
response = self.session.get(OMDB_BASE_URL, params=params, timeout=10)
|
||
response.raise_for_status()
|
||
data = response.json()
|
||
|
||
# t= ist exakt — wenn leer, unscharfe Suche (s=) und den besten
|
||
# Treffer per imdbID nachladen
|
||
if data.get("Response") != "True":
|
||
suche = self.session.get(
|
||
OMDB_BASE_URL,
|
||
params={"apikey": self.api_key, "s": title, "r": "json"},
|
||
timeout=10,
|
||
).json()
|
||
treffer = (suche.get("Search") or []) if suche.get("Response") == "True" else []
|
||
if treffer:
|
||
# Bester Kandidat statt blind Treffer #1: echte Filme vor
|
||
# Episoden/Reviews, mit Poster vor ohne, kurzer Titel vor
|
||
# langem (Zusatz-Geschwafel wie "Episode 110 - …" verliert).
|
||
def rang(t: Dict):
|
||
return (
|
||
0 if t.get("Type") == "movie" else (1 if t.get("Type") == "series" else 2),
|
||
0 if t.get("Poster") not in (None, "", "N/A") else 1,
|
||
len(t.get("Title") or ""),
|
||
)
|
||
|
||
bester = sorted(treffer, key=rang)[0]
|
||
data = self.session.get(
|
||
OMDB_BASE_URL,
|
||
params={"apikey": self.api_key, "i": bester["imdbID"], "r": "json"},
|
||
timeout=10,
|
||
).json()
|
||
except Exception as e:
|
||
print(f"OMDb API Error: {e}")
|
||
return None
|
||
|
||
if data.get("Response") != "True":
|
||
return None
|
||
|
||
plot = data.get("Plot", "")
|
||
if plot == "N/A":
|
||
plot = ""
|
||
poster = data.get("Poster", "")
|
||
if poster == "N/A":
|
||
poster = ""
|
||
|
||
# Qualitäts-Gate: Einträge ohne Poster UND ohne Beschreibung sind
|
||
# fast immer Datenmüll (Review-Episoden etc.) — als Nicht-Treffer
|
||
# werten, damit die Kandidaten-Kette weiterprobiert.
|
||
if not poster and not plot:
|
||
return None
|
||
|
||
ergebnis = {
|
||
"type": "tv" if data.get("Type") == "series" else "movie",
|
||
"id": data.get("imdbID", ""),
|
||
"title": data.get("Title", title),
|
||
"year": parse_year(data.get("Year", "")),
|
||
"overview": plot,
|
||
"poster_path": poster,
|
||
"backdrop_path": "",
|
||
"runtime": parse_runtime_minutes(data.get("Runtime", "")),
|
||
"genres": [g.strip() for g in data.get("Genre", "").split(",") if g.strip() and g.strip() != "N/A"],
|
||
"source": "omdb",
|
||
}
|
||
cache_set(cache_key, ergebnis)
|
||
return ergebnis
|