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
+23
View File
@@ -73,6 +73,29 @@ IOCTL_STORAGE_EJECT_MEDIA = ctl_code(
IOCTL_STORAGE_LOAD_MEDIA = ctl_code(
FILE_DEVICE_MASS_STORAGE, 0x0203, METHOD_BUFFERED, FILE_READ_ACCESS)
# Hersteller, Modell und Seriennummer des LAUFWERKS (nicht der Disc).
#
# Die Entsprechung von `/sys/class/block/<name>/device/{vendor,model,wwid}`
# unter Linux. Ohne diesen Aufruf stand im UI „Modell: unbekannt,
# Seriennummer: " — der Commander hat es am 28.08.2026 gemeldet.
#
# Erwartet eine STORAGE_PROPERTY_QUERY (PropertyId=0 StorageDeviceProperty,
# QueryType=0 PropertyStandardQuery) und antwortet mit einem
# STORAGE_DEVICE_DESCRIPTOR: feste Kopfdaten mit OFFSETS auf die Zeichenketten,
# die dahinter im selben Puffer liegen.
IOCTL_STORAGE_QUERY_PROPERTY = ctl_code(
FILE_DEVICE_MASS_STORAGE, 0x0500, METHOD_BUFFERED, FILE_ANY_ACCESS)
STORAGE_DEVICE_PROPERTY = 0
PROPERTY_STANDARD_QUERY = 0
# Feldpositionen im STORAGE_DEVICE_DESCRIPTOR (winioctl.h), in Bytes.
SDD_VENDOR_ID_OFFSET = 12
SDD_PRODUCT_ID_OFFSET = 16
SDD_PRODUCT_REVISION_OFFSET = 20
SDD_SERIAL_NUMBER_OFFSET = 24
# ── CD-ROM (IOCTL_CDROM_*) ──────────────────────────────────────────────
# Audio- oder Datenspur? Grob, aber genau das, was CDROM_DISC_STATUS auch
# liefert — mehr braucht die Einordnung nicht (die Feinunterscheidung
+78 -2
View File
@@ -376,6 +376,66 @@ def eject(geraet: str, api=None) -> None:
# ── Für das UI ──────────────────────────────────────────────────────────
def _text_bei(puffer: bytes, offset_pos: int) -> str:
"""Eine nullterminierte Zeichenkette, auf die ein Offset im Puffer zeigt.
Reine Funktion, damit die Auswertung ohne Laufwerk prüfbar ist. Offset 0
heißt „gibt es nicht" — das ist der dokumentierte Weg, wie ein
STORAGE_DEVICE_DESCRIPTOR ein fehlendes Feld meldet.
"""
if len(puffer) < offset_pos + 4:
return ""
offset = int.from_bytes(puffer[offset_pos:offset_pos + 4], "little")
if not offset or offset >= len(puffer):
return ""
ende = puffer.find(b"\x00", offset)
roh = puffer[offset:ende if ende >= 0 else len(puffer)]
return roh.decode("latin-1", errors="replace").strip()
def geraeteangaben(geraet: str, api=None) -> dict:
"""Hersteller, Modell und Seriennummer des LAUFWERKS.
## Warum es das gibt (Commander-Befund 28.08.2026)
Im UI stand:
Modell unbekannt
Seriennummer
Der Linux-Treiber liest beides aus `/sys/class/block/<name>/device/`; der
Windows-Treiber lieferte schlicht leere Felder. Die Entsprechung ist
`IOCTL_STORAGE_QUERY_PROPERTY` — es antwortet mit einem
`STORAGE_DEVICE_DESCRIPTOR`, dessen Kopfdaten OFFSETS auf die
Zeichenketten dahinter enthalten.
Wirft nicht: Ein Laufwerk ohne Angaben ist ärgerlich, aber kein Grund,
die ganze Geräteliste scheitern zu lassen.
"""
api = _api(api)
abfrage = (w.STORAGE_DEVICE_PROPERTY.to_bytes(4, "little")
+ w.PROPERTY_STANDARD_QUERY.to_bytes(4, "little")
+ b"\x00" * 4)
try:
handle = api.oeffnen(geraet)
except OSError:
return {}
try:
puffer = api.steuern(handle, w.IOCTL_STORAGE_QUERY_PROPERTY,
abfrage, 1024)
except OSError:
return {}
finally:
api.schliessen(handle)
return {
"hersteller": _text_bei(puffer, w.SDD_VENDOR_ID_OFFSET),
"modell": _text_bei(puffer, w.SDD_PRODUCT_ID_OFFSET),
"fassung": _text_bei(puffer, w.SDD_PRODUCT_REVISION_OFFSET),
"seriennummer": _text_bei(puffer, w.SDD_SERIAL_NUMBER_OFFSET),
}
def device_info(geraet: str, api=None) -> dict:
"""Der Geräte-Eintrag fürs UI — gleiche Felder wie beim Linux-Treiber.
@@ -400,12 +460,28 @@ def device_info(geraet: str, api=None) -> dict:
status = "unknown"
buchstabe = geraet.rstrip(":").rsplit("\\", 1)[-1].rstrip(":")
angaben = _angaben_gemerkt(geraet, api)
modell = " ".join(t for t in (angaben.get("hersteller"),
angaben.get("modell")) if t)
return {
"id": buchstabe,
"name": f"Laufwerk {buchstabe}:",
"type": disc_typ,
"path": geraet,
"status": status,
"model": "",
"serial": "",
"model": modell,
"serial": angaben.get("seriennummer", ""),
}
# Die Angaben eines Laufwerks aendern sich nicht, solange es dasselbe
# Laufwerk ist -- die Disc-Wache ruft `device_info` aber alle drei Sekunden.
# Ohne dieses Gedaechtnis waere das alle drei Sekunden ein CreateFileW plus
# DeviceIoControl auf ein Geraet, das gerade rippt.
_ANGABEN_SPEICHER: dict = {}
def _angaben_gemerkt(geraet: str, api=None) -> dict:
if geraet not in _ANGABEN_SPEICHER:
_ANGABEN_SPEICHER[geraet] = geraeteangaben(geraet, api) or {}
return _ANGABEN_SPEICHER[geraet]