"""MakeMKV-Datenverzeichnis: KEYDB.cfg, AACS-Dumps, settings.conf. WARUM ES DIESE DATEI GIBT (Befund 25.07.2026, live im Worker nachgemessen): Bei 4K-UHD-Discs meldet MakeMKV "The volume key is unknown for this disc". Das Laufwerk ist dabei in Ordnung — makemkvcon meldet "Using LibreDrive mode (v06.3)" und liest die Disc — MakeMKV kennt nur den Schluessel DIESER Pressung nicht. Und es holt ihn NICHT mehr online nach: * /root/.MakeMKV/_private_data.tar enthielt ausser der Index-Datei keine einzige hkd_*.bin, also nie einen heruntergeladenen Schluessel; * auch mit erzwungener frischer Pruefung (update.conf geloescht, Meldung 5074 belegt den Web-Kontakt) und mit app_UpdateEnable = "1" kam keiner; * die im MakeMKV-Forum dokumentierten Schluessel-Server hkdata.fairuse.org und hkdata.crabdance.com loesen weltweit nicht mehr auf (gegen Fritz!Box, 8.8.8.8 und 1.1.1.1 geprueft: NXDOMAIN). Betroffen war Akira UHD (MKB v76, Pressung von Dezember 2020) — also gerade KEINE Neuerscheinung. Die bisherige Fehlermeldung ("Disc neuer als die Schluessel-Datenbank, mit einem der naechsten Updates rippbar") war damit schlicht falsch. Der einzige heute funktionierende Weg ist eine KEYDB.cfg im MakeMKV-Datenverzeichnis. Rippy liefert KEINE Schluessel mit, laedt keine herunter und verteilt keine — es stellt nur den Platz dafuer bereit und zeigt ehrlich an, was dort liegt. Die Datei bringt der Nutzer selbst mit. QUELLEN (AGENTS Regel D — externe Schnittstellen nie aus dem Kopf): * Datenverzeichnis und Dateiname GROSS geschrieben (unter Linux case-sensitiv), dort geloest mit "cp KEYDB.cfg ~/.MakeMKV/": https://forum.makemkv.com/forum/viewtopic.php?t=30636 * Heruntergeladene Schluessel liegen als hkd_*.bin in _private_data.tar: https://forum.makemkv.com/forum/viewtopic.php?t=32675 * Zeilenformat der KEYDB.cfg (libaacs): Disc-Kennung = 40 Hex-Zeichen, optional mit 0x-Praefix, dann "= Titel", danach optionale Felder wie "| V | <32 Hex>"; Zeilen ab ";" sind Kommentare: https://github.com/ShiftMediaProject/libaacs/blob/master/KEYDB.cfg * Den AACS-Dump legt MakeMKV selbst ab, Meldung 3332 "Saved AACS dump file as file:///root/.MakeMKV/.tgz" (am 25.07.2026 so beobachtet). ZWILLINGSDATEI: liegt identisch unter docker/api/ und docker/worker/ — beide Images brauchen sie, ein gemeinsames Paket gibt es in diesem Projekt nicht (gleiche Lage wie bei db.py). Aenderungen IMMER in BEIDEN Dateien nachziehen. """ import os import re from datetime import datetime, timezone # Container-Pfad aus der Umgebung: /root/.MakeMKV im Worker (nur dort sucht # makemkvcon), /app/makemkv-data in der API. Beide zeigen laut # docker-compose.yml auf DASSELBE Host-Verzeichnis. DATEN_DIR = os.getenv("MAKEMKV_DATA_DIR", "/root/.MakeMKV") # GROSS geschrieben — unter Linux case-sensitiv, "keydb.cfg" wird ignoriert. KEYDB_NAME = "KEYDB.cfg" # Obergrenze fuer den Upload. Eine vollstaendige oeffentliche KEYDB.cfg liegt # im einstelligen MB-Bereich; 64 MB sind reichlich Luft und verhindern, dass # eine versehentlich hochgeladene Riesendatei den Speicher vollschreibt. MAX_KEYDB_BYTES = 64 * 1024 * 1024 # Eine Disc-Zeile beginnt mit der 40 Zeichen langen Hex-Kennung (optional mit # 0x davor), danach folgt das Gleichheitszeichen. Alles andere (Kommentare ab # ";", Leerzeilen, Fortsetzungsfelder) zaehlt nicht als Eintrag. _DISC_ZEILE = re.compile(r"^\s*(?:0x)?[0-9a-fA-F]{40}\s*=") def _iso(zeitstempel: float) -> str: """Unix-Zeit -> ISO-8601 in UTC (sekundengenau), wie db.utcnow() es tut.""" return datetime.fromtimestamp(zeitstempel, timezone.utc).isoformat(timespec="seconds") def keydb_pfad(daten_dir: str = None) -> str: """Voller Pfad zur KEYDB.cfg im Datenverzeichnis.""" return os.path.join(daten_dir or DATEN_DIR, KEYDB_NAME) def zaehle_disc_eintraege(inhalt: str) -> int: """Zeilen mit Disc-Kennung zaehlen (pure Funktion, testbar). Bewusst eine Heuristik und keine vollstaendige Auswertung: MakeMKV liest die Datei mit seinem eigenen Parser, und wie viele Eintraege es daraus macht, ist von aussen nicht sichtbar. Die Zahl dient nur dazu, im UI "da liegt wirklich etwas drin" von "leere oder falsche Datei" zu unterscheiden — sie wird deshalb auch genau so beschriftet. """ return sum(1 for zeile in inhalt.splitlines() if _DISC_ZEILE.match(zeile)) def keydb_pruefen(inhalt: str) -> str: """Prueft hochgeladenen Inhalt; gibt deutschen Fehlertext oder "" zurueck. Verhindert den haeufigsten Bedienfehler: statt der KEYDB.cfg landet die HTML-Fehlerseite eines Downloads oder eine leere Datei im Verzeichnis — MakeMKV wuerde dann still weiter "volume key is unknown" melden. """ if not inhalt.strip(): return "Die Datei ist leer." if len(inhalt.encode("utf-8")) > MAX_KEYDB_BYTES: return ( "Die Datei ist groesser als " f"{MAX_KEYDB_BYTES // (1024 * 1024)} MB — das ist keine KEYDB.cfg." ) if inhalt.lstrip()[:1] == "<": return ( "Das sieht nach HTML aus, nicht nach einer KEYDB.cfg — " "vermutlich wurde eine Fehlerseite statt der Datei geladen." ) if zaehle_disc_eintraege(inhalt) == 0: return ( "Keine einzige Zeile mit Disc-Kennung gefunden (40 Hex-Zeichen, " "dann ein Gleichheitszeichen). Das ist keine KEYDB.cfg." ) return "" def keydb_status(daten_dir: str = None) -> dict: """Zustand der KEYDB.cfg. Fehlt sie, ist das der Normalfall, kein Fehler.""" pfad = keydb_pfad(daten_dir) try: angaben = os.stat(pfad) except OSError: return { "vorhanden": False, "pfad": pfad, "groesse_bytes": 0, "eintraege": 0, "geaendert": "", } eintraege = 0 try: with open(pfad, encoding="utf-8", errors="replace") as datei: eintraege = zaehle_disc_eintraege(datei.read()) except OSError: pass # Datei da, aber unlesbar: Groesse/Datum stimmen trotzdem return { "vorhanden": True, "pfad": pfad, "groesse_bytes": angaben.st_size, "eintraege": eintraege, "geaendert": _iso(angaben.st_mtime), } def keydb_schreiben(inhalt: str, daten_dir: str = None) -> dict: """Schreibt die KEYDB.cfg atomar und meldet den neuen Zustand. Erst in eine Nebendatei, dann os.replace: waehrend ein Rip laeuft, darf makemkvcon niemals eine halb geschriebene Datei zu sehen bekommen. """ pfad = keydb_pfad(daten_dir) os.makedirs(os.path.dirname(pfad), exist_ok=True) neben = pfad + ".neu" try: with open(neben, "w", encoding="utf-8", newline="\n") as datei: datei.write(inhalt) os.replace(neben, pfad) except OSError: # Die Nebendatei nie liegen lassen: eine halb geschriebene # KEYDB.cfg.neu verwirrt jeden, der ins Verzeichnis schaut, und # belegt im Extremfall 64 MB, die niemand mehr aufräumt. try: os.remove(neben) except OSError: pass raise return keydb_status(daten_dir) def keydb_loeschen(daten_dir: str = None) -> dict: """Entfernt die KEYDB.cfg (z. B. nach einem Fehlgriff beim Hochladen). Nur "Datei war schon weg" wird geschluckt — das ist das gewünschte Ergebnis. Jeder andere Fehler (schreibgeschützter Mount, fremder Eigentümer) MUSS nach oben durch: sonst meldete die API einen Erfolg, den es nicht gab, und die Datei wirkte beim nächsten Rip weiter. """ try: os.remove(keydb_pfad(daten_dir)) except FileNotFoundError: pass return keydb_status(daten_dir) def ist_aacs_dump(name: str) -> bool: """Dateiname eines AACS-Dumps? (pure Funktion, testbar) MakeMKV legt ihn als .tgz direkt im Datenverzeichnis ab (Meldung 3332). Pfadtrenner und fuehrende Punkte werden hier schon ausgeschlossen, damit der Download-Endpunkt keine Pfad-Tricks erlaubt. """ return ( name.endswith(".tgz") and "/" not in name and "\\" not in name and not name.startswith(".") ) def dumps_auflisten(daten_dir: str = None) -> list: """Alle AACS-Dumps im Datenverzeichnis, neueste zuerst.""" ordner = daten_dir or DATEN_DIR try: namen = os.listdir(ordner) except OSError: return [] liste = [] for name in namen: if not ist_aacs_dump(name): continue try: angaben = os.stat(os.path.join(ordner, name)) except OSError: continue liste.append( { "name": name, "groesse_bytes": angaben.st_size, "geaendert": _iso(angaben.st_mtime), "_sort": angaben.st_mtime, } ) liste.sort(key=lambda eintrag: eintrag["_sort"], reverse=True) for eintrag in liste: del eintrag["_sort"] return liste def settings_conf_zusammenfuehren(inhalt: str, key: str) -> str: """app_Key setzen, ohne den Rest der settings.conf zu verlieren (pure). Bis zum 25.07.2026 haben entrypoint.sh UND tasks.py die Datei komplett ueberschrieben. Mit dem jetzt persistenten Datenverzeichnis waere damit bei jedem Containerstart und vor jedem Rip alles andere weg — z. B. app_UpdateEnable. Leerer Key laesst die vorhandene Zeile ebenfalls fallen, damit ein bewusst geleerter Key nicht heimlich weiterwirkt. """ zeilen = [z for z in inhalt.splitlines() if not z.lstrip().startswith("app_Key")] if key: zeilen.append('app_Key = "{}"'.format(key)) text = "\n".join(zeilen).strip("\n") return text + "\n" if text else ""