Files
rippy/docker/api/cache/cache.py
T
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

123 lines
4.0 KiB
Python

"""Zwischenspeicher für API-Antworten — mit Redis, wenn es eins gibt.
## Warum das nicht mehr scheitern darf (Befund 28.08.2026)
Hier stand ein nacktes `redis_client.get(key)`. Der Vorgabe-Host ist `redis`
— der Dienstname aus `docker-compose.yml`. Im Container stimmt das; auf einem
Windows-PC gibt es kein Redis, und jeder Aufruf endete mit:
redis.exceptions.ConnectionError: Error 11001 connecting to redis:6379
Das riss den Pre-Scan mit. Der Commander sah die Folge, nicht die Ursache:
Eine eingelegte Blu-ray blieb namenlos, und die Metadaten-Suche lief nie an.
**Ein Zwischenspeicher ist eine Beschleunigung, keine Voraussetzung.** Ist
keiner da, wird eben jedes Mal neu gefragt — TMDB und OMDb halten das aus.
Deshalb fängt jeder Zugriff hier den Verbindungsfehler ab und tut so, als
wäre der Eintrag nicht vorhanden. Das ist genau die Wahrheit: Er ist es
nicht.
## Warum trotzdem einmal gemeldet wird
Ein Zwischenspeicher, der still nie greift, ist eine unsichtbare
Verlangsamung — und auf der VM wäre ein weggefallenes Redis ein echter
Befund. Deshalb sagt `zustand()`, woran man ist, und die erste fehlgeschlagene
Verbindung schreibt eine Zeile. Danach Ruhe: Eine Meldung je Anfrage wäre
Lärm.
"""
import json
import os
from typing import Any, Optional
from redis import Redis
from redis.exceptions import RedisError
redis_host = os.getenv("REDIS_HOST", "redis")
redis_port = int(os.getenv("REDIS_PORT", "6379"))
# Kurze Zeitgrenzen: Ein Zwischenspeicher, auf den man wartet, ist keiner.
# Ohne sie hing jeder Aufruf am Standard-Timeout der Bibliothek.
redis_client = Redis(host=redis_host, port=redis_port, decode_responses=True,
socket_connect_timeout=1.5, socket_timeout=1.5)
# Erst beim ersten Zugriff bekannt. None = noch nichts versucht.
_erreichbar: Optional[bool] = None
def _melde_einmal(fehler: Exception) -> None:
"""Beim ERSTEN Fehlschlag eine Zeile, danach Ruhe."""
global _erreichbar
if _erreichbar is False:
return
_erreichbar = False
print("Zwischenspeicher (%s:%s) nicht erreichbar — es wird ohne "
"gearbeitet: %s" % (redis_host, redis_port, fehler))
def zustand() -> dict:
"""Woran man ist. Für Diagnose und Selbstauskunft."""
return {"host": redis_host, "port": redis_port, "erreichbar": _erreichbar}
def init_cache() -> None:
"""Einmal anklopfen, damit der Zustand bekannt ist. Wirft nie."""
global _erreichbar
try:
redis_client.ping()
_erreichbar = True
except (RedisError, OSError) as e:
_melde_einmal(e)
def get(key: str) -> Optional[Any]:
"""Wert aus dem Zwischenspeicher. None = nicht da ODER kein Speicher."""
global _erreichbar
try:
value = redis_client.get(key)
except (RedisError, OSError) as e:
_melde_einmal(e)
return None
_erreichbar = True
if value:
try:
return json.loads(value)
except (TypeError, ValueError):
# Ein kaputter Eintrag ist kein Grund, den Aufrufer scheitern zu
# lassen — er holt sich die Antwort dann eben frisch.
return None
return None
def set(key: str, value: Any, expire: Optional[int] = None) -> bool:
"""Wert ablegen. False heißt: ging nicht — und das ist in Ordnung."""
global _erreichbar
try:
serialized = json.dumps(value)
if expire:
return bool(redis_client.setex(key, expire, serialized))
return bool(redis_client.set(key, serialized))
except (RedisError, OSError) as e:
_melde_einmal(e)
return False
except (TypeError, ValueError):
# Nicht serialisierbar. Das ist ein Fehler des Aufrufers, aber keiner,
# der eine Metadaten-Abfrage umwerfen darf.
return False
def delete(key: str) -> bool:
try:
return bool(redis_client.delete(key))
except (RedisError, OSError) as e:
_melde_einmal(e)
return False
def clear() -> bool:
try:
return bool(redis_client.flushdb())
except (RedisError, OSError) as e:
_melde_einmal(e)
return False