"""Tests für die puren Pre-Scan-/OMDb-Helfer (Label-Normalisierung, Parsing)."""
from clients.omdb import parse_runtime_minutes, parse_year
from prescan.prescan import normalize_disc_label, parse_udf_dstring
def test_udf_dstring_latin1():
# d-string: [comp_id=8]["MEIN_FILM"][padding][len=10]
daten = bytes([8]) + b"MEIN_FILM" + bytes(21) + bytes([10])
assert parse_udf_dstring(daten) == "MEIN_FILM"
def test_udf_dstring_utf16():
text = "FILM".encode("utf-16-be")
daten = bytes([16]) + text + bytes(32 - 2 - len(text)) + bytes([1 + len(text)])
assert parse_udf_dstring(daten) == "FILM"
def test_udf_dstring_kaputt_gibt_leer():
assert parse_udf_dstring(b"") == ""
assert parse_udf_dstring(bytes(32)) == "" # Länge 0
daten = bytes([8]) + b"X" + bytes(29) + bytes([99]) # Länge > Puffer
assert parse_udf_dstring(daten) == ""
def test_label_normalisierung():
# Disc-Labels sind fast immer GROSS_MIT_UNTERSTRICHEN
assert normalize_disc_label("PULP_FICTION") == "Pulp Fiction"
assert normalize_disc_label("DIE_HARD_4.0_DE") == "Die Hard 4 0 De"
assert normalize_disc_label(" doppel raum ") == "doppel raum"
assert normalize_disc_label("") == ""
def test_label_mit_gemischter_schreibung_bleibt():
# Wer schon sauber benannt hat, wird nicht plattgewalzt
assert normalize_disc_label("Inception") == "Inception"
def test_omdb_runtime_parsing():
assert parse_runtime_minutes("142 min") == 142
assert parse_runtime_minutes("N/A") == 0
assert parse_runtime_minutes("") == 0
def test_omdb_jahr_parsing():
assert parse_year("1999") == 1999
assert parse_year("2005–2012") == 2005 # Serien-Zeitspanne
assert parse_year("N/A") is None
assert parse_year("") is None
# --- Reihenfolge der Metadaten-Quellen: exakt schlägt unscharf ---------------
#
# Änderung vom 26.07.2026. Vorher galt der erste Jikan-Treffer über dem Gate
# 0,55 sofort als sicherer Fund, und OMDb kam nie dran — ein Kinofilm, den TMDB
# verpasst, bekam damit Anime-Metadaten. Die Messwerte dazu stehen in
# test_jikan_helpers.py („Alien" gegen „Alien 9" = 0,83).
class _StummerClient:
"""Ein Client, der auf jede Frage dasselbe antwortet — und mitzählt."""
def __init__(self, antwort=None):
self.antwort = antwort
self.aufrufe = 0
def lookup(self, *a, **k):
self.aufrufe += 1
return self.antwort
def search_movie(self, *a, **k):
return []
def search_tv(self, *a, **k):
return []
def get_movie_details(self, *a, **k):
return None
def find_by_imdb(self, *a, **k):
return None
def _scanner(monkeypatch, jikan_antwort, omdb_antwort):
"""PreScan ohne __init__ (das bräuchte DB + API-Keys), Cache stillgelegt."""
from prescan import prescan as modul
monkeypatch.setattr(modul, "get", lambda k: None)
monkeypatch.setattr(modul, "cache_set", lambda *a, **k: None)
scan = modul.PreScan.__new__(modul.PreScan)
scan.tmdb = _StummerClient()
scan.jikan = _StummerClient(jikan_antwort)
scan.omdb = _StummerClient(omdb_antwort)
return scan
def _jikan(score):
return {"type": "movie", "id": "mal-1", "title": "Alien 9",
"source": "jikan", "match_score": score}
def _omdb():
return {"type": "movie", "id": "tt0078748", "title": "Alien",
"source": "omdb", "year": 1979}
def test_unscharfer_jikan_treffer_laesst_omdb_zum_zug(monkeypatch):
"""Der eigentliche Fehler: „Alien" traf mit 0,83 auf den Anime „Alien 9",
und der Film wurde nie gesucht."""
scan = _scanner(monkeypatch, _jikan(0.83), _omdb())
ergebnis = scan._scan_video("/dev/sr0", {"title": "Alien", "disc_type": "Blu-ray"})
assert ergebnis.metadata["source"] == "omdb"
assert ergebnis.confidence == 0.8
assert scan.omdb.aufrufe > 0
def test_exakter_jikan_treffer_gewinnt_sofort(monkeypatch):
"""Der Fall, für den Jikan gebaut wurde, darf NICHT verlorengehen
(AGENTS Regel B): Bei einem sicheren Treffer wird OMDb nicht mehr gefragt."""
scan = _scanner(monkeypatch, _jikan(1.0), _omdb())
ergebnis = scan._scan_video("/dev/sr0", {"title": "Alien 9", "disc_type": "Blu-ray"})
assert ergebnis.metadata["source"] == "jikan"
assert ergebnis.confidence == 0.85
assert scan.omdb.aufrufe == 0
def test_unscharfer_jikan_treffer_bleibt_als_vorschlag(monkeypatch):
"""Hat OMDb nichts (kein Key, kein Treffer), ist der unscharfe Jikan-Treffer
besser als nichts — aber er wird als VORSCHLAG ausgewiesen (0,6), nicht als
sicherer Fund."""
scan = _scanner(monkeypatch, _jikan(0.83), None)
ergebnis = scan._scan_video("/dev/sr0", {"title": "Alien", "disc_type": "Blu-ray"})
assert ergebnis.metadata["source"] == "jikan"
assert ergebnis.confidence == 0.6
def test_ohne_jede_quelle_bleibt_es_unbekannt(monkeypatch):
"""Antwortet keine Quelle, bleibt es beim Disc-Titel mit Confidence 0,3 und
`type: unknown` — der Bestand, unverändert. Der Rip laeuft trotzdem, die
Datei heisst dann nur wie das Disc-Label."""
scan = _scanner(monkeypatch, None, None)
ergebnis = scan._scan_video("/dev/sr0", {"title": "Nix Da", "disc_type": "DVD"})
assert ergebnis.confidence == 0.3
assert ergebnis.metadata["type"] == "unknown"
assert ergebnis.title == "Nix Da"
# ── Was auf FREMDEN Rechnern gilt (Befund 28.08.2026) ───────────────────
#
# Der Commander: "du hast jetzt nur mein laufwerk analysiert oder? wenn ich
# rippy weitergebe wie laeuft es dort?"
#
# Berechtigt. Gemessen wurde an EINEM Laufwerk (LG BU40N) und EINER Disc
# (BD-50 mit BDMV/META). Diese Tests halten fest, was bei ANDEREN Discs
# passieren muss -- ohne dass eine davon vorhanden sein muss.
def test_bluray_ohne_metadaten_gibt_ehrlich_nichts_zurueck(tmp_path):
"""Sehr viele Blu-rays haben kein BDMV/META. Dann greift der Rueckfall
auf das Volume-Label -- aber diese Funktion darf nichts erfinden."""
from prescan.prescan import titel_aus_bdmt
(tmp_path / "BDMV" / "STREAM").mkdir(parents=True)
assert titel_aus_bdmt(str(tmp_path)) is None
def test_dvd_hat_kein_bdmv(tmp_path):
from prescan.prescan import titel_aus_bdmt
(tmp_path / "VIDEO_TS").mkdir()
assert titel_aus_bdmt(str(tmp_path)) is None
def test_bdmt_wird_gelesen_wenn_da(tmp_path):
"""Der Weg, der bei der Disc des Commanders 'Evangelion 2.22' lieferte —
hier ohne Disc nachgestellt."""
from prescan.prescan import titel_aus_bdmt
meta = tmp_path / "BDMV" / "META" / "DL"
meta.mkdir(parents=True)
(meta / "bdmt_deu.xml").write_text(
''
'Evangelion 2.22'
'', encoding="utf-8")
assert titel_aus_bdmt(str(tmp_path)) == "Evangelion 2.22"
def test_englisch_wird_bevorzugt(tmp_path):
"""Die APIs suchen mit englischen Titeln besser."""
from prescan.prescan import titel_aus_bdmt
meta = tmp_path / "BDMV" / "META" / "DL"
meta.mkdir(parents=True)
for datei, name in (("bdmt_deu.xml", "Deutscher Titel"),
("bdmt_eng.xml", "English Title")):
(meta / datei).write_text(
"%s" % name, encoding="utf-8")
assert titel_aus_bdmt(str(tmp_path)) == "English Title"
def test_prescan_ruft_makemkvcon_nicht_mehr_auf():
"""Der makemkvcon-Zweig (Titel-Quelle 3) wurde mit d3e86d2 bewusst
ENTFERNT — er lief unter Windows nie (`shutil.which` findet dort nichts,
Programme liegen in „Programme", nicht im PATH) und war unter Docker
doppelt zum eigentlichen Rip-Weg. Dieser Waechter passt auf, dass weder
der Aufruf noch die shutil.which-Suche zurueckkommen.
(Bis 30.08.2026 verlangte er stattdessen `werkzeug_katalog.finden` —
das war die Welt VOR d3e86d2. Er laeuft nur in der CI (fcntl) und hat
die Entfernung deshalb erst dort gemeldet.)"""
import os
hier = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(hier, "prescan", "prescan.py"), encoding="utf-8") as f:
# NUR Code, keine Kommentare: Der erste Anlauf dieses Tests fiel ueber
# den eigenen Erklaertext, in dem der alte Aufruf zitiert wird. Ein
# Waechter, der an einer Erklaerung scheitert, prueft das Falsche.
code = [z for z in f if not z.strip().startswith("#")]
quelle = "".join(code)
assert "shutil.which(" not in quelle, (
"shutil.which findet unter Windows keine Programme aus 'Programme'")
assert "makemkvcon" not in quelle, (
"Der makemkvcon-Zweig im Prescan wurde mit d3e86d2 entfernt und "
"soll nicht zurueckkommen — Titel kommen aus Label/BDMV/IFO.")