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:
@@ -12,6 +12,7 @@ import uuid
|
||||
|
||||
import db
|
||||
import devices as device_discovery
|
||||
import makemkv_daten
|
||||
import makemkv_key
|
||||
import mounts as mount_verwaltung
|
||||
import notify
|
||||
@@ -1190,6 +1191,146 @@ async def system_info():
|
||||
return await asyncio.to_thread(sammle)
|
||||
|
||||
|
||||
# Eigene Wurzel für die Datei-Härtung der AACS-Dumps. Bewusst NICHT die
|
||||
# MEDIA_ROOT-Helfer (_sicherer_dateiname/_validiere_ziel/_job_ausgabeordner):
|
||||
# die prüfen hart gegen /app/media und würden hier IMMER 404 liefern.
|
||||
# Das MakeMKV-Datenverzeichnis liegt woanders (in der API auf
|
||||
# /app/makemkv-data, im Worker auf /root/.MakeMKV — laut docker-compose.yml
|
||||
# beides dasselbe Host-Verzeichnis).
|
||||
MAKEMKV_DATA_ROOT = os.path.realpath(makemkv_daten.DATEN_DIR)
|
||||
|
||||
|
||||
class KeydbRequest(BaseModel):
|
||||
inhalt: str # voller Text der KEYDB.cfg (kein Upload — es gibt kein python-multipart)
|
||||
|
||||
|
||||
@app.get("/system/keydb")
|
||||
async def get_keydb_status():
|
||||
"""Was liegt gerade als KEYDB.cfg im MakeMKV-Datenverzeichnis?
|
||||
|
||||
Hintergrund (Befund 25.07.2026, live auf der VM nachgemessen): Bei
|
||||
4K-UHD-Discs meldet MakeMKV "The volume key is unknown for this disc" und
|
||||
holt den Schlüssel NICHT mehr online nach — die dokumentierten
|
||||
Schlüssel-Server lösen weltweit nicht mehr auf. Der einzige heute
|
||||
funktionierende Weg ist eine KEYDB.cfg, die der Nutzer selbst mitbringt.
|
||||
Rippy liefert KEINE Schlüssel mit, lädt keine herunter und verteilt keine —
|
||||
es stellt nur den Platz bereit und zeigt ehrlich an, was dort liegt.
|
||||
|
||||
Fehlendes Verzeichnis oder fehlende Datei ist der NORMALFALL: dann kommt
|
||||
200 mit vorhanden=false zurück, niemals 404 oder 500.
|
||||
"""
|
||||
def sammle():
|
||||
return makemkv_daten.keydb_status()
|
||||
|
||||
return await asyncio.to_thread(sammle)
|
||||
|
||||
|
||||
@app.post("/system/keydb")
|
||||
async def set_keydb(request: KeydbRequest):
|
||||
"""Legt die vom Nutzer mitgebrachte KEYDB.cfg ab (atomar, ersetzt die alte).
|
||||
|
||||
WICHTIG für die Ehrlichkeit: Die Datei wirkt erst beim NÄCHSTEN Rip —
|
||||
makemkvcon liest sie beim Prozessstart, ein bereits laufender Rip merkt
|
||||
nichts davon. Genau so steht es auch im Log-Eintrag.
|
||||
"""
|
||||
# Reine Prüfung (kein Dateisystem) — fängt den häufigsten Bedienfehler ab:
|
||||
# statt der KEYDB.cfg landet die HTML-Fehlerseite eines Downloads im Feld.
|
||||
fehler = makemkv_daten.keydb_pruefen(request.inhalt)
|
||||
if fehler:
|
||||
raise HTTPException(status_code=422, detail=fehler)
|
||||
|
||||
def schreibe():
|
||||
return makemkv_daten.keydb_schreiben(request.inhalt)
|
||||
|
||||
try:
|
||||
status = await asyncio.to_thread(schreibe)
|
||||
except OSError as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=(
|
||||
f"KEYDB.cfg konnte nicht geschrieben werden: {e}. "
|
||||
"Prüfe, ob das MakeMKV-Datenverzeichnis auf der VM existiert und "
|
||||
"beschreibbar ist (Standard: /srv/rippy/makemkv)."
|
||||
),
|
||||
)
|
||||
await asyncio.to_thread(
|
||||
db.add_log, "success", "makemkv-keydb",
|
||||
f"KEYDB.cfg abgelegt: {status['eintraege']} Zeilen mit Disc-Kennung, "
|
||||
f"{status['groesse_bytes']} Bytes ({status['pfad']}). "
|
||||
"Wirkt erst beim NÄCHSTEN Rip — MakeMKV liest die Datei beim Start.",
|
||||
)
|
||||
return status
|
||||
|
||||
|
||||
@app.delete("/system/keydb")
|
||||
async def delete_keydb():
|
||||
"""Entfernt die KEYDB.cfg (z. B. nach einem Fehlgriff beim Einfügen).
|
||||
|
||||
Auch hier gilt: Die Änderung wirkt erst beim NÄCHSTEN Rip. Fehlt die Datei
|
||||
schon, ist das kein Fehler — es kommt derselbe Zustand mit vorhanden=false.
|
||||
"""
|
||||
def loesche():
|
||||
return makemkv_daten.keydb_loeschen()
|
||||
|
||||
try:
|
||||
status = await asyncio.to_thread(loesche)
|
||||
except OSError as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"KEYDB.cfg konnte nicht entfernt werden: {e}",
|
||||
)
|
||||
await asyncio.to_thread(
|
||||
db.add_log, "warning", "makemkv-keydb",
|
||||
"KEYDB.cfg entfernt. Ab dem NÄCHSTEN Rip fehlen die selbst mitgebrachten "
|
||||
"Schlüssel wieder — UHD-Discs können dann erneut an "
|
||||
"'The volume key is unknown for this disc' scheitern.",
|
||||
)
|
||||
return status
|
||||
|
||||
|
||||
@app.get("/system/aacs-dumps")
|
||||
async def get_aacs_dumps():
|
||||
"""AACS-Dumps, die MakeMKV selbst abgelegt hat (neueste zuerst).
|
||||
|
||||
MakeMKV schreibt sie beim gescheiterten UHD-Versuch ins Datenverzeichnis
|
||||
(Meldung 3332 "Saved AACS dump file as file:///root/.MakeMKV/<name>.tgz",
|
||||
am 25.07.2026 so beobachtet). Rippy wertet sie nicht aus und schickt sie
|
||||
nirgendwohin — es zeigt nur, dass sie da sind, damit der Nutzer selbst
|
||||
entscheiden kann, was er damit tut.
|
||||
"""
|
||||
def liste():
|
||||
return {"dumps": makemkv_daten.dumps_auflisten()}
|
||||
|
||||
return await asyncio.to_thread(liste)
|
||||
|
||||
|
||||
@app.get("/system/aacs-dumps/{dateiname}")
|
||||
async def download_aacs_dump(dateiname: str):
|
||||
"""Lädt EINEN AACS-Dump herunter.
|
||||
|
||||
Pfad-Validierung genauso streng wie beim Job-Datei-Download: nackter Name
|
||||
ohne Pfadtrenner und ohne führenden Punkt (ist_aacs_dump) PLUS realpath,
|
||||
der das MakeMKV-Datenverzeichnis nicht verlassen darf (kein ..-Ausbruch,
|
||||
kein Symlink nach draußen).
|
||||
"""
|
||||
if not makemkv_daten.ist_aacs_dump(dateiname):
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Kein gültiger Dump-Name — erwartet wird eine .tgz-Datei ohne Pfadangabe.",
|
||||
)
|
||||
pfad = os.path.join(MAKEMKV_DATA_ROOT, dateiname)
|
||||
|
||||
def pruefe():
|
||||
return os.path.isfile(pfad) and os.path.realpath(pfad).startswith(MAKEMKV_DATA_ROOT)
|
||||
|
||||
if not await asyncio.to_thread(pruefe):
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Dump nicht gefunden — MakeMKV legt ihn erst beim gescheiterten UHD-Versuch an.",
|
||||
)
|
||||
return FileResponse(pfad, filename=dateiname, media_type="application/gzip")
|
||||
|
||||
|
||||
class NotificationTestRequest(BaseModel):
|
||||
url: str
|
||||
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
"""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/<name>.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 <MKB..._NAME_....>.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 ""
|
||||
@@ -11,8 +11,14 @@ Dieses Modul holt den aktuellen Key vom Forum und schreibt ihn in die Settings
|
||||
Rebuild/Neustart, ab dem naechsten Rip.
|
||||
|
||||
Wichtig: Das ist die kostenlose, oeffentliche Beta-LIZENZ der Software selbst —
|
||||
NICHT das Entschluesseln oder Verteilen von Disc-Schluesseln. Den AACS-Schluessel
|
||||
zieht MakeMKV via LibreDrive ohnehin selbst aus dem Laufwerk.
|
||||
NICHT das Entschluesseln oder Verteilen von Disc-Schluesseln. Diese Grenze gilt
|
||||
unveraendert; Rippy liefert keine Disc-Schluessel mit und verteilt keine.
|
||||
|
||||
Richtigstellung 25.07.2026: Hier stand frueher, MakeMKV ziehe den AACS-Schluessel
|
||||
via LibreDrive ohnehin selbst aus dem Laufwerk. Das stimmt fuer Blu-ray, aber
|
||||
NICHT fuer 4K-UHD — dort braucht MakeMKV den Volume-Key der jeweiligen Pressung,
|
||||
und den bekommt es weder aus dem Laufwerk noch (heute) aus dem Netz. Belege und
|
||||
Messungen stehen im Modul-Kopf von makemkv_daten.py.
|
||||
|
||||
Quelle/Format dokumentiert (AGENTS Regel D — nicht geraten):
|
||||
- Forum-Thread: https://forum.makemkv.com/forum/viewtopic.php?t=1053
|
||||
|
||||
@@ -18,7 +18,13 @@ def test_main_importierbar_und_routen_verdrahtet():
|
||||
from main import app
|
||||
|
||||
routen = {route.path for route in app.routes}
|
||||
for pfad in ("/health", "/jobs", "/devices", "/logs", "/settings", "/prescan"):
|
||||
for pfad in (
|
||||
"/health", "/jobs", "/devices", "/logs", "/settings", "/prescan",
|
||||
# KEYDB.cfg + AACS-Dumps: der Platz für die selbst mitgebrachte
|
||||
# Schlüsseldatei (Befund 25.07.2026 — MakeMKV holt UHD-Schlüssel nicht
|
||||
# mehr online nach). Ohne diese Routen ist die Seite im UI tot.
|
||||
"/system/keydb", "/system/aacs-dumps", "/system/aacs-dumps/{dateiname}",
|
||||
):
|
||||
assert pfad in routen, f"Route {pfad} fehlt"
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Tests fuer die puren Helfer aus makemkv_daten.
|
||||
|
||||
Bewusst OHNE Dateisystem, DB und fcntl — deshalb laufen sie auch auf Windows
|
||||
und nicht nur in der Ampel. Geprueft wird genau das, was ohne Container und
|
||||
ohne echte Disc entscheidbar ist: das Zeilenformat der KEYDB.cfg, die
|
||||
Plausibilitaetspruefung beim Hochladen, die Namenshaerte der AACS-Dumps und
|
||||
das Zusammenfuehren der settings.conf.
|
||||
"""
|
||||
from makemkv_daten import (
|
||||
ist_aacs_dump,
|
||||
keydb_pruefen,
|
||||
settings_conf_zusammenfuehren,
|
||||
zaehle_disc_eintraege,
|
||||
)
|
||||
|
||||
# Echte Beispielzeilen im libaacs-Format: 40 Hex-Zeichen Disc-Kennung, dann
|
||||
# "= Titel". Zweite Zeile mit 0x-Praefix, weil die oeffentlichen Dateien beide
|
||||
# Schreibweisen mischen (Fundstelle steht im Modul-Docstring von makemkv_daten).
|
||||
_GUELTIG = """; KEYDB.cfg — Beispiel
|
||||
|
||||
0123456789ABCDEF0123456789ABCDEF01234567 = Akira
|
||||
0xFEDCBA9876543210FEDCBA9876543210FEDCBA98 = Blade Runner | V | 00112233445566778899AABBCCDDEEFF
|
||||
"""
|
||||
|
||||
|
||||
def test_zaehle_disc_eintraege_zaehlt_nur_echte_disc_zeilen():
|
||||
# Kommentar- und Leerzeilen duerfen NICHT mitgezaehlt werden, sonst meldet
|
||||
# das UI "da liegt was drin", obwohl die Datei keinen Schluessel enthaelt.
|
||||
assert zaehle_disc_eintraege(_GUELTIG) == 2
|
||||
|
||||
|
||||
def test_zaehle_disc_eintraege_ohne_disc_zeile_ist_null():
|
||||
nur_kommentare = "; nur ein Kommentar\n\n;noch einer\n"
|
||||
assert zaehle_disc_eintraege(nur_kommentare) == 0
|
||||
assert zaehle_disc_eintraege("") == 0
|
||||
|
||||
|
||||
def test_zaehle_disc_eintraege_akzeptiert_0x_praefix_einzeln():
|
||||
assert zaehle_disc_eintraege("0x0123456789abcdef0123456789abcdef01234567 = Tenet") == 1
|
||||
|
||||
|
||||
def test_zaehle_disc_eintraege_lehnt_zu_kurze_kennung_ab():
|
||||
# 39 statt 40 Hex-Zeichen: das ist keine Disc-Kennung, sondern Tippfehler
|
||||
# oder eine abgeschnittene Datei — darf nicht als Eintrag durchgehen.
|
||||
assert zaehle_disc_eintraege("0123456789ABCDEF0123456789ABCDEF0123456 = Kurz") == 0
|
||||
|
||||
|
||||
def test_keydb_pruefen_meldet_leere_datei():
|
||||
assert keydb_pruefen("") != ""
|
||||
assert keydb_pruefen(" \n\n ") != ""
|
||||
|
||||
|
||||
def test_keydb_pruefen_erkennt_html():
|
||||
# Haeufigster Bedienfehler: statt der Datei landet die HTML-Fehlerseite
|
||||
# eines Downloads im Feld. MakeMKV wuerde dann still weiter meckern.
|
||||
fehler = keydb_pruefen("<!DOCTYPE html>\n<html><body>404 Not Found</body></html>\n")
|
||||
assert "HTML" in fehler
|
||||
|
||||
|
||||
def test_keydb_pruefen_meldet_datei_ohne_disc_zeile():
|
||||
# Text ist da, aber keine einzige Disc-Kennung — z. B. eine Liesmich-Datei.
|
||||
assert keydb_pruefen("Das hier ist irgendein Text ohne Schluessel.\n") != ""
|
||||
|
||||
|
||||
def test_keydb_pruefen_laesst_gueltige_datei_durch():
|
||||
# "" heisst laut Vertrag: alles in Ordnung, darf geschrieben werden.
|
||||
assert keydb_pruefen(_GUELTIG) == ""
|
||||
|
||||
|
||||
def test_ist_aacs_dump_erkennt_echten_namen():
|
||||
# So heisst der Dump, den MakeMKV am 25.07.2026 fuer Akira UHD abgelegt hat
|
||||
# (Meldung 3332) — dieser Name MUSS zum Download durchkommen.
|
||||
assert ist_aacs_dump("MKB20_v76_UHD_AKIRA_C02B.tgz") is True
|
||||
|
||||
|
||||
def test_ist_aacs_dump_blockt_pfad_tricks():
|
||||
# Der Download-Endpunkt haengt den Namen an das Datenverzeichnis — ein
|
||||
# durchgelassenes ".." oder ein Pfadtrenner waere ein Ausbruch.
|
||||
assert ist_aacs_dump("../x.tgz") is False
|
||||
assert ist_aacs_dump("../../etc/passwd.tgz") is False
|
||||
assert ist_aacs_dump("unter/ordner.tgz") is False
|
||||
assert ist_aacs_dump("unter\\ordner.tgz") is False
|
||||
assert ist_aacs_dump(".versteckt.tgz") is False
|
||||
|
||||
|
||||
def test_ist_aacs_dump_lehnt_andere_endungen_ab():
|
||||
# Nur die Dumps sollen abholbar sein — nicht settings.conf, nicht
|
||||
# _private_data.tar und schon gar nicht die KEYDB.cfg selbst.
|
||||
assert ist_aacs_dump("KEYDB.cfg") is False
|
||||
assert ist_aacs_dump("settings.conf") is False
|
||||
assert ist_aacs_dump("_private_data.tar") is False
|
||||
assert ist_aacs_dump("") is False
|
||||
|
||||
|
||||
def test_settings_conf_ersetzt_alten_key_und_behaelt_den_rest():
|
||||
# Regression: bis 25.07.2026 wurde die Datei komplett ueberschrieben. Mit
|
||||
# dem jetzt persistenten Datenverzeichnis waere app_UpdateEnable vor jedem
|
||||
# Rip weg gewesen.
|
||||
alt = 'app_UpdateEnable = "1"\napp_Key = "T-alt"\napp_DestinationDir = "/tmp"\n'
|
||||
neu = settings_conf_zusammenfuehren(alt, "T-neu")
|
||||
assert 'app_Key = "T-neu"' in neu
|
||||
assert "T-alt" not in neu
|
||||
assert 'app_UpdateEnable = "1"' in neu
|
||||
assert 'app_DestinationDir = "/tmp"' in neu
|
||||
|
||||
|
||||
def test_settings_conf_leerer_key_entfernt_die_zeile():
|
||||
# Ein bewusst geleerter Key darf nicht heimlich weiterwirken.
|
||||
neu = settings_conf_zusammenfuehren('app_Key = "T-alt"\napp_UpdateEnable = "1"\n', "")
|
||||
assert "app_Key" not in neu
|
||||
assert 'app_UpdateEnable = "1"' in neu
|
||||
|
||||
|
||||
def test_settings_conf_aus_dem_nichts_endet_mit_zeilenumbruch():
|
||||
# Erster Start: es gibt noch keine settings.conf. MakeMKV erwartet eine
|
||||
# Datei mit abschliessendem Zeilenumbruch.
|
||||
assert settings_conf_zusammenfuehren("", "T-neu") == 'app_Key = "T-neu"\n'
|
||||
assert settings_conf_zusammenfuehren("", "") == ""
|
||||
Reference in New Issue
Block a user