f449c4ee34
Ampel / ampel (push) Successful in 28s
Richtigstellung des Vortags-Befunds. Dort stand, MakeMKVs Schluessel-Kanal
sei abgeschaltet. Das war FALSCH: die Herleitung stuetzte sich auf zwei
Hostnamen aus alten Forumsbeitraegen (hkdata.fairuse.org,
hkdata.crabdance.com), die zwar wirklich nicht mehr aufloesen, von MakeMKV
aber laengst nicht mehr benutzt werden. Aufgedeckt durch den Einwand des
Commanders, unter Windows ginge es sofort.
Gegenprobe mit demselben Laufwerk und derselben Disc (Akira UHD, MKB v76):
Linux (Worker) Windows
Verbindungen KEINE EINZIGE 185.84.108.20:443
Meldung 3338 nie "Downloading latest HK"
_private_data.tar 2048 B, 0 Keys 6,4 MB, 604 Keys
Disc volume key unknown TCOUNT:5, geht auf
Gegengeprueft mit leerem UND gefuelltem Speicher, mit und ohne --noscan,
mit dev:/dev/sr0 und disc:0, mit geloeschter update.conf. Linux fragt nie.
Die Meldungsvorlage "Downloading latest %1 to %2 ..." steckt sehr wohl im
Linux-Binary - sie loest nur nicht aus. Gleiches Symptom im MakeMKV-Forum,
seit Jahren offen (t=25782, t=34022). Der Dienst lebt; der Worker erreicht
185.84.108.20:443 sogar problemlos.
BEWIESEN: Nach Uebernahme des Windows-Schluesselspeichers oeffnet
makemkvcon auf der VM die Akira-UHD - "Operation successfully completed",
TCOUNT:5, fuenf Titel, identisch zum Windows-Ergebnis. Erster belegter
UHD-Disc-Zugriff auf der Rippy-Maschine.
- makemkv_daten.py (beide Zwillinge): zaehle_schluessel,
private_data_pruefen, schluesselspeicher_status, private_data_schreiben.
Die Pruefung lehnt einen Speicher OHNE hkd_*.bin ab - sonst laedt jemand
den leeren Vorrat einer frischen Installation hoch, nichts aendert sich,
und niemand versteht warum. Modulkopf komplett neu, inkl. der
Fehldiagnose als Warnung fuer spaeter.
- API: GET/POST /system/keystore. Der Rohkoerper der Anfrage IST die Datei
(binaer - JSON/Base64 waere Ballast, Multipart kann die API nicht).
Groessengrenze 64 MB = client_max_body_size in nginx.conf.
- UI: neuer Block "Disc-Schluessel fuer 4K-UHD" UEBER dem KEYDB-Block, mit
Schluessel-Anzahl, Upload und Anleitung fuer den Windows-Weg. KEYDB.cfg
ist jetzt als Notnagel beschriftet. Worker-Plakette zeigt die Anzahl;
0 heisst sichtbar "4K-UHD scheitert".
- tasks.py: UHD-Fehlertext sagt den Windows-Weg an und nennt die Anzahl
bekannter Schluessel dieses Workers.
- caps.py meldet schluessel je Worker.
- Alle Falschaussagen korrigiert: UI (3), Anleitung (2), README (3),
KONZEPT §8 + §10, Worker-Dockerfile, makemkv_key.py (dort stand "Den
AACS-Schluessel zieht MakeMKV via LibreDrive ohnehin selbst aus dem
Laufwerk" - gilt fuer Blu-ray, NICHT fuer UHD).
- SAVEPOINT v3.11, ROADMAP Etappe 18 (Etappe 17 mit Nachtrag), AGENTS.
Offen: voller UHD-Rip inkl. Transcode-E2E; und ob sich der Abruf unter
Linux doch anstossen laesst.
Quellen (AGENTS Regel D):
- Linux laedt keine Hashed Keys, gleiches Symptom:
https://forum.makemkv.com/forum/viewtopic.php?t=25782
https://forum.makemkv.com/forum/viewtopic.php?t=34022
- Schluessel als hkd_*.bin in _private_data.tar:
https://forum.makemkv.com/forum/viewtopic.php?t=32675
- Meldungsformat: https://www.makemkv.com/developers/usage.txt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
245 lines
10 KiB
Python
245 lines
10 KiB
Python
"""Tests fuer makemkv_daten.py: die reinen Helfer rund um das MakeMKV-Datenverzeichnis.
|
|
|
|
WARUM ES DIESE TESTS GIBT (Befund 25.07.2026, live im Worker nachgemessen):
|
|
Eine 4K-UHD-Disc (Akira UHD, MKB v76) scheiterte mit "The volume key is unknown
|
|
for this disc", obwohl Laufwerk und MakeMKV in Ordnung waren. Der einzige heute
|
|
noch funktionierende Weg ist eine selbst mitgebrachte KEYDB.cfg im
|
|
Datenverzeichnis. Damit haengt einiges an diesen kleinen Funktionen: erkennen wir
|
|
die Datei falsch, meldet das UI "alles gut", waehrend MakeMKV weiter scheitert.
|
|
|
|
Getestet wird nur, was ohne Postgres, Redis und ohne Laufwerk laeuft — also die
|
|
puren Funktionen mit echten Beispieldaten. Zeilenformat der KEYDB.cfg laut
|
|
libaacs (AGENTS Regel D, externe Schnittstellen nie aus dem Kopf):
|
|
https://github.com/ShiftMediaProject/libaacs/blob/master/KEYDB.cfg
|
|
|
|
WARUM DER DATEINAME "_worker" HINTEN DRANHAENGT (25.07.2026): makemkv_daten.py
|
|
ist eine Zwillingsdatei, es gibt sie unter docker/api/ UND docker/worker/, und
|
|
beide Seiten haben Tests. Da im Projekt keine __init__.py liegen, importiert
|
|
pytest Testdateien unter ihrem blossen Dateinamen — zwei Dateien namens
|
|
test_makemkv_daten.py brechen deshalb die Sammelphase ab ("import file
|
|
mismatch") und faerben die ganze Ampel rot. Nicht zurueckbenennen.
|
|
"""
|
|
|
|
import hashlib
|
|
import importlib.util
|
|
import os
|
|
|
|
# WICHTIG (Prüfbefund 25.07.2026): Ein schlichtes "from makemkv_daten import ..."
|
|
# lädt bei "pytest -q" vom Repo-Wurzelverzeichnis NICHT diese Datei, sondern die
|
|
# API-Kopie — docker/api wird zuerst gesammelt, und jeder weitere Import trifft
|
|
# nur noch den sys.modules-Cache. Die Tests hier hätten den Worker-Zwilling also
|
|
# nie angefasst und eine Abweichung wäre grün durchgelaufen. Deshalb wird er
|
|
# ausdrücklich über seinen Pfad geladen.
|
|
_HIER = os.path.dirname(os.path.abspath(__file__))
|
|
_WORKER_MODUL = os.path.join(_HIER, "makemkv_daten.py")
|
|
_API_MODUL = os.path.abspath(os.path.join(_HIER, "..", "api", "makemkv_daten.py"))
|
|
|
|
_spec = importlib.util.spec_from_file_location("makemkv_daten_worker_kopie", _WORKER_MODUL)
|
|
_modul = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(_modul)
|
|
|
|
ist_aacs_dump = _modul.ist_aacs_dump
|
|
keydb_pruefen = _modul.keydb_pruefen
|
|
settings_conf_zusammenfuehren = _modul.settings_conf_zusammenfuehren
|
|
zaehle_disc_eintraege = _modul.zaehle_disc_eintraege
|
|
private_data_pruefen = _modul.private_data_pruefen
|
|
zaehle_schluessel = _modul.zaehle_schluessel
|
|
|
|
|
|
def _tar_mit(namen):
|
|
"""Baut ein tar-Archiv im Speicher — so sieht MakeMKVs Schluesselspeicher aus.
|
|
|
|
Echte Eintragsnamen aus dem Speicher der Windows-Installation vom
|
|
25.07.2026: hkd_<8 Hex>.bin fuer die Schluessel, dazu Index-Dateien.
|
|
"""
|
|
import io
|
|
import tarfile
|
|
|
|
puffer = io.BytesIO()
|
|
with tarfile.open(fileobj=puffer, mode="w") as archiv:
|
|
for name in namen:
|
|
eintrag = tarfile.TarInfo(name)
|
|
eintrag.size = 3
|
|
archiv.addfile(eintrag, io.BytesIO(b"abc"))
|
|
return puffer.getvalue()
|
|
|
|
|
|
def test_zaehle_schluessel_zaehlt_nur_hkd_eintraege():
|
|
# Nur hkd_*.bin sind Disc-Schluessel. Index- und sdf-Dateien gehoeren zum
|
|
# Speicher dazu, sind aber keine Schluessel — sonst meldete das UI
|
|
# "1 Schluessel vorhanden" fuer einen komplett leeren Vorrat.
|
|
voll = _tar_mit([
|
|
"hkd_00000059.bin",
|
|
"hkd_0000005a.bin",
|
|
"sdf_000000a6.bin",
|
|
"--index-A2E950B3C3FC57DA9CB856DCAFBA5275F40423DB.bin",
|
|
])
|
|
assert zaehle_schluessel(voll) == 2
|
|
|
|
|
|
def test_zaehle_schluessel_leerer_speicher_ist_null():
|
|
# Genau dieser Zustand lag am 25.07.2026 auf der VM vor: ein Archiv mit
|
|
# ausschliesslich der Index-Datei. Jede unbekannte UHD-Disc scheitert dann.
|
|
leer = _tar_mit(["--index-4EF73C3560D489497ACE763962A7F07A4A1545C4.bin"])
|
|
assert zaehle_schluessel(leer) == 0
|
|
|
|
|
|
def test_zaehle_schluessel_bei_muell_kein_absturz():
|
|
# Darf niemals werfen — die Zahl landet im Worker-Herzschlag.
|
|
assert zaehle_schluessel(b"") == 0
|
|
assert zaehle_schluessel(b"das ist kein tar") == 0
|
|
|
|
|
|
def test_private_data_pruefen_nimmt_echten_speicher_an():
|
|
assert private_data_pruefen(_tar_mit(["hkd_00000059.bin"])) == ""
|
|
|
|
|
|
def test_private_data_pruefen_lehnt_leere_und_falsche_dateien_ab():
|
|
assert "leer" in private_data_pruefen(b"")
|
|
assert "tar-Archiv" in private_data_pruefen(b"<html>Fehlerseite</html>")
|
|
|
|
|
|
def test_private_data_pruefen_lehnt_speicher_ohne_schluessel_ab():
|
|
# Der teuerste Bedienfehler: den Speicher einer Installation hochladen,
|
|
# die selbst noch nie Schluessel geholt hat. Ohne diese Pruefung aendert
|
|
# sich nichts und niemand versteht, warum.
|
|
leer = _tar_mit(["--index-4EF73C3560D489497ACE763962A7F07A4A1545C4.bin"])
|
|
assert "kein einziger Schluessel" in private_data_pruefen(leer)
|
|
|
|
|
|
def test_zwillinge_sind_byteweise_identisch():
|
|
"""docker/api/makemkv_daten.py MUSS dieselbe Datei sein wie diese hier.
|
|
|
|
Das Modul existiert bewusst doppelt — es gibt in diesem Projekt kein
|
|
gemeinsames Paket für API und Worker (gleiche Lage wie bei db.py). Genau
|
|
deshalb braucht es einen Wächter: laufen die beiden auseinander, zeigt das
|
|
UI etwas anderes an, als der rippende Worker tatsächlich sieht, und es
|
|
fällt niemandem auf. Dieser Test ist die einzige Stelle, die das
|
|
mechanisch prüft.
|
|
"""
|
|
with open(_WORKER_MODUL, "rb") as datei:
|
|
worker = hashlib.sha256(datei.read()).hexdigest()
|
|
with open(_API_MODUL, "rb") as datei:
|
|
api = hashlib.sha256(datei.read()).hexdigest()
|
|
assert worker == api, (
|
|
"docker/worker/makemkv_daten.py und docker/api/makemkv_daten.py sind "
|
|
"auseinandergelaufen - Aenderungen immer in BEIDE Dateien uebernehmen."
|
|
)
|
|
|
|
# Eine kleine, aber echte KEYDB.cfg im libaacs-Format: Kommentarkopf, eine
|
|
# Disc-Zeile MIT 0x-Praefix, eine OHNE, dazu ein Fortsetzungsfeld und eine
|
|
# Leerzeile. Erwartete Zahl der Eintraege: 2.
|
|
BEISPIEL_KEYDB = """; KEYDB.cfg
|
|
; Kommentarzeilen beginnen mit einem Semikolon
|
|
|
|
0x8F4E2C1A9B7D3E5F0A6C8B2D4E1F3A5C7B9D0E2F = AKIRA
|
|
| V | 0123456789ABCDEF0123456789ABCDEF
|
|
|
|
A1B2C3D4E5F60718293A4B5C6D7E8F90A1B2C3D4 = BLADE RUNNER 2049
|
|
"""
|
|
|
|
|
|
def test_zaehle_disc_eintraege_zaehlt_nur_echte_disc_zeilen():
|
|
"""Nur Zeilen mit 40 Hex-Zeichen und Gleichheitszeichen sind Eintraege.
|
|
|
|
Kommentare, Leerzeilen und Fortsetzungsfelder duerfen nicht mitzaehlen —
|
|
sonst meldet das UI bei einer reinen Kommentardatei stolz "42 Eintraege".
|
|
"""
|
|
assert zaehle_disc_eintraege(BEISPIEL_KEYDB) == 2
|
|
|
|
|
|
def test_zaehle_disc_eintraege_ignoriert_kommentare_und_leerzeilen():
|
|
# Eine Datei ganz ohne Disc-Zeile hat null Eintraege, nicht drei.
|
|
nur_beiwerk = "; nur ein Kommentar\n\n| V | 0123456789ABCDEF0123456789ABCDEF\n"
|
|
assert zaehle_disc_eintraege(nur_beiwerk) == 0
|
|
|
|
|
|
def test_zaehle_disc_eintraege_ignoriert_zu_kurze_kennung():
|
|
"""39 Hex-Zeichen sind keine Disc-Kennung.
|
|
|
|
Genau so sieht eine beim Kopieren verstuemmelte Datei aus — die darf nicht
|
|
als gueltig durchgehen, sonst sucht der Commander den Fehler beim Laufwerk.
|
|
"""
|
|
zu_kurz = "A1B2C3D4E5F60718293A4B5C6D7E8F90A1B2C3D = KAPUTT\n"
|
|
assert zaehle_disc_eintraege(zu_kurz) == 0
|
|
|
|
|
|
def test_keydb_pruefen_meldet_leere_datei():
|
|
# Haeufigster Fehlgriff: das Textfeld war leer, es wird trotzdem gespeichert.
|
|
assert keydb_pruefen("") != ""
|
|
assert keydb_pruefen(" \n\n ") != ""
|
|
|
|
|
|
def test_keydb_pruefen_erkennt_html_fehlerseite():
|
|
"""Der zweithaeufigste Fehlgriff: der Download lieferte eine HTML-Seite.
|
|
|
|
MakeMKV wuerde die Datei still ignorieren und weiter "volume key is unknown"
|
|
melden — deshalb muss der Fehler schon beim Hochladen sichtbar werden.
|
|
"""
|
|
html = "<!DOCTYPE html>\n<html><body><h1>404 Not Found</h1></body></html>\n"
|
|
meldung = keydb_pruefen(html)
|
|
assert meldung != ""
|
|
assert "HTML" in meldung
|
|
|
|
|
|
def test_keydb_pruefen_meldet_text_ohne_disc_zeile():
|
|
# Irgendein Text (hier: eine README) ist keine KEYDB.cfg.
|
|
meldung = keydb_pruefen("Diese Datei enthaelt keine Schluessel, nur Prosa.\n")
|
|
assert meldung != ""
|
|
|
|
|
|
def test_keydb_pruefen_akzeptiert_gueltigen_inhalt():
|
|
# Leerer Rueckgabewert heisst laut Vertrag: alles in Ordnung.
|
|
assert keydb_pruefen(BEISPIEL_KEYDB) == ""
|
|
|
|
|
|
def test_ist_aacs_dump_akzeptiert_echten_namen():
|
|
"""Name aus der Praxis: so legt MakeMKV den Dump laut Meldung 3332 ab
|
|
(am 25.07.2026 im Worker so beobachtet)."""
|
|
assert ist_aacs_dump("MKB20_v76_UHD_AKIRA_C02B.tgz") is True
|
|
|
|
|
|
def test_ist_aacs_dump_lehnt_pfad_tricks_und_fremde_dateien_ab():
|
|
"""Der Download-Endpunkt haengt den Namen an das Datenverzeichnis an —
|
|
ohne diese Pruefung koennte man sich damit aus dem Verzeichnis heraus
|
|
lesen. Versteckte Dateien und Nicht-Dumps sind ebenfalls nichts fuer die
|
|
Liste."""
|
|
assert ist_aacs_dump("../ausbruch.tgz") is False
|
|
assert ist_aacs_dump(".versteckt.tgz") is False
|
|
assert ist_aacs_dump("irgendwas.txt") is False
|
|
assert ist_aacs_dump("..\\windows\\ausbruch.tgz") is False
|
|
|
|
|
|
def test_settings_conf_ersetzt_key_und_behaelt_den_rest():
|
|
"""DIE Regression, um die es geht: bis zum 25.07.2026 haben entrypoint.sh
|
|
und tasks.py die settings.conf komplett ueberschrieben. Mit dem jetzt
|
|
persistenten Datenverzeichnis waere damit bei jedem Containerstart und vor
|
|
jedem Rip alles andere weg — allen voran app_UpdateEnable."""
|
|
alt = 'app_Key = "T-alterSchluessel"\napp_UpdateEnable = "1"\napp_DefaultSelectionString = "+sel:all"\n'
|
|
neu = settings_conf_zusammenfuehren(alt, "T-neuerSchluessel")
|
|
assert 'app_Key = "T-neuerSchluessel"' in neu
|
|
assert 'app_Key = "T-alterSchluessel"' not in neu
|
|
assert 'app_UpdateEnable = "1"' in neu
|
|
assert 'app_DefaultSelectionString = "+sel:all"' in neu
|
|
# Genau EINE app_Key-Zeile, sonst gewinnt am Ende die falsche.
|
|
assert neu.count("app_Key") == 1
|
|
|
|
|
|
def test_settings_conf_leerer_key_entfernt_die_zeile():
|
|
# Ein bewusst geleerter Key darf nicht heimlich weiterwirken.
|
|
alt = 'app_Key = "T-alterSchluessel"\napp_UpdateEnable = "1"\n'
|
|
neu = settings_conf_zusammenfuehren(alt, "")
|
|
assert "app_Key" not in neu
|
|
assert 'app_UpdateEnable = "1"' in neu
|
|
|
|
|
|
def test_settings_conf_aus_dem_nichts_ergibt_saubere_datei():
|
|
"""Erststart: die Datei gibt es noch gar nicht. Der abschliessende
|
|
Zeilenumbruch ist Absicht — MakeMKV liest die Datei zeilenweise."""
|
|
assert settings_conf_zusammenfuehren("", "T-neuerSchluessel") == 'app_Key = "T-neuerSchluessel"\n'
|
|
|
|
|
|
def test_settings_conf_ohne_key_und_ohne_inhalt_bleibt_leer():
|
|
# Kein Inhalt, kein Key: keine Datei mit einer einsamen Leerzeile erzeugen.
|
|
assert settings_conf_zusammenfuehren("", "") == ""
|