Files
rippy/docker/api/clients/omdb.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

152 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 '20052012' (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