feat(uhd): KEYDB.cfg-Unterstuetzung - MakeMKVs Schluessel-Kanal liefert nichts mehr
Ampel / ampel (push) Successful in 28s
Ampel / ampel (push) Successful in 28s
4K-UHD scheiterte an "The volume key is unknown for this disc". Am 25.07.2026
im Worker-Container nachgemessen: Laufwerk und MakeMKV sind in Ordnung
(LibreDrive v06.3 / Meldung 1011, Disc wird gelesen, AACS-Dump geschrieben) -
MakeMKV versucht gar nicht erst, online einen Schluessel zu holen. Belege:
_private_data.tar enthaelt nur die Index-Datei und KEINE hkd_*.bin; auch mit
geloeschter update.conf (Meldung 5074 belegt den Web-Kontakt) und mit
app_UpdateEnable="1" kam keiner; hkdata.fairuse.org und hkdata.crabdance.com
loesen weltweit nicht mehr auf (NXDOMAIN gegen Fritz!Box, 8.8.8.8, 1.1.1.1).
Betroffen war Akira UHD (MKB v76, Pressung Dez. 2020) - also gerade KEINE
Neuerscheinung. Der bisherige Fehlertext ("Disc neuer als die
Schluessel-Datenbank, mit einem der naechsten Updates rippbar") war falsch.
Einziger heute funktionierender Weg ist eine vom Nutzer selbst mitgebrachte
KEYDB.cfg. Rippy liefert KEINE Schluessel mit, laedt keine herunter und
verteilt keine - es stellt nur den Platz bereit und zeigt an, was dort liegt.
- Datenverzeichnis persistent gemountet (MAKEMKV_DATA_HOST, Default
/srv/rippy/makemkv): KEYDB.cfg und AACS-Dumps ueberleben jeden Rebuild.
Vorher loeschte jeder "up -d --build" beides - inklusive des Dumps, auf den
die Fehlermeldung selbst verwies.
- entrypoint.sh und tasks.py schreiben settings.conf ergaenzend statt
zerstoerend. Der entrypoint bricht bei nicht beschreibbarem Verzeichnis
nicht mehr ab - mit "restart: unless-stopped" waere das ein Crashloop
gewesen, in dem auch reines DVD-Rippen tot ist.
- Neues Zwillings-Modul makemkv_daten.py (docker/api + docker/worker,
byteweise identisch; test_zwillinge_sind_byteweise_identisch wacht darueber
und wurde durch absichtliches Verstellen als wirksam nachgewiesen).
- API: GET/POST/DELETE /system/keydb, GET /system/aacs-dumps(/{dateiname}).
JSON-Body statt Multipart - python-multipart ist bewusst nicht installiert
und wuerde die API beim Import toeten. nginx client_max_body_size 64m,
sonst scheitert der Upload mit 413, bevor die API ihn sieht.
- UI (Einstellungen -> System): Status, Hochladen per Datei-Dialog, Entfernen,
Dump-Download, KEYDB-Plakette je Worker (nur wo das Verzeichnis wirklich
gemountet ist - ein Remote-Transcode-Worker truege sonst eine Warnung,
die ihn nichts angeht).
- parse_msg() + log_cb: MakeMKV-Meldungen landen im Rippy-Log (gedrosselt:
Code 1003 raus, keine Wiederholungen, max. 40 je Rip). Nebenbei behoben:
der alte Parser (split(",", 4)[3]) schnitt jede Meldung am ersten Komma ab.
- Fuenf Stellen richtiggestellt, die behaupteten, MakeMKV-Updates braechten
die neueste Disc-Schluessel-Datenbank mit (UI, Anleitung, README,
Worker-Dockerfile, makemkv_key.py).
NICHT bewiesen: ein erfolgreicher UHD-Rip - es lag keine KEYDB.cfg mit dem
Akira-Schluessel vor. Belegt sind der Befund und die neue Mechanik. So steht
es auch im SAVEPOINT und in der ROADMAP.
Quellen (AGENTS Regel D):
- Datenverzeichnis + Dateiname GROSS/case-sensitiv:
https://forum.makemkv.com/forum/viewtopic.php?t=30636
- hkd_*.bin in _private_data.tar:
https://forum.makemkv.com/forum/viewtopic.php?t=32675
- headless settings.conf / app_UpdateEnable:
https://forum.makemkv.com/forum/viewtopic.php?t=20364
- KEYDB.cfg-Zeilenformat (libaacs):
https://github.com/ShiftMediaProject/libaacs/blob/master/KEYDB.cfg
- MSG-/PRGV-Format: https://www.makemkv.com/developers/usage.txt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
"""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
|
||||
|
||||
|
||||
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("", "") == ""
|
||||
Reference in New Issue
Block a user