fix: die Metadaten-Suche war seit V2-1 tot — auf Windows UND auf der VM
Ampel / ampel (push) Successful in 54s
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:
co-authored by
Claude Opus 5
parent
ea8a043643
commit
878b24c43b
@@ -0,0 +1,157 @@
|
||||
"""Die Metadaten-Clients muessen sich OHNE Datenbank und OHNE Redis bauen lassen.
|
||||
|
||||
## Warum es diese Tests gibt (Befund 28.08.2026)
|
||||
|
||||
Der Commander meldete, dass eine eingelegte Blu-ray namenlos blieb und keine
|
||||
Metadaten-Abfrage lief. Zwei Ursachen, beide unsichtbar:
|
||||
|
||||
**1. Ein Phantom-Modul.** `clients/tmdb.py`, `clients/omdb.py` und
|
||||
`clients/thetvdb.py` machten im Konstruktor `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`, auf Windows UND auf der
|
||||
VM. `_auto_prescan` verschluckte es in seinem `except Exception`.
|
||||
|
||||
**2. Ein harter Redis-Zwang.** `cache/cache.py` sprach `redis:6379` an — den
|
||||
Dienstnamen aus `docker-compose.yml`. Auf einem Windows-PC gibt es kein
|
||||
Redis, und jeder Zugriff endete mit `ConnectionError`. Das riss den Pre-Scan
|
||||
mit.
|
||||
|
||||
Beide Male war der SYMPTOM sichtbar (keine Metadaten) und die URSACHE nicht.
|
||||
Diese Tests machen die Ursache sichtbar: Sie bauen die Clients und benutzen
|
||||
den Zwischenspeicher, ohne dass irgendetwas davon erreichbar sein muss.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_tmdb_client_baut_sich_ohne_datenbank():
|
||||
"""Der Test, der den Phantom-Import gefangen haette."""
|
||||
from clients.tmdb import TMDBClient
|
||||
|
||||
klient = TMDBClient()
|
||||
assert hasattr(klient, "api_key")
|
||||
|
||||
|
||||
def test_omdb_client_baut_sich_ohne_datenbank():
|
||||
from clients.omdb import OMDbClient
|
||||
|
||||
assert OMDbClient() is not None
|
||||
|
||||
|
||||
def test_thetvdb_client_baut_sich_ohne_datenbank():
|
||||
from clients.thetvdb import TheTVDBClient
|
||||
|
||||
assert TheTVDBClient() is not None
|
||||
|
||||
|
||||
def test_kein_client_importiert_das_verschwundene_db_modul():
|
||||
"""Der Waechter gegen genau diesen Fehler, mechanisch.
|
||||
|
||||
Ein `from db import ...` faellt sonst erst auf, wenn jemand eine Disc
|
||||
einlegt — und dann als „keine Metadaten", nicht als Import-Fehler.
|
||||
"""
|
||||
import os
|
||||
|
||||
hier = os.path.dirname(os.path.abspath(__file__))
|
||||
fundstellen = []
|
||||
for wurzel, ordner, dateien in os.walk(os.path.join(hier, "clients")):
|
||||
ordner[:] = [o for o in ordner if o != "__pycache__"]
|
||||
for name in dateien:
|
||||
if not name.endswith(".py"):
|
||||
continue
|
||||
pfad = os.path.join(wurzel, name)
|
||||
with open(pfad, encoding="utf-8") as f:
|
||||
for nr, zeile in enumerate(f, 1):
|
||||
if zeile.strip().startswith(("from db import", "import db")):
|
||||
fundstellen.append("%s:%d" % (name, nr))
|
||||
assert not fundstellen, (
|
||||
"docker/api/db.py gibt es seit V2-1 nicht mehr — benutzt "
|
||||
"rippy.store. Fundstellen: " + ", ".join(fundstellen))
|
||||
|
||||
|
||||
# ── Der Zwischenspeicher ────────────────────────────────────────────────
|
||||
def test_cache_ohne_redis_liefert_None_statt_zu_werfen():
|
||||
"""Ein Zwischenspeicher ist eine Beschleunigung, keine Voraussetzung.
|
||||
|
||||
Auf einem Windows-PC gibt es kein Redis. Vorher riss jeder Zugriff den
|
||||
Pre-Scan mit — und die Disc blieb namenlos.
|
||||
"""
|
||||
from cache import cache
|
||||
|
||||
class ToterRedis:
|
||||
def get(self, *a, **k):
|
||||
raise cache.RedisError("nicht erreichbar")
|
||||
|
||||
def set(self, *a, **k):
|
||||
raise cache.RedisError("nicht erreichbar")
|
||||
|
||||
def setex(self, *a, **k):
|
||||
raise cache.RedisError("nicht erreichbar")
|
||||
|
||||
def delete(self, *a, **k):
|
||||
raise cache.RedisError("nicht erreichbar")
|
||||
|
||||
def flushdb(self, *a, **k):
|
||||
raise cache.RedisError("nicht erreichbar")
|
||||
|
||||
def ping(self, *a, **k):
|
||||
raise cache.RedisError("nicht erreichbar")
|
||||
|
||||
echt = cache.redis_client
|
||||
cache.redis_client = ToterRedis()
|
||||
try:
|
||||
assert cache.get("egal") is None
|
||||
assert cache.set("egal", {"a": 1}) is False
|
||||
assert cache.delete("egal") is False
|
||||
assert cache.clear() is False
|
||||
cache.init_cache() # darf ebenfalls nicht werfen
|
||||
assert cache.zustand()["erreichbar"] is False
|
||||
finally:
|
||||
cache.redis_client = echt
|
||||
|
||||
|
||||
def test_cache_meldet_den_ausfall_nur_EINMAL(capsys):
|
||||
"""Eine Meldung je Anfrage waere Laerm — und bei jedem Titel-Suchlauf
|
||||
kaeme sie mehrfach."""
|
||||
from cache import cache
|
||||
|
||||
class ToterRedis:
|
||||
def get(self, *a, **k):
|
||||
raise cache.RedisError("weg")
|
||||
|
||||
echt, zustand = cache.redis_client, cache._erreichbar
|
||||
cache.redis_client = ToterRedis()
|
||||
cache._erreichbar = None
|
||||
try:
|
||||
for _ in range(5):
|
||||
cache.get("x")
|
||||
ausgabe = capsys.readouterr().out
|
||||
assert ausgabe.count("nicht erreichbar") == 1, ausgabe
|
||||
finally:
|
||||
cache.redis_client, cache._erreichbar = echt, zustand
|
||||
|
||||
|
||||
def test_ein_kaputter_eintrag_wirft_nicht():
|
||||
"""Ein unlesbarer Eintrag ist kein Grund zu scheitern — dann wird die
|
||||
Antwort eben frisch geholt."""
|
||||
from cache import cache
|
||||
|
||||
class KaputterRedis:
|
||||
def get(self, *a, **k):
|
||||
return "{kein json"
|
||||
|
||||
echt = cache.redis_client
|
||||
cache.redis_client = KaputterRedis()
|
||||
try:
|
||||
assert cache.get("x") is None
|
||||
finally:
|
||||
cache.redis_client = echt
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", ["tmdb", "omdb", "jikan", "musicbrainz"])
|
||||
def test_jeder_client_ist_importierbar(name):
|
||||
"""Ein Tippfehler in einem Modulpfad faellt sonst erst auf, wenn jemand
|
||||
eine Disc einlegt."""
|
||||
__import__("clients." + name)
|
||||
Reference in New Issue
Block a user