Files
rippy/docker/worker/test_schluessel.py
Hitonabi c171f8879c
Ampel / ampel (push) Successful in 31s
feat(schluessel): der Windows-PC holt die 4K-Schluessel jetzt selbst
Auf Commander-Entscheid gebaut. Belegt am 25.07.2026 auf beiden Maschinen:
`makemkvcon` unter LINUX ruft Disc-Schluessel NIE ab, die WINDOWS-Version schon
(Meldung 3338). Deshalb scheiterte jede unbekannte UHD-Disc auf der VM mit "The
volume key is unknown", und der Weg, der funktioniert, war Handarbeit: Laufwerk
an den PC, Disc oeffnen, _private_data.tar suchen, im UI hochladen.

Das laeuft jetzt von selbst - und zwar ZWEISCHICHTIG, mit Absicht:

  1. Der WAECHTER (verlaesslich): sieht _private_data.tar nach und laedt sie zu
     Rippy hoch, sobald sie sich geaendert hat. Braucht keine
     Laufwerkserkennung, kein Disc-Oeffnen, nichts geraten. Deckt auch den Fall
     ab, dass man die Disc einfach in der MakeMKV-Oberflaeche oeffnet.
  2. Das ANSTOSSEN (nach bestem Wissen): liegt eine Disc im Laufwerk, wird
     `makemkvcon info` darauf losgelassen - dabei holt MakeMKV den Schluessel.

Warum getrennt: Das Format der BELEGTEN `DRV:`-Zeile liess sich auf dem
Commander-PC nicht messen, weil dort kein optisches Laufwerk steckt (alle 16
Plaetze melden `DRV:i,256,999,0,"","",""` - das ist gemessen). Geraten wird also
nur in Schicht 2, und wenn die Vermutung falsch ist, passiert dort einfach
nichts - Schicht 1 arbeitet weiter. Die teure Annahme steckt nie im
verlaesslichen Teil.

Gemessene Fundstellen: Datenverzeichnis ist `%USERPROFILE%\.MakeMKV` (NICHT
%APPDATA%\MakeMKV, wie man vermuten wuerde) - dort lag die echte Datei mit
6.420.480 Bytes. Programm: C:\Program Files (x86)\MakeMKV\makemkvcon64.exe,
v1.18.4. Hochgeladen wird mit ROHEM Koerper an POST /system/keystore, weil der
API python-multipart fehlt.

Die Automatik schaltet sich selbst ab, wenn MakeMKV nicht installiert ist: Auf
einem reinen Encoding-PC gibt es nichts zu holen, und eine Schleife, die jede
Minute ins Leere greift, waere nur Rauschen. Ihre Meldungen gehen ueber die
Log-Bruecke auch nach Rippy - das ist genau die Auskunft, auf die man nach dem
Einlegen einer neuen UHD-Disc wartet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 15:14:52 +02:00

195 lines
6.7 KiB
Python

"""Tests der Schlüssel-Automatik — ohne MakeMKV, ohne Laufwerk, ohne Netz.
Die Testdaten sind gemessen (26.07.2026, Commander-PC, MakeMKV 1.18.4):
`makemkvcon -r --cache=1 info disc:9999` auf einer Maschine OHNE optisches
Laufwerk gibt sechzehn leere `DRV:`-Zeilen. Die BELEGTE Form ließ sich dort nicht
messen — deshalb prüfen die Tests vor allem, dass die Automatik im Zweifel
NICHTS tut.
"""
import schluessel
# Wörtlich aus dem echten Lauf (gekürzt auf drei der sechzehn Zeilen).
DRV_LEER = """MSG:1005,0,1,"MakeMKV v1.18.4 win(x64-release) gestartet","%1 gestartet","MakeMKV v1.18.4 win(x64-release)"
MSG:5042,0,0,"Das Programm konnte keine verwendbaren optischen Laufwerke finden.","Das Programm konnte keine verwendbaren optischen Laufwerke finden."
DRV:0,256,999,0,"","",""
DRV:1,256,999,0,"","",""
DRV:2,256,999,0,"","",""
MSG:5010,0,0,"Das Öffnen der Disk schlug fehl ","Das Öffnen der Disk schlug fehl "
TCOUNT:0
"""
# So sieht eine belegte Zeile AUS SICHT DES FORMATS aus — nicht gemessen,
# deshalb steht sie nur hier im Test und nicht als Annahme im Code.
DRV_MIT_DISC = """DRV:0,2,999,12,"BD-RE HL-DT-ST BU40N","AKIRA","D:"
DRV:1,256,999,0,"","",""
"""
def test_leere_laufwerke_ergeben_nichts():
"""Der gemessene Fall: kein optisches Laufwerk → die Automatik tut nichts."""
assert schluessel.parse_laufwerke(DRV_LEER) == []
def test_kein_disc_name_gilt_als_nichts_da():
"""Konservativ mit Absicht: Ein falsch-negativ ist harmlos (der Wächter
greift trotzdem), ein falsch-positiv wäre ein makemkvcon-Lauf ins Leere."""
assert schluessel.parse_laufwerke('DRV:0,2,999,12,"BD-RE BU40N","",""') == []
def test_belegtes_laufwerk_wird_gefunden():
gefunden = schluessel.parse_laufwerke(DRV_MIT_DISC)
assert len(gefunden) == 1
assert gefunden[0]["index"] == 0
assert gefunden[0]["disc"] == "AKIRA"
assert gefunden[0]["geraet"] == "D:"
def test_muell_und_leer_stuerzen_nicht_ab():
assert schluessel.parse_laufwerke("") == []
assert schluessel.parse_laufwerke(None) == []
assert schluessel.parse_laufwerke("irgendwas ganz anderes") == []
# --- Wann hochgeladen wird --------------------------------------------------
def test_erste_runde_laedt_immer_hoch():
"""Rippy soll den Bestand kennen, auch wenn MakeMKV gerade nichts Neues
gelernt hat."""
assert schluessel.hat_sich_geaendert(None, (6420480, 1753000000)) is True
def test_unveraenderte_datei_wird_nicht_wieder_geschickt():
stand = (6420480, 1753000000)
assert schluessel.hat_sich_geaendert(stand, stand) is False
def test_geaenderte_datei_wird_geschickt():
assert schluessel.hat_sich_geaendert(
(6420480, 1753000000), (6500000, 1753009999)) is True
def test_fehlende_datei_loest_keinen_upload_aus():
"""(0, 0) heißt „gibt es nicht" — dann gibt es auch nichts zu schicken."""
assert schluessel.hat_sich_geaendert(None, (0, 0)) is False
assert schluessel.hat_sich_geaendert((6420480, 1753000000), (0, 0)) is False
# --- Der Upload -------------------------------------------------------------
def test_upload_schickt_den_rohen_koerper(tmp_path):
"""Kein JSON, kein Multipart: genau so nimmt POST /system/keystore die Datei
an (der API fehlt python-multipart)."""
datei = tmp_path / "_private_data.tar"
datei.write_bytes(b"TAR-INHALT")
gesehen = {}
class Antwort:
def __enter__(self):
return self
def __exit__(self, *a):
return False
def read(self):
return b""
def oeffner(anfrage, timeout=None):
gesehen["url"] = anfrage.full_url
gesehen["daten"] = anfrage.data
gesehen["typ"] = anfrage.headers.get("Content-type")
return Antwort()
assert schluessel.hochladen("1.2.3.4", str(datei), oeffner=oeffner) == ""
assert gesehen["url"] == "http://1.2.3.4/api/system/keystore"
assert gesehen["daten"] == b"TAR-INHALT"
assert gesehen["typ"] == "application/octet-stream"
def test_upload_ohne_adresse_oder_datei_meldet_klartext():
assert "Adresse" in schluessel.hochladen("", "/x.tar")
assert "Schlüsseldatei" in schluessel.hochladen("1.2.3.4", "")
def test_upload_fehler_wird_gemeldet_nicht_geworfen(tmp_path):
"""Der Worker muss weiterarbeiten, auch wenn Rippy gerade weg ist."""
datei = tmp_path / "_private_data.tar"
datei.write_bytes(b"x")
def kaputt(anfrage, timeout=None):
raise OSError("Netz weg")
fehler = schluessel.hochladen("1.2.3.4", str(datei), oeffner=kaputt)
assert "Hochladen fehlgeschlagen" in fehler
# --- Eine ganze Runde -------------------------------------------------------
def test_runde_ohne_makemkv_tut_nichts():
"""Auf einem PC ohne MakeMKV darf die Automatik nicht herumfummeln."""
stand, was = schluessel.runde("1.2.3.4", None, programm="")
assert (stand, was) == (None, "kein-makemkv")
def test_runde_stoesst_erst_an_und_prueft_dann(monkeypatch, tmp_path):
"""Reihenfolge mit Absicht: Ein gerade geholter Schluessel soll in DERSELBEN
Runde mitgenommen werden, nicht erst eine Minute spaeter."""
datei = tmp_path / "_private_data.tar"
datei.write_bytes(b"neu")
monkeypatch.setattr(schluessel, "schluesseldatei", lambda: str(datei))
ablauf = []
def laufen(cmd, **kwargs):
# disc:9999 = Laufwerke auflisten, disc:0 = Disc oeffnen
ablauf.append(cmd[-1])
class E:
stdout = DRV_MIT_DISC if cmd[-1] == "disc:9999" else ""
return E()
class Antwort:
def __enter__(self):
return self
def __exit__(self, *a):
return False
def read(self):
return b""
meldungen = []
stand, was = schluessel.runde(
"1.2.3.4", None, melden=lambda lvl, t: meldungen.append((lvl, t)),
programm="makemkvcon.exe", laufen=laufen,
oeffner=lambda a, timeout=None: Antwort())
assert ablauf == ["disc:9999", "disc:0"]
assert was == "hochgeladen"
assert stand == schluessel.datei_stand(str(datei))
assert any("Disc erkannt" in t for _, t in meldungen)
assert any("wirkt ab dem nächsten Rip" in t for _, t in meldungen)
def test_runde_ohne_aenderung_schickt_nichts(monkeypatch, tmp_path):
datei = tmp_path / "_private_data.tar"
datei.write_bytes(b"alt")
monkeypatch.setattr(schluessel, "schluesseldatei", lambda: str(datei))
vorher = schluessel.datei_stand(str(datei))
def laufen(cmd, **kwargs):
class E:
stdout = DRV_LEER
return E()
def darf_nicht(anfrage, timeout=None):
raise AssertionError("kein Upload erwartet")
stand, was = schluessel.runde(
"1.2.3.4", vorher, programm="makemkvcon.exe", laufen=laufen,
oeffner=darf_nicht)
assert (stand, was) == (vorher, "unveraendert")