feat(uhd): KEYDB.cfg-Unterstuetzung - MakeMKVs Schluessel-Kanal liefert nichts mehr
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:
Hitonabi
2026-07-25 01:26:08 +02:00
parent f74f4e54f6
commit 0935766f61
23 changed files with 1717 additions and 69 deletions
+56 -16
View File
@@ -114,7 +114,7 @@ def lies_titel_info(device_path: str, timeout: int = 300) -> list:
def rip_titel_auswahl(device_path: str, output_dir: str, titel_liste: list,
progress_cb=None) -> dict:
progress_cb=None, log_cb=None) -> dict:
"""Rippt GENAU die gewählten Titel (makemkvcon kann pro Aufruf nur einen
Titel oder 'all' — also ein Aufruf je Titel, Fortschritt anteilig)."""
gesamt = len(titel_liste)
@@ -124,7 +124,8 @@ def rip_titel_auswahl(device_path: str, output_dir: str, titel_liste: list,
if progress_cb:
progress_cb(int((_i * 100 + p) / gesamt))
ergebnis = run_makemkv(device_path, output_dir, progress_cb=anteilig, titel=str(nr))
ergebnis = run_makemkv(device_path, output_dir, progress_cb=anteilig, titel=str(nr),
log_cb=log_cb)
if ergebnis.get("status") == "cancelled":
return ergebnis
if ergebnis.get("status") != "success":
@@ -181,6 +182,27 @@ def lies_datei_dauer(pfad: str, timeout: int = 120) -> int:
return parse_scan_dauer((ergebnis.stdout or "") + (ergebnis.stderr or ""))
_MSG_RE = re.compile(r'^MSG:(\d+),\d+,\d+,"((?:[^"\\]|\\.)*)"')
def parse_msg(zeile: str):
"""MSG-Zeile -> (code, klartext) oder None (pure Funktion, testbar).
Format laut https://www.makemkv.com/developers/usage.txt:
MSG:code,flags,count,"message","format","param0",... — Feld 4 ist der
fertig zusammengesetzte Klartext.
Bis zum 25.07.2026 stand hier line.split(",", 4)[3]: das schnitt JEDE
Meldung ab, die selbst ein Komma enthaelt — und MakeMKV schreibt solche
laufend ("Title #1 has length of 12 seconds, which is less than ...").
Deshalb eine Regex, die die Anfuehrungszeichen respektiert.
"""
treffer = _MSG_RE.match(zeile.strip())
if not treffer:
return None
return int(treffer.group(1)), treffer.group(2).replace('\\"', '"')
def get_progress_from_prgv(line: str) -> int:
"""Extrahiert Gesamt-Fortschritt (0-100) aus einer PRGV-Zeile.
@@ -297,8 +319,17 @@ def write_abcde_config(output_dir: str) -> str:
return tmp.name
def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str = "all") -> dict:
"""Rippt eine DVD/Blu-ray verlustfrei mit makemkvcon; meldet Fortschritt."""
def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str = "all",
log_cb=None) -> dict:
"""Rippt eine DVD/Blu-ray verlustfrei mit makemkvcon; meldet Fortschritt.
log_cb(code, text) bekommt JEDE MakeMKV-Meldung. Bewusst ein eigener
Kanal statt progress_cb: der Fortschritts-Callback in tasks.py verwirft
Aufrufe, bei denen sich die Prozentzahl nicht geaendert hat — Meldungen
waeren dort also grossteils verschwunden. Ohne diesen Kanal war am
25.07.2026 nicht von aussen erkennbar, dass MakeMKV bei der UHD-Disc
nicht einmal versucht, einen Schluessel zu holen (siehe makemkv_daten).
"""
if not check_makemkv_installed():
return {"status": "error", "error": "makemkvcon ist nicht installiert"}
@@ -328,15 +359,23 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str
try:
for line in process.stdout:
progress = get_progress_from_prgv(line)
if progress >= 0 and progress_cb:
progress_cb(progress)
elif line.startswith("MSG:"):
# MSG:code,flags,count,"message",... — Klartext ist Feld 4
teile = line.split(",", 4)
if len(teile) >= 4:
letzte_meldung = teile[3].strip('"')
if any(muster in letzte_meldung for muster in KRITISCH):
kritische_meldungen.append(letzte_meldung)
if progress >= 0:
if progress_cb:
progress_cb(progress)
continue
meldung = parse_msg(line)
if meldung is None:
continue
code, letzte_meldung = meldung
if any(muster in letzte_meldung for muster in KRITISCH):
kritische_meldungen.append(letzte_meldung)
if log_cb:
try:
log_cb(code, letzte_meldung)
except RipAbbruch:
raise
except Exception:
pass # Protokollieren darf einen laufenden Rip nie beenden
except RipAbbruch:
process.kill()
process.wait()
@@ -373,7 +412,7 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str
def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_cb=None,
output_dir: str = None, nur_hauptfilm: bool = False,
titel_liste: list = None) -> dict:
titel_liste: list = None, log_cb=None) -> dict:
"""Rippt eine DVD oder Blu-ray verlustfrei mit MakeMKV.
Bewusst KEIN eigener Celery-Task: der einzige Task ist worker.tasks.rip_disc,
@@ -388,7 +427,7 @@ def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_c
if titel_liste:
os.makedirs(output_dir, exist_ok=True)
return rip_titel_auswahl(device_path, output_dir, titel_liste, progress_cb)
return rip_titel_auswahl(device_path, output_dir, titel_liste, progress_cb, log_cb)
titel = "all"
if nur_hauptfilm:
@@ -397,7 +436,8 @@ def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_c
if haupt is not None:
titel = str(haupt)
# Kein Titel ermittelbar → ehrlich auf 'all' zurückfallen statt raten
return run_makemkv(device_path, output_dir, progress_cb=progress_cb, titel=titel)
return run_makemkv(device_path, output_dir, progress_cb=progress_cb, titel=titel,
log_cb=log_cb)
def rip_cd(device_path: str, disc_id: str, progress_cb=None, output_dir: str = None) -> dict: