fix: die Metadaten-Suche war seit V2-1 tot — auf Windows UND auf der VM
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>
This commit is contained in:
Hitonabi
2026-08-28 15:59:47 +02:00
co-authored by Claude Opus 5
parent ea8a043643
commit 878b24c43b
8 changed files with 463 additions and 62 deletions
+79 -36
View File
@@ -80,50 +80,93 @@ def parse_udf_dstring(data: bytes) -> str:
return ""
def disc_wurzel(device_path: str):
r"""Ein lesbarer Einstieg in die Disc — Windows braucht dafuer kein Mounten.
Gibt `(wurzel, aufraeumen)` zurueck. `aufraeumen` ist eine Funktion, die
nach dem Lesen aufgerufen wird; unter Windows tut sie nichts.
## Warum das getrennt ist (28.08.2026)
Der Weg hier war fest `mount -t udf` — also Linux. Unter Windows haengt
Windows die Disc selbst ein: `\.\G:` entspricht `G:\`, und dort liegen
die Dateien einfach da. Ohne diese Unterscheidung fand Rippy unter
Windows NIE den Klartext-Titel und blieb beim Volume-Label haengen
(gemessen an einer echten Disc: `BD_EVG_D2` statt `Evangelion 2.22`) --
und mit so einem Label findet keine Metadaten-API etwas.
"""
if os.name == "nt":
# \.\G: -> G:\ (der Buchstabe ist alles, was wir brauchen)
buchstabe = device_path.rstrip(":").rsplit("\\", 1)[-1].rstrip(":")
wurzel = buchstabe + ":" + "\\"
return (wurzel, lambda: None) if os.path.isdir(wurzel) else (None, lambda: None)
mountpoint = "/mnt/rippy-disc"
os.makedirs(mountpoint, exist_ok=True)
ergebnis = subprocess.run(
["mount", "-t", "udf", "-o", "ro", device_path, mountpoint],
capture_output=True, text=True, timeout=30,
)
if ergebnis.returncode != 0:
return None, lambda: None
def abhaengen():
subprocess.run(["umount", mountpoint], capture_output=True, timeout=15)
return mountpoint, abhaengen
def titel_aus_bdmt(wurzel: str) -> Optional[str]:
"""Der Klartext-Titel aus BDMV/META/DL/bdmt_*.xml. (ohne Mounten)
Getrennt von der Beschaffung der Wurzel, damit er ohne Disc pruefbar ist:
ein Ordner mit einer bdmt-Datei reicht.
"""
import re as _re
meta_dir = os.path.join(wurzel, "BDMV", "META", "DL")
if not os.path.isdir(meta_dir):
return None
try:
kandidaten = sorted(os.listdir(meta_dir))
except OSError:
return None
# bdmt_eng.xml bevorzugen, sonst die erste bdmt-Datei (bdmt_deu.xml, ...)
bdmt = next((k for k in kandidaten if k == "bdmt_eng.xml"), None) or next(
(k for k in kandidaten if k.startswith("bdmt_") and k.endswith(".xml")), None
)
if not bdmt:
return None
try:
with open(os.path.join(meta_dir, bdmt), "rb") as f:
inhalt = f.read(65536).decode("utf-8", errors="replace")
except OSError:
return None
# <di:name>Titel</di:name> - bewusst per Regex statt XML-Parser
# (Namespaces variieren je Authoring-Werkzeug)
treffer = _re.search(r"<di:name>([^<]{2,120})</di:name>", inhalt)
return treffer.group(1).strip() if treffer else None
def read_disc_title_via_mount(device_path: str) -> Optional[str]:
"""Liest den KLARTEXT-Titel einer Blu-ray aus BDMV/META/DL/bdmt_*.xml.
Das Volume-Label ist oft kryptisch (BD_EVG_D2) der echte Titel
(„Evangelion: 2.22") steht in den Disc-Metadaten. Die API darf
read-only mounten (CAP_SYS_ADMIN ist für die Speicherziele ohnehin da).
Gibt None zurück, wenn kein BD-Metadatensatz existiert (z. B. DVD).
Das Volume-Label ist oft kryptisch (`BD_EVG_D2`) - der echte Titel
(„Evangelion 2.22") steht in den Disc-Metadaten. Unter Linux wird dafuer
read-only gemountet, unter Windows haengt Windows die Disc selbst ein.
Gibt None zurueck, wenn kein BD-Metadatensatz existiert (z. B. DVD).
"""
import re as _re
mountpoint = "/mnt/rippy-disc"
os.makedirs(mountpoint, exist_ok=True)
gemountet = False
wurzel, aufraeumen = None, lambda: None
try:
ergebnis = subprocess.run(
["mount", "-t", "udf", "-o", "ro", device_path, mountpoint],
capture_output=True, text=True, timeout=30,
)
if ergebnis.returncode != 0:
return None
gemountet = True
meta_dir = os.path.join(mountpoint, "BDMV", "META", "DL")
if not os.path.isdir(meta_dir):
return None
kandidaten = sorted(os.listdir(meta_dir))
# bdmt_eng.xml bevorzugen, sonst erste bdmt-Datei (bdmt_ger.xml, …)
bdmt = next((k for k in kandidaten if k == "bdmt_eng.xml"), None) or next(
(k for k in kandidaten if k.startswith("bdmt_") and k.endswith(".xml")), None
)
if not bdmt:
return None
with open(os.path.join(meta_dir, bdmt), "rb") as f:
inhalt = f.read(65536).decode("utf-8", errors="replace")
# <di:name>Titel</di:name> — bewusst per Regex statt XML-Parser
# (Namespaces variieren je Authoring-Werkzeug)
treffer = _re.search(r"<di:name>([^<]{2,120})</di:name>", inhalt)
if treffer:
return treffer.group(1).strip()
return None
wurzel, aufraeumen = disc_wurzel(device_path)
return titel_aus_bdmt(wurzel) if wurzel else None
except Exception:
return None
finally:
if gemountet:
subprocess.run(["umount", mountpoint], capture_output=True, timeout=15)
try:
aufraeumen()
except Exception:
pass
def titel_kandidaten(titel: str) -> List[str]: