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
+7
View File
@@ -33,6 +33,13 @@ OMDB_API_KEY=
# nächsten Rip, ohne Neustart, und schlägt diesen Env-Wert). # nächsten Rip, ohne Neustart, und schlägt diesen Env-Wert).
MAKEMKV_APP_KEY= MAKEMKV_APP_KEY=
# MakeMKV-Datenverzeichnis auf dem HOST (bleibt über Rebuilds hinweg erhalten).
# Hier hinein gehört die KEYDB.cfg für 4K-UHD-Discs; hier landen auch die
# AACS-Dumps fehlgeschlagener Discs. Beides ist ab Einstellungen → System
# im Browser erreichbar — dieser Pfad ist nur für den Fall, dass du die
# Dateien direkt auf der Maschine anfassen willst.
#MAKEMKV_DATA_HOST=/srv/rippy/makemkv
# MakeMKV-Version fürs Worker-Image (Einstellungen → System meldet Updates). # MakeMKV-Version fürs Worker-Image (Einstellungen → System meldet Updates).
# Update: Version hier anheben, dann auf der Rippy-Maschine # Update: Version hier anheben, dann auf der Rippy-Maschine
# docker compose build worker && docker compose up -d worker # docker compose build worker && docker compose up -d worker
+4 -1
View File
@@ -55,10 +55,13 @@ Bibliotheks-APIs: `--help`/Doku prüfen und die Fundstelle im Commit nennen.
- Kein Proxmox-LXC-Nesting-Workaround — das Projekt lebt bewusst in normalem Docker. - Kein Proxmox-LXC-Nesting-Workaround — das Projekt lebt bewusst in normalem Docker.
- Kein ARM-Fork — Rippy ist ein Eigenbau von Grund auf. - Kein ARM-Fork — Rippy ist ein Eigenbau von Grund auf.
## Aktueller Stand (24.07.2026) ## Aktueller Stand (25.07.2026)
-**E2E bewiesen (v3.1):** BD-50 komplett durch die Kette (43 GB → 4,8 GB) -**E2E bewiesen (v3.1):** BD-50 komplett durch die Kette (43 GB → 4,8 GB)
-**Etappe 13 (v3.2):** Universal-Komfort-Runde — Media-Server-Integration, -**Etappe 13 (v3.2):** Universal-Komfort-Runde — Media-Server-Integration,
echte Benachrichtigungen, SMB-Klartext-Fehler, UHD-Arbeitsverzeichnis, echte Benachrichtigungen, SMB-Klartext-Fehler, UHD-Arbeitsverzeichnis,
MakeMKV-Key via UI, Job-Detail-Popup, Toast-Feedback, Ampel-Blocker behoben MakeMKV-Key via UI, Job-Detail-Popup, Toast-Feedback, Ampel-Blocker behoben
-**Etappe 17 (v3.10):** 4K-UHD-Disc-Schlüssel — persistentes
MakeMKV-Datenverzeichnis + `KEYDB.cfg` im UI (der Nachweis mit einer
echten Schlüssel-Datei steht noch aus)
- 📝 **Details immer in SAVEPOINT.md** — diese Sektion nennt nur die Etappe - 📝 **Details immer in SAVEPOINT.md** — diese Sektion nennt nur die Etappe
+16
View File
@@ -127,6 +127,7 @@ Ein modular aufgebautes System, das bei Disc-Einwurf automatisch den Typ erkennt
| Risiko | Status | Behandlung | | Risiko | Status | Behandlung |
|--------|--------|------------| |--------|--------|------------|
| MakeMKV-Beta-Key-Management | Gelöst | Key-Erneuerung als Cron-Job; DMCA-Ausnahme in DE | | MakeMKV-Beta-Key-Management | Gelöst | Key-Erneuerung als Cron-Job; DMCA-Ausnahme in DE |
| **Disc-Schlüssel für 4K-UHD (AACS 2.0)** | **Offen — durch Rippy nicht lösbar** | Am 25.07.2026 auf der VM gemessen: MakeMKV holt für unbekannte UHD-Discs keinen Schlüssel mehr (kein Netz-Versuch im Log, keine `hkd_*.bin` im `_private_data.tar`), die dokumentierten Schlüssel-Server sind weltweit tot. Rippy stellt NUR ein persistentes Datenverzeichnis für eine vom Nutzer selbst mitgebrachte `KEYDB.cfg` bereit und gibt die AACS-Dumps heraus — es liefert, lädt und verteilt keine Schlüssel. Siehe §10 (25.07.2026) |
| LXC-Device-Node-Änderungen | Gelöst | udev-Resolver (UUID/Serial) | | LXC-Device-Node-Änderungen | Gelöst | udev-Resolver (UUID/Serial) |
| Pending-Queue / State-Manager | Gelöst | Redis mit AOF-Persistence | | Pending-Queue / State-Manager | Gelöst | Redis mit AOF-Persistence |
| Hybrid-Discs | Offen | MVP erkennt nur Standard; als "Kann" notiert | | Hybrid-Discs | Offen | MVP erkennt nur Standard; als "Kann" notiert |
@@ -182,6 +183,21 @@ Ein modular aufgebautes System, das bei Disc-Einwurf automatisch den Typ erkennt
passlib/bcrypt-Abhängigkeit hat die CI-Ampel gebrochen. Rate-Limiting passlib/bcrypt-Abhängigkeit hat die CI-Ampel gebrochen. Rate-Limiting
(pro IP) bleibt. Wer Rippy je nach außen öffnet, stellt einen (pro IP) bleibt. Wer Rippy je nach außen öffnet, stellt einen
Reverse-Proxy mit eigener Auth davor (z. B. Authelia/Caddy basicauth). Reverse-Proxy mit eigener Auth davor (z. B. Authelia/Caddy basicauth).
- **25.07.2026 — 4K-UHD-Disc-Schlüssel: Rippy stellt Platz bereit, keine
Schlüssel:** Das Muss-Feature „MakeMKV-Ripping (lossless)" bleibt
unverändert; ergänzt wird nur ein **persistentes MakeMKV-Datenverzeichnis**
(`MAKEMKV_DATA_HOST`, im Worker `/root/.MakeMKV`, in der API
`/app/makemkv-data`) samt Bedienung im UI. Begründung: Am 25.07.2026 auf
der VM nachgemessen — MakeMKV versucht bei einer unbekannten UHD-Disc gar
keinen Online-Abruf mehr, und die früher genutzten Schlüssel-Server sind
weltweit tot; der einzige heute funktionierende Weg ist eine `KEYDB.cfg`
im Datenverzeichnis. **Rippy liefert und verteilt KEINE Disc-Schlüssel und
lädt auch keine herunter** — es hält nur den Platz für eine Datei bereit,
die der Nutzer selbst mitbringt, zeigt ehrlich an, was dort liegt, und gibt
die AACS-Dumps heraus, die MakeMKV ohnehin selbst schreibt. Das ist genau
die Grenze, die `docker/api/makemkv_key.py` in Zeile 14 zieht: die
kostenlose Beta-LIZENZ der Software ist etwas anderes als das
Entschlüsseln oder Verteilen von Disc-Schlüsseln.
- **24.07.2026 — Serien-Flow:** Staffel-Ablage <Serie>/Season NN plus - **24.07.2026 — Serien-Flow:** Staffel-Ablage <Serie>/Season NN plus
Episoden-Zuordnung per Laufzeitabgleich (TMDB) — erfüllt Etappe-12-Ziel Episoden-Zuordnung per Laufzeitabgleich (TMDB) — erfüllt Etappe-12-Ziel
„Serien-Episoden-Erkennung" in der ersten Ausbaustufe (nur bei „Serien-Episoden-Erkennung" in der ersten Ausbaustufe (nur bei
+55 -6
View File
@@ -74,9 +74,26 @@ NICHT als emuliertes CD-ROM (`media=cdrom`), das kann keine SCSI-Kommandos.
nur die Benennung. **Jellyfin/Emby**: Server-URL + API-Key eintragen, nur die Benennung. **Jellyfin/Emby**: Server-URL + API-Key eintragen,
dann stößt Rippy nach jedem fertigen Rip sofort einen Bibliotheks-Scan dann stößt Rippy nach jedem fertigen Rip sofort einen Bibliotheks-Scan
an — Disc rein, Film erscheint im Server. an — Disc rein, Film erscheint im Server.
5. **4K-UHD**: braucht ein LibreDrive-fähiges Laufwerk (MakeMKV-Forum: 5. **4K-UHD**: braucht **zwei** Dinge — ein LibreDrive-fähiges Laufwerk
„Ultimate UHD Drives Flashing Guide"). Normale BD/DVD gehen mit jedem (MakeMKV-Forum: „Ultimate UHD Drives Flashing Guide") **und** einen
Laufwerk. ⚠️ UHD-Rohdaten sind bis 100 GB groß — wenn die Platte der passenden Disc-Schlüssel. Normale BD/DVD gehen mit jedem Laufwerk und
ohne Schlüssel-Datei.
**Der Schlüssel ist heute die eigentliche Hürde.** Am 25.07.2026 auf
der Rippy-VM gemessen (Akira UHD, MKB v76, Pressung Dezember 2020):
Laufwerk und MakeMKV waren in Ordnung — „Using LibreDrive mode
(v06.3)", die Disc wurde gelesen — und trotzdem kam „The volume key is
unknown for this disc". MakeMKV versucht dabei **gar nicht mehr**,
online einen Schlüssel zu holen, und die früher genutzten
Schlüssel-Server (`hkdata.fairuse.org`, `hkdata.crabdance.com`) lösen
weltweit nicht mehr auf. Ein MakeMKV-Update ändert daran nichts.
Der einzige Weg, der heute funktioniert, ist eine Datei **`KEYDB.cfg`**
(GROSS geschrieben — Linux unterscheidet Groß- und Kleinschreibung) im
MakeMKV-Datenverzeichnis. Die legst du unter **Einstellungen → System**
ab (siehe unten).
⚠️ **Rippy liefert keine Schlüssel mit, lädt keine herunter und
verteilt keine.** Rippy stellt nur den Platz für eine Datei bereit, die
du selbst mitbringst, und zeigt dir ehrlich an, was dort liegt.
⚠️ UHD-Rohdaten sind bis 100 GB groß — wenn die Platte der
Rippy-Maschine dafür zu klein ist, lege das **Arbeitsverzeichnis** Rippy-Maschine dafür zu klein ist, lege das **Arbeitsverzeichnis**
(Einstellungen → Verarbeitung) auf eine eingehängte Freigabe, z. B. (Einstellungen → Verarbeitung) auf eine eingehängte Freigabe, z. B.
`/app/media/nas-arbeit`. Rippy bricht sonst VOR dem Rip mit einer `/app/media/nas-arbeit`. Rippy bricht sonst VOR dem Rip mit einer
@@ -106,7 +123,7 @@ fehlgeschlagen, abgebrochen) und erkennt den Dienst an der URL selbst:
| ntfy (Handy-Push) | `https://ntfy.sh/mein-geheimes-thema` | Roh-Text + Titel | | ntfy (Handy-Push) | `https://ntfy.sh/mein-geheimes-thema` | Roh-Text + Titel |
| Eigenes (HA, n8n, …) | beliebige HTTPS-URL | `{"title","message","level"}` | | Eigenes (HA, n8n, …) | beliebige HTTPS-URL | `{"title","message","level"}` |
## System, MakeMKV-Beta-Key & Updates ## System, MakeMKV-Beta-Key, Disc-Schlüssel & Updates
**Einstellungen → System** zeigt die Werkzeug-Versionen jedes Workers **Einstellungen → System** zeigt die Werkzeug-Versionen jedes Workers
(MakeMKV, HandBrake), den Key-Status und den freien Speicherplatz. Der (MakeMKV, HandBrake), den Key-Status und den freien Speicherplatz. Der
@@ -114,11 +131,37 @@ MakeMKV-Beta-Key (wechselt ~monatlich, Forum-Thread t=1053) wird hier im
UI eingetragen und gilt ab dem **nächsten Rip** — ohne Rebuild, ohne UI eingetragen und gilt ab dem **nächsten Rip** — ohne Rebuild, ohne
Neustart; er schlägt den Key aus der `.env`. Neustart; er schlägt den Key aus der `.env`.
**Disc-Schlüssel (`KEYDB.cfg`)** — im selben Tab, nur für 4K-UHD nötig:
- **Status**: Rippy zeigt, ob eine `KEYDB.cfg` im MakeMKV-Datenverzeichnis
liegt, mit Pfad, Größe, Anzahl der Disc-Einträge und Änderungsdatum.
- **Hochladen**: Knopf „KEYDB.cfg hochladen", dann deine eigene Datei im
Datei-Dialog auswählen — mehr ist nicht zu tun. Rippy prüft den Inhalt
auf Plausibilität und lehnt Unsinn (leere Datei, versehentlich geladene
HTML-Fehlerseite) mit einer deutschen Klartext-Meldung ab, statt ihn
stillschweigend zu speichern.
- **Entfernen**: ein Knopf, die Datei ist wieder weg.
- **AACS-Dumps herunterladen**: MakeMKV legt bei jeder unbekannten Disc
selbst einen Dump ab. Genau den braucht man, wenn man im MakeMKV-Forum
um den Schlüssel für eine neue Pressung bittet — hier holst du ihn dir
aus dem Container, ohne SSH.
Der Beta-Key ist die **Lizenz für die Software**; die `KEYDB.cfg` sind
**Disc-Schlüssel** — zwei völlig verschiedene Dinge. **Rippy liefert und
lädt keine Disc-Schlüssel**, es hält nur ein persistentes Verzeichnis
dafür bereit (`MAKEMKV_DATA_HOST`, siehe Tabelle unten) — rebuild-fest,
damit deine Datei einen `docker compose build` überlebt.
**Updates:** „Auf Updates prüfen" vergleicht mit makemkv.com und den **Updates:** „Auf Updates prüfen" vergleicht mit makemkv.com und den
offiziellen HandBrake-Releases (12 h gecacht). MakeMKV-Update ohne offiziellen HandBrake-Releases (12 h gecacht). MakeMKV-Update ohne
Code-Änderung: `MAKEMKV_VERSION=x.y.z` in die `.env`, dann Code-Änderung: `MAKEMKV_VERSION=x.y.z` in die `.env`, dann
`docker compose build worker && docker compose up -d worker` — neue `docker compose build worker && docker compose up -d worker`.
Versionen bringen auch die neueste Disc-Schlüssel-Datenbank mit. ⚠️ **Richtigstellung (25.07.2026):** Hier stand früher, neue MakeMKV-
Versionen brächten „auch die neueste Disc-Schlüssel-Datenbank mit". Das
ist widerlegt — auf der VM nachgemessen: MakeMKV holt für eine unbekannte
UHD-Disc keinen Schlüssel mehr, weder mitgeliefert noch aus dem Netz. Ein
Update lohnt für Programmfehler und neue Laufwerke, hilft aber NICHT
gegen „The volume key is unknown". Dafür brauchst du die `KEYDB.cfg`.
HandBrake kommt im Docker-Worker bewusst aus Debian (stabil, hinkt der HandBrake kommt im Docker-Worker bewusst aus Debian (stabil, hinkt der
offiziellen Version hinterher); der native Windows-Worker nutzt die offiziellen Version hinterher); der native Windows-Worker nutzt die
aktuelle Version direkt. aktuelle Version direkt.
@@ -153,6 +196,7 @@ bleibt All-in-one.
| `THETVDB_API_KEY` | optional | Serien-Fallback | | `THETVDB_API_KEY` | optional | Serien-Fallback |
| `MAKEMKV_APP_KEY` | optional | MakeMKV-Beta-Key (Forum); DVDs gehen ohne — bequemer: im UI unter Einstellungen → System pflegen | | `MAKEMKV_APP_KEY` | optional | MakeMKV-Beta-Key (Forum); DVDs gehen ohne — bequemer: im UI unter Einstellungen → System pflegen |
| `MAKEMKV_URL_BASE` | optional | alternative Download-Quelle für den Image-Build | | `MAKEMKV_URL_BASE` | optional | alternative Download-Quelle für den Image-Build |
| `MAKEMKV_DATA_HOST` | optional | Host-Verzeichnis für MakeMKVs Daten (Default `/srv/rippy/makemkv`) — hier liegen deine `KEYDB.cfg` und die AACS-Dumps; persistent, überlebt jeden Rebuild |
| `OPTICAL_SR` | je Host | Host-Pfad des CD-ROM-Knotens (Default `/dev/sr0`) — siehe „Laufwerk anpassen" | | `OPTICAL_SR` | je Host | Host-Pfad des CD-ROM-Knotens (Default `/dev/sr0`) — siehe „Laufwerk anpassen" |
| `OPTICAL_SG` | je Host | Host-Pfad des sg-Knotens des Laufwerks (Default `/dev/sg1`; oft `/dev/sg0`) | | `OPTICAL_SG` | je Host | Host-Pfad des sg-Knotens des Laufwerks (Default `/dev/sg1`; oft `/dev/sg0`) |
| `POSTGRES_PASSWORD` | optional | DB-Passwort (Default `rippy`) — für exponierte Umgebungen ein starkes Passwort setzen | | `POSTGRES_PASSWORD` | optional | DB-Passwort (Default `rippy`) — für exponierte Umgebungen ein starkes Passwort setzen |
@@ -181,6 +225,11 @@ Voraussetzungen auf dem Ziel-Host:
``` ```
(reboot-fest als systemd-`.mount`-Unit persistieren.) Braucht einen klassischen (reboot-fest als systemd-`.mount`-Unit persistieren.) Braucht einen klassischen
Linux-Docker-Host mit `SYS_ADMIN` — nicht Docker-Desktop/rootless/Podman. Linux-Docker-Host mit `SYS_ADMIN` — nicht Docker-Desktop/rootless/Podman.
4. MakeMKV-Datenverzeichnis anlegen: `mkdir -p /srv/rippy/makemkv` (oder
eigenen Pfad über `MAKEMKV_DATA_HOST` in der `.env`). Dort liegen
MakeMKVs Programmzustand, die AACS-Dumps und — falls du 4K-UHD rippst —
deine eigene `KEYDB.cfg`. Das Verzeichnis gehört dem Host, damit ein
`docker compose build` deine Datei nicht wegwirft.
Dann wie im Schnellstart: klonen, `.env` füllen, `docker compose up -d Dann wie im Schnellstart: klonen, `.env` füllen, `docker compose up -d
--build`, `http://<host>` öffnen — der Einrichtungs-Assistent führt durch --build`, `http://<host>` öffnen — der Einrichtungs-Assistent führt durch
+59
View File
@@ -414,6 +414,65 @@ irrelevant und kann komplett raus."
--- ---
## Etappe 17 (25.07.2026): 4K-UHD-Schlüssel (KEYDB.cfg)
**Quelle:** Messung am 25.07. live auf der Rippy-VM im Worker-Container.
Eine 4K-UHD (Akira, MKB v76, Pressung Dezember 2020) scheitert an „The
volume key is unknown for this disc" — obwohl makemkvcon „Using LibreDrive
mode (v06.3)" und „Using direct disc access mode" meldet, die Disc liest
und den AACS-Dump ablegt (Meldung 3332). Das Debug-Log geht ohne einen
einzigen Netz-Versuch von „Loaded content hash table" direkt auf den
Fehler; `_private_data.tar` enthielt nur die Index-Datei und keine einzige
`hkd_*.bin`; auch mit gelöschter `update.conf` (Meldung 5074 belegt den
Web-Kontakt) und `app_UpdateEnable = "1"` kam kein Schlüssel; die
Forum-Schlüssel-Server `hkdata.fairuse.org` und `hkdata.crabdance.com`
lösen weltweit nicht mehr auf. **Fazit: Ein MakeMKV-Update löst das
nicht** (das behauptete v3.3 — dort richtiggestellt). Der einzige heute
funktionierende Weg ist eine `KEYDB.cfg` im MakeMKV-Datenverzeichnis.
**Gebaut:**
- [x] **Persistentes Datenverzeichnis**: `${MAKEMKV_DATA_HOST:-/srv/rippy/makemkv}`
vom Host gemountet — Worker `/root/.MakeMKV`, API `/app/makemkv-data`,
beide mit `MAKEMKV_DATA_DIR`. `KEYDB.cfg` und AACS-Dumps überleben
jeden Rebuild. `.env.example` erklärt die Variable.
- [x] **entrypoint.sh entschärft**: `settings.conf` wird ergänzt statt
überschrieben (der Beta-Key hatte sonst alles andere gelöscht),
`app_UpdateEnable = "1"` gesetzt.
- [x] **Zwillings-Modul `makemkv_daten.py`** (identisch in `docker/api/`
und `docker/worker/`) als einzige Wahrheit über das Verzeichnis:
Status lesen, Inhalt prüfen, atomar schreiben, löschen, Dumps
auflisten — alles reine Funktionen, damit die Ampel sie ohne
Postgres/Redis testen kann.
- [x] **API**: `GET/POST/DELETE /system/keydb`, `GET /system/aacs-dumps`
und `GET /system/aacs-dumps/{dateiname}`. Der Inhalt kommt bewusst
als JSON-Body — es gibt kein `python-multipart`, ein Endpunkt mit
`UploadFile`/`File()` würde die API beim Import töten.
- [x] **UI (Einstellungen → System)**: Status der `KEYDB.cfg` (Pfad,
Größe, Anzahl Disc-Einträge, Datum), Inhalt einfügen, entfernen,
AACS-Dumps herunterladen — ohne SSH auf die VM.
- [x] **MakeMKV redet endlich**: `parse_msg()` in `ripping.py` + Log-Callback
in `tasks.py` schreiben MakeMKV-Meldungen ins Rippy-Log (gedrosselt:
Code 1003 raus, keine Wiederholungen, max. 40 je Rip). UHD-Fehlertext
ehrlich neu geschrieben; `caps.py` meldet `keydb: ja | nein | unbekannt`.
- [x] **Doku nachgezogen**: README (UHD-Voraussetzungen, System-Panel,
`MAKEMKV_DATA_HOST`, Bereitstellung), SAVEPOINT v3.10 inkl.
Richtigstellung von v3.3, KONZEPT §8 + §10.
**Offen aus dieser Runde:**
- [ ] **Der Nachweis mit einer echten `KEYDB.cfg` steht aus.** Zum
Zeitpunkt der Änderung lag keine Datei vor, die den Akira-Schlüssel
enthält — belegt sind der Befund und die Mechanik, NICHT ein
erfolgreicher UHD-Rip.
- [ ] Damit bleibt auch der volle Transcode-E2E an einer UHD weiter offen
(siehe SAVEPOINT v3.7) — an normalen BD/DVD ist die Kette bewiesen.
**Ausdrücklich nicht gebaut (und wird es auch nicht):** Rippy liefert
keine Disc-Schlüssel mit, lädt keine herunter und verteilt keine. Es
stellt nur den Platz für eine Datei bereit, die der Nutzer selbst
mitbringt, und zeigt ehrlich an, was dort liegt.
---
## Ideen-Katalog (Rest) — bewusst offen ## Ideen-Katalog (Rest) — bewusst offen
1. **Design 2.0** — an Gemini übergeben (24.07.2026). Vollständiges 1. **Design 2.0** — an Gemini übergeben (24.07.2026). Vollständiges
+77 -6
View File
@@ -1,6 +1,73 @@
# SAVEPOINT — Rippy # SAVEPOINT — Rippy
## Aktueller Stand: v3.9Windows-Installer als echte .exe (Icon, kein Konsolenfenster) (24.07.2026) ## Aktueller Stand: v3.104K-UHD: KEYDB.cfg statt Warten auf MakeMKV (25.07.2026)
- **Der Befund (am 25.07. live auf der VM im Worker-Container
nachgemessen — kein Verdacht, alles belegt):** Eine 4K-UHD (Akira,
MKB v76, Pressung Dezember 2020) scheitert mit „The volume key is
unknown for this disc". **Laufwerk und MakeMKV sind dabei in Ordnung:**
makemkvcon meldet „Using LibreDrive mode (v06.3)" und „Using direct
disc access mode", liest die Disc und legt den AACS-Dump ab
(Meldung 3332). Der Fehler liegt also NICHT an der Hardware.
- **Die echte Ursache — MakeMKV fragt gar nicht erst:** Das Debug-Log
geht ohne einen einzigen Netz-Versuch von „Loaded content hash table"
direkt auf „The volume key is unknown". **Beweise:**
`/root/.MakeMKV/_private_data.tar` enthielt nur die Index-Datei und
KEINE einzige `hkd_*.bin` — es wurde also nie ein Schlüssel geladen.
Auch mit erzwungener frischer Prüfung (`update.conf` gelöscht,
Meldung 5074 belegt den Web-Kontakt) und mit `app_UpdateEnable = "1"`
kam keiner. Und die im MakeMKV-Forum dokumentierten Schlüssel-Server
`hkdata.fairuse.org` und `hkdata.crabdance.com` lösen weltweit nicht
mehr auf (NXDOMAIN gegen Fritz!Box, 8.8.8.8 und 1.1.1.1).
- **Richtigstellung zu v3.3 (weiter unten korrigiert):** Dort stand, die
Ursache sei „Disc neuer als MakeMKVs Schlüssel-DB" und ein
MakeMKV-Update werde das lösen. Das ist widerlegt — es gibt keine
nachladbare Schlüssel-Datenbank mehr. **Warten auf ein MakeMKV-Update
hilft bei diesem Fehler nicht.**
- **Der einzige Weg, der heute funktioniert: `KEYDB.cfg`** — GROSS
geschrieben (unter Linux case-sensitiv) im MakeMKV-Datenverzeichnis.
Rippy macht diesen Weg jetzt begehbar, statt auf MakeMKV zu warten.
- **Gebaut — persistentes Datenverzeichnis:** `docker-compose.yml` mountet
`${MAKEMKV_DATA_HOST:-/srv/rippy/makemkv}` vom Host — im Worker auf
`/root/.MakeMKV`, in der API auf `/app/makemkv-data`; beide Container
bekommen `MAKEMKV_DATA_DIR`. Damit überleben `KEYDB.cfg` und die
AACS-Dumps jeden Rebuild. `entrypoint.sh` schreibt `settings.conf`
jetzt ergänzend statt zerstörend (sonst hätte der Beta-Key den Rest
überbügelt) und setzt `app_UpdateEnable = "1"`.
- **Gebaut — eine einzige Wahrheit über das Verzeichnis:** neues
Zwillings-Modul `makemkv_daten.py` (identisch in `docker/api/` und
`docker/worker/`) mit reinen Helfern — Status lesen, Inhalt prüfen,
atomar schreiben, löschen, AACS-Dumps auflisten. Nur reine Funktionen,
damit die Ampel sie ohne Postgres/Redis testen kann.
- **Gebaut — Bedienung im UI:** Einstellungen → System zeigt den
`KEYDB.cfg`-Status (Pfad, Größe, Anzahl Disc-Einträge, Datum), nimmt die
Datei über einen ganz normalen Datei-Dialog entgegen (der Browser liest
sie und schickt den Text als JSON — serverseitig bewusst KEIN
Multipart-Upload, es gibt kein `python-multipart`, das würde die API
beim Import töten), lehnt
unplausiblen Inhalt mit deutschem Klartext ab, kann die Datei wieder
entfernen und die AACS-Dumps zum Download anbieten.
- **Gebaut — man sieht endlich, was MakeMKV sagt:** `parse_msg()` in
`ripping.py` plus Log-Callback in `tasks.py` schreiben MakeMKV-Meldungen
ins Rippy-Log (gedrosselt: Code 1003 raus, keine Wiederholungen, max. 40
je Rip). Der UHD-Fehlertext ist ehrlich neu geschrieben. `caps.py`
meldet zusätzlich `keydb: ja | nein | unbekannt` je Worker.
- **Abgrenzung, die überall durchscheint:** **Rippy liefert KEINE
Schlüssel mit, lädt keine herunter und verteilt keine.** Rippy stellt
nur den Platz für eine Datei bereit, die der Nutzer selbst mitbringt,
und zeigt ehrlich an, was dort liegt. Genau die Grenze zieht schon
`docker/api/makemkv_key.py` (Zeile 14) für den Beta-Key: das ist die
Software-LIZENZ, nicht das Entschlüsseln oder Verteilen von
Disc-Schlüsseln.
- **⚠️ NOCH NICHT end-to-end bewiesen — ehrlich gesagt:** Zum Zeitpunkt
dieser Änderung lag KEINE `KEYDB.cfg` vor, die den Akira-Schlüssel
enthält. Belegt sind der Befund oben und die neue Mechanik (Mount,
Modul, Endpunkte, UI) — **nicht** ein erfolgreicher UHD-Rip. Der
Nachweis steht aus und braucht eine echte Schlüssel-Datei.
---
## Vorheriger Stand: v3.9 — Windows-Installer als echte .exe (Icon, kein Konsolenfenster) (24.07.2026)
- **RippyWorkerSetup.exe** ersetzt den .vbs/.bat-Weg (Commander-Einwand: - **RippyWorkerSetup.exe** ersetzt den .vbs/.bat-Weg (Commander-Einwand:
.vbs ist abgekündigt + wird als gefährlich geflaggt). install-gui.ps1 via .vbs ist abgekündigt + wird als gefährlich geflaggt). install-gui.ps1 via
@@ -183,16 +250,20 @@ Konvertierung (Infrastruktur steht), AI-Box-NFS-Export, Kodi-Refresh.
- **UHD-Kette diagnostiziert (Nachtrag, gleicher Tag): Der BU40N-Flash IST - **UHD-Kette diagnostiziert (Nachtrag, gleicher Tag): Der BU40N-Flash IST
erledigt** — makemkvcon meldet „Using LibreDrive mode (v06.3)". Der erledigt** — makemkvcon meldet „Using LibreDrive mode (v06.3)". Der
Summer-Wars-Fehlschlag lag NICHT am Laufwerk, sondern an „The volume key Summer-Wars-Fehlschlag lag NICHT am Laufwerk, sondern an „The volume key
is unknown": die Disc (MKB v82) ist neuer als MakeMKVs Schlüssel-DB — is unknown" — auf der VM mit 1.17.7 UND der aktuellsten 1.18.4
auf der VM mit 1.17.7 UND der aktuellsten 1.18.4 bewiesen. Konsequenzen: reproduziert. Konsequenzen:
MakeMKV auf **1.18.4** gehoben (Scan-Hänger durch überall genutztes MakeMKV auf **1.18.4** gehoben (Scan-Hänger durch überall genutztes
--noscan umgangen, auf der BU40N sauber gelaufen; URL-Base → /download), --noscan umgangen, auf der BU40N sauber gelaufen; URL-Base → /download),
run_makemkv reicht kritische Meldungen (volume key/Key abgelaufen) in run_makemkv reicht kritische Meldungen (volume key/Key abgelaufen) in
den Fehlertext durch, UHD-Klartext unterscheidet jetzt „Laufwerk kann den Fehlertext durch, UHD-Klartext unterscheidet jetzt „Laufwerk kann
kein UHD" von „Disc neuer als Key-DB" inkl. Forum-Dump-Hinweis kein UHD" von „Disc-Schlüssel fehlt" inkl. Forum-Dump-Hinweis
(MakeMKV speichert den AACS-Dump automatisch unter /root/.MakeMKV/). (MakeMKV speichert den AACS-Dump automatisch unter /root/.MakeMKV/).
Summer Wars bleibt bis zu einem MakeMKV-Key-Update nicht entschlüsselbar **⚠️ Korrigiert am 25.07.2026 (siehe v3.10 ganz oben):** Die hier
— das ist Stand der Technik, kein Rippy-Bug. ursprünglich genannte Erklärung „die Disc ist neuer als MakeMKVs
Schlüssel-DB, ein MakeMKV-Update löst das" ist FALSCH. Nachgemessen:
MakeMKV hat gar keine nachladbare Schlüssel-DB mehr und versucht auch
keinen Online-Abruf; die alten Schlüssel-Server sind tot. Der Weg zum
Ziel heißt `KEYDB.cfg`, nicht „warten auf die nächste Version".
- **Vollautomatik** (Einstellungen → Ripping): Disc erkannt → Rip startet - **Vollautomatik** (Einstellungen → Ripping): Disc erkannt → Rip startet
ohne Popup in den passenden Schnellwahl-Ordner (Serie/Film/Musik). ohne Popup in den passenden Schnellwahl-Ordner (Serie/Film/Musik).
- **Job-Verwaltung**: „Neu komprimieren" nur noch, wenn Rohdaten wirklich - **Job-Verwaltung**: „Neu komprimieren" nur noch, wenn Rohdaten wirklich
+17
View File
@@ -16,6 +16,10 @@ services:
- TMDB_API_KEY=${TMDB_API_KEY:-} - TMDB_API_KEY=${TMDB_API_KEY:-}
- THETVDB_API_KEY=${THETVDB_API_KEY:-} - THETVDB_API_KEY=${THETVDB_API_KEY:-}
- OMDB_API_KEY=${OMDB_API_KEY:-} - OMDB_API_KEY=${OMDB_API_KEY:-}
# Dasselbe Host-Verzeichnis wie beim Worker, nur an neutraler Stelle:
# die API liest den KEYDB-Status und die AACS-Dumps und nimmt eine
# hochgeladene KEYDB.cfg entgegen (Einstellungen → System).
- MAKEMKV_DATA_DIR=/app/makemkv-data
- LOG_LEVEL=INFO - LOG_LEVEL=INFO
healthcheck: healthcheck:
test: ["CMD-SHELL", "python -c 'import urllib.request; urllib.request.urlopen(\"http://localhost:8000/health\")'"] test: ["CMD-SHELL", "python -c 'import urllib.request; urllib.request.urlopen(\"http://localhost:8000/health\")'"]
@@ -47,6 +51,8 @@ services:
bind: bind:
propagation: rshared propagation: rshared
- temp:/app/temp - temp:/app/temp
# MakeMKV-Datenverzeichnis (dasselbe wie im Worker, siehe dort).
- ${MAKEMKV_DATA_HOST:-/srv/rippy/makemkv}:/app/makemkv-data
devices: devices:
- ${OPTICAL_SR:-/dev/sr0}:/dev/sr0 - ${OPTICAL_SR:-/dev/sr0}:/dev/sr0
networks: networks:
@@ -78,6 +84,10 @@ services:
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- RIP_OUTPUT_DIR=/app/media - RIP_OUTPUT_DIR=/app/media
- MAKEMKV_APP_KEY=${MAKEMKV_APP_KEY} - MAKEMKV_APP_KEY=${MAKEMKV_APP_KEY}
# MakeMKVs Datenverzeichnis im Container — fest verdrahtet in MakeMKV
# selbst (HOME/.MakeMKV, HOME ist /root). Steht hier, damit Worker-Code
# und Mount dieselbe Wahrheit benutzen.
- MAKEMKV_DATA_DIR=/root/.MakeMKV
# Anzeigename in Einstellungen → Worker (stabil über Rebuilds hinweg; # Anzeigename in Einstellungen → Worker (stabil über Rebuilds hinweg;
# via .env übersteuerbar, z. B. WORKER_NAME=wohnzimmer-vm) # via .env übersteuerbar, z. B. WORKER_NAME=wohnzimmer-vm)
- WORKER_NAME=${WORKER_NAME:-rippy-hauptworker} - WORKER_NAME=${WORKER_NAME:-rippy-hauptworker}
@@ -92,6 +102,13 @@ services:
bind: bind:
propagation: rslave propagation: rslave
- temp:/app/temp - temp:/app/temp
# MakeMKV-Datenverzeichnis PERSISTENT (Befund 25.07.2026, live gemessen):
# Hier liegen die KEYDB.cfg — heute die EINZIGE funktionierende
# Schlüsselquelle für 4K-UHD, weil MakeMKVs Online-Kanal nichts mehr
# liefert —, die AACS-Dumps fehlgeschlagener Discs und settings.conf.
# Ohne diesen Mount löschte JEDER `up -d --build` beides; die
# Fehlermeldung schickte den Nutzer zu einer Datei, die es nicht mehr gab.
- ${MAKEMKV_DATA_HOST:-/srv/rippy/makemkv}:/root/.MakeMKV
devices: devices:
# Host-Geräteknoten via .env (OPTICAL_SR/OPTICAL_SG) — je Rechner ANDERS! # Host-Geräteknoten via .env (OPTICAL_SR/OPTICAL_SG) — je Rechner ANDERS!
# MakeMKV spricht Laufwerke über die SCSI-Generic-Schicht an; ohne den # MakeMKV spricht Laufwerke über die SCSI-Generic-Schicht an; ohne den
+141
View File
@@ -12,6 +12,7 @@ import uuid
import db import db
import devices as device_discovery import devices as device_discovery
import makemkv_daten
import makemkv_key import makemkv_key
import mounts as mount_verwaltung import mounts as mount_verwaltung
import notify import notify
@@ -1190,6 +1191,146 @@ async def system_info():
return await asyncio.to_thread(sammle) 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): class NotificationTestRequest(BaseModel):
url: str url: str
+242
View File
@@ -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 ""
+8 -2
View File
@@ -11,8 +11,14 @@ Dieses Modul holt den aktuellen Key vom Forum und schreibt ihn in die Settings
Rebuild/Neustart, ab dem naechsten Rip. Rebuild/Neustart, ab dem naechsten Rip.
Wichtig: Das ist die kostenlose, oeffentliche Beta-LIZENZ der Software selbst Wichtig: Das ist die kostenlose, oeffentliche Beta-LIZENZ der Software selbst
NICHT das Entschluesseln oder Verteilen von Disc-Schluesseln. Den AACS-Schluessel NICHT das Entschluesseln oder Verteilen von Disc-Schluesseln. Diese Grenze gilt
zieht MakeMKV via LibreDrive ohnehin selbst aus dem Laufwerk. 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): Quelle/Format dokumentiert (AGENTS Regel D nicht geraten):
- Forum-Thread: https://forum.makemkv.com/forum/viewtopic.php?t=1053 - Forum-Thread: https://forum.makemkv.com/forum/viewtopic.php?t=1053
+7 -1
View File
@@ -18,7 +18,13 @@ def test_main_importierbar_und_routen_verdrahtet():
from main import app from main import app
routen = {route.path for route in app.routes} 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" assert pfad in routen, f"Route {pfad} fehlt"
+118
View File
@@ -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("", "") == ""
+6
View File
@@ -17,6 +17,12 @@ server {
# SSE (/api/stream/jobs) braucht ungepufferte, lange Verbindungen: # SSE (/api/stream/jobs) braucht ungepufferte, lange Verbindungen:
proxy_buffering off; proxy_buffering off;
proxy_read_timeout 3600s; proxy_read_timeout 3600s;
# Der nginx-Default ist 1 MB. Eine echte KEYDB.cfg ist deutlich groesser,
# der Upload ueber POST /api/system/keydb wuerde also schon hier mit
# 413 abgewiesen die API bekaeme die Anfrage nie zu sehen und das UI
# haette keinen detail-Text, den es anzeigen koennte. 64m entspricht dem
# Limit MAX_KEYDB_BYTES in makemkv_daten.py.
client_max_body_size 64m;
} }
location / { location / {
+28 -5
View File
@@ -134,12 +134,15 @@ export default function AnleitungPage() {
Einstellungen System eintragen gilt ab dem nächsten Rip, ohne Neustart. DVDs gehen Einstellungen System eintragen gilt ab dem nächsten Rip, ohne Neustart. DVDs gehen
immer auch ohne Key. Dort stehen auch die Werkzeug-Versionen und der freie Speicherplatz. immer auch ohne Key. Dort stehen auch die Werkzeug-Versionen und der freie Speicherplatz.
</p> </p>
{/* 25.07.2026 richtiggestellt: Updates bringen keine Disc-Schluessel mit — siehe UHD-Absatz unten. */}
<p> <p>
<span className={fett}>Updates:</span> Auf Updates prüfen" (ebenfalls Einstellungen <span className={fett}>Updates:</span> Auf Updates prüfen" (ebenfalls Einstellungen
System) vergleicht die installierten Versionen mit makemkv.com und den offiziellen System) vergleicht die installierten Versionen mit makemkv.com und den offiziellen
HandBrake-Releases. Gibt es ein MakeMKV-Update, zeigt Rippy den fertigen HandBrake-Releases. Gibt es ein MakeMKV-Update, zeigt Rippy den fertigen
Update-Befehl an wichtig, weil neue Versionen auch die neueste Update-Befehl an neue Versionen bringen bessere Laufwerks-Unterstützung und
Disc-Schlüssel-Datenbank mitbringen. Fehlerbehebungen. <span className={fett}>Disc-Schlüssel für 4K-UHD kommen dagegen nicht
aus einem Update</span>, sondern nur aus der Datei <code>KEYDB.cfg</code>, die du selbst
unter Einstellungen System hochlädst (mehr dazu unter Häufige Fragen").
</p> </p>
</Abschnitt> </Abschnitt>
@@ -157,11 +160,31 @@ export default function AnleitungPage() {
<span className={fett}>Diese Disc wurde bereits gerippt"?</span> Rippy erkennt Discs am <span className={fett}>Diese Disc wurde bereits gerippt"?</span> Rippy erkennt Discs am
Fingerabdruck. Nochmal rippen geht trotzdem der Hinweis verhindert nur Versehen. Fingerabdruck. Nochmal rippen geht trotzdem der Hinweis verhindert nur Versehen.
</p> </p>
{/*
25.07.2026 auf der Rippy-VM nachgemessen und komplett neu geschrieben:
Hier stand vorher, ein MakeMKV-Update mache die Disc rippbar. Das stimmt
nicht MakeMKV versucht bei einer unbekannten UHD-Pressung gar nicht mehr,
online einen Schluessel zu holen, und die dokumentierten Schluessel-Server
(hkdata.fairuse.org, hkdata.crabdance.com) loesen weltweit nicht mehr auf.
*/}
<p> <p>
<span className={fett}>4K-UHD schlägt fehl mit volume key is unknown"?</span> Das Laufwerk <span className={fett}>4K-UHD schlägt fehl mit volume key is unknown"?</span> Das Laufwerk
liest die Disc (LibreDrive), aber MakeMKV kennt den Schlüssel dieser (zu neuen) Pressung liest die Disc einwandfrei (LibreDrive) MakeMKV fehlt nur der Schlüssel dieser Pressung.
noch nicht. Den automatisch gespeicherten AACS-Dump im MakeMKV-Forum einreichen mit einem Früher holte MakeMKV solche Schlüssel selbst aus dem Netz; dieser Kanal liefert heute nichts
der nächsten Updates ist die Disc rippbar. mehr, und ein MakeMKV-Update ändert daran nichts.
</p>
<p>
Der einzige Weg, der heute funktioniert, ist eine Datei namens <code>KEYDB.cfg</code>
eine Textliste mit Disc-Schlüsseln, die du selbst mitbringst. Unter Einstellungen System
hochladen, sie wirkt ab dem nächsten Rip. <span className={fett}>Rippy liefert keine
Schlüssel mit und lädt auch keine herunter</span> Rippy stellt nur den Platz für deine
Datei bereit und zeigt dir an, was dort liegt.
</p>
<p>
Der <span className={fett}>AACS-Dump</span> zu einer gescheiterten Disc bleibt jetzt
erhalten und steht unter Einstellungen System zum Herunterladen. Ihn kannst du im
MakeMKV-Forum im Bereich Ultra HD Blu-ray" einreichen daraus lässt sich der Schlüssel
für deine Pressung ermitteln, den du dann in deine <code>KEYDB.cfg</code> einträgst.
</p> </p>
<p> <p>
<span className={fett}>Neu komprimieren" fehlt bei einem Fehl-Job?</span> Der Knopf <span className={fett}>Neu komprimieren" fehlt bei einem Fehl-Job?</span> Der Knopf
+262 -6
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useRef } from 'react'
import { Save, Disc, Cpu, Database, Globe, AlertCircle, CheckCircle, HardDrive, Wrench, Send, Tv } from 'lucide-react' import { Save, Disc, Cpu, Database, Globe, AlertCircle, CheckCircle, HardDrive, Wrench, Send, Tv, KeyRound, Upload, Trash2, Download } from 'lucide-react'
import { api } from '../lib/api' import { api } from '../lib/api'
import { useToast } from '../context/ToastContext' import { useToast } from '../context/ToastContext'
import StorageMounts from '../components/StorageMounts' import StorageMounts from '../components/StorageMounts'
@@ -62,7 +62,26 @@ type SettingsTab = 'ripping' | 'verarbeitung' | 'worker' | 'speicherziele' | 'ap
interface WorkerInfo { interface WorkerInfo {
name: string name: string
encoders: string[] encoders: string[]
info?: { makemkv?: string, handbrake?: string, makemkv_key?: string } // keydb: "ja" | "nein" | "unbekannt" — sagt, ob DIESER Worker eine KEYDB.cfg
// in seinem MakeMKV-Datenverzeichnis sieht. Nur der Worker, der wirklich
// rippt, zaehlt, deshalb steht die Angabe pro Worker und nicht global.
info?: { makemkv?: string, handbrake?: string, makemkv_key?: string, keydb?: string }
}
// Antwort von GET/POST/DELETE /system/keydb (Feldnamen exakt wie die API sie liefert).
interface KeydbStatus {
vorhanden: boolean
pfad: string
groesse_bytes: number
eintraege: number
geaendert: string
}
// Ein AACS-Dump aus GET /system/aacs-dumps.
interface AacsDump {
name: string
groesse_bytes: number
geaendert: string
} }
interface SystemInfo { interface SystemInfo {
@@ -73,6 +92,24 @@ interface SystemInfo {
webhook_gesetzt: boolean webhook_gesetzt: boolean
} }
// Byte-Zahl menschenlesbar — eine echte KEYDB.cfg ist mehrere MB gross.
function bytesLesbar(bytes: number): string {
if (!bytes) return '0 B'
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`
return `${bytes} B`
}
// Die API liefert ISO-8601 in UTC (oder einen leeren String). Ohne Zonen-Kennung
// wuerde der Browser den Zeitstempel als Ortszeit lesen und die Uhrzeit um den
// Zonen-Versatz verschieben — deshalb notfalls ein Z anhaengen.
function zeitLesbar(iso: string): string {
if (!iso) return 'unbekannt'
const mitZone = /[Zz]$|[+-]\d{2}:?\d{2}$/.test(iso) ? iso : `${iso}Z`
const d = new Date(mitZone)
return isNaN(d.getTime()) ? 'unbekannt' : d.toLocaleString('de-DE')
}
export default function SettingsPage() { export default function SettingsPage() {
const [settings, setSettings] = useState<SettingsState>(defaultSettings) const [settings, setSettings] = useState<SettingsState>(defaultSettings)
const [activeTab, setActiveTab] = useState<SettingsTab>('ripping') const [activeTab, setActiveTab] = useState<SettingsTab>('ripping')
@@ -86,6 +123,13 @@ export default function SettingsPage() {
const [quellenBusy, setQuellenBusy] = useState(false) const [quellenBusy, setQuellenBusy] = useState(false)
const [updates, setUpdates] = useState<Record<string, { installiert?: string, verfuegbar?: string, update?: boolean }> | null>(null) const [updates, setUpdates] = useState<Record<string, { installiert?: string, verfuegbar?: string, update?: boolean }> | null>(null)
const [updatesBusy, setUpdatesBusy] = useState(false) const [updatesBusy, setUpdatesBusy] = useState(false)
// KEYDB.cfg und AACS-Dumps sind DATEIEN im MakeMKV-Datenverzeichnis, keine
// Einstellungen — deshalb bewusst NICHT in SettingsState: der globale
// Speichern-Knopf postet dieses Objekt komplett und wuerde sie mitschleifen.
const [keydb, setKeydb] = useState<KeydbStatus | null>(null)
const [keydbBusy, setKeydbBusy] = useState(false)
const [dumps, setDumps] = useState<AacsDump[]>([])
const keydbInput = useRef<HTMLInputElement>(null)
const { toast } = useToast() const { toast } = useToast()
@@ -106,6 +150,49 @@ export default function SettingsPage() {
} }
} }
// Status der KEYDB.cfg + Liste der AACS-Dumps frisch holen. Wird beim Laden
// der Seite und nach jeder Aktion aufgerufen — die API ist die Wahrheit,
// nicht das, was wir gerade hochgeschickt haben.
const keydbLaden = () => {
api.get('/system/keydb').then(r => setKeydb(r.data)).catch(() => setKeydb(null))
api.get('/system/aacs-dumps').then(r => setDumps(r.data?.dumps || [])).catch(() => setDumps([]))
// Die Worker-Plaketten oben kommen aus /capabilities und stammen aus dem
// letzten Herzschlag des Workers (minuetlich) — ohne dieses Nachladen
// widerspraeche die Kachel nach einem Upload sichtbar der Erfolgsmeldung.
// Ganz sofort ist sie trotzdem nicht: der Worker meldet erst beim
// naechsten Herzschlag neu. Genau so steht es auch im Erklaertext.
api.get('/capabilities').then(r => setWorkers(r.data.workers || [])).catch(() => setWorkers([]))
}
const keydbHochladen = async (datei: File) => {
setKeydbBusy(true)
try {
// Bewusst als JSON-Text und nicht als Multipart-Upload: der API fehlt
// python-multipart, ein File()-Endpunkt wuerde sie beim Import killen.
const inhalt = await datei.text()
await api.post('/system/keydb', { inhalt })
keydbLaden()
toast('success', 'KEYDB.cfg gespeichert — sie wirkt ab dem nächsten Rip.')
} catch (e: any) {
toast('error', e?.response?.data?.detail || 'Hochladen fehlgeschlagen — ist das wirklich eine KEYDB.cfg, und läuft die API?')
} finally {
setKeydbBusy(false)
}
}
const keydbEntfernen = async () => {
setKeydbBusy(true)
try {
await api.delete('/system/keydb')
keydbLaden()
toast('success', 'KEYDB.cfg entfernt — Rippy nutzt jetzt keine Schlüsseldatei mehr.')
} catch (e: any) {
toast('error', e?.response?.data?.detail || 'Entfernen fehlgeschlagen — läuft die API?')
} finally {
setKeydbBusy(false)
}
}
const quellenPruefen = async () => { const quellenPruefen = async () => {
setQuellenBusy(true) setQuellenBusy(true)
try { try {
@@ -131,6 +218,12 @@ export default function SettingsPage() {
api.get('/system/info') api.get('/system/info')
.then(r => setSystemInfo(r.data)) .then(r => setSystemInfo(r.data))
.catch(() => setSystemInfo(null)) .catch(() => setSystemInfo(null))
api.get('/system/keydb')
.then(r => setKeydb(r.data))
.catch(() => setKeydb(null))
api.get('/system/aacs-dumps')
.then(r => setDumps(r.data?.dumps || []))
.catch(() => setDumps([]))
}, []) }, [])
const webhookTesten = async () => { const webhookTesten = async () => {
@@ -633,13 +726,40 @@ export default function SettingsPage() {
HandBrake {w.info.handbrake} HandBrake {w.info.handbrake}
</span> </span>
)} )}
{/* Nur das Verzeichnis des rippenden Workers zaehlt — deshalb je Worker. */}
{w.info?.keydb === 'ja' && (
<span className="text-xs px-2.5 py-0.5 rounded-md font-medium bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30">
KEYDB.cfg vorhanden
</span>
)}
{w.info?.keydb === 'nein' && (
<span className="text-xs px-2.5 py-0.5 rounded-md font-medium bg-amber-500/15 text-amber-600 dark:text-amber-400 border border-amber-500/30">
keine KEYDB.cfg
</span>
)}
{w.info?.keydb === 'unbekannt' && (
<span className="text-xs px-2.5 py-0.5 rounded-md font-medium bg-slate-200 dark:bg-slate-800 text-slate-500 dark:text-slate-400 border border-slate-300 dark:border-slate-700">
KEYDB.cfg unbekannt
</span>
)}
</div> </div>
)) ))
)} )}
{/*
Bis 25.07.2026 stand hier, MakeMKV-Updates braechten die neueste
Disc-Schluessel-Datenbank mit. Auf der Rippy-VM nachgemessen: falsch.
MakeMKV hat gar keine mitgelieferte Schluessel-Datenbank, und der
Online-Kanal liefert nichts mehr (die Server hkdata.fairuse.org und
hkdata.crabdance.com loesen weltweit nicht mehr auf). Updates bringen
Laufwerks-Firmware-Unterstuetzung und Fehlerbehebungen Schluessel
fuer neue UHD-Pressungen kommen ausschliesslich aus der KEYDB.cfg.
*/}
<p className="text-xs mt-3 pt-3 border-t border-slate-200/60 dark:border-slate-800 text-slate-500 dark:text-slate-400"> <p className="text-xs mt-3 pt-3 border-t border-slate-200/60 dark:border-slate-800 text-slate-500 dark:text-slate-400">
<strong className="text-slate-700 dark:text-slate-300">MakeMKV</strong> lässt sich aktuell halten <strong className="text-slate-700 dark:text-slate-300">MakeMKV</strong> lässt sich aktuell halten
(Update-Check unten; Version über <code>MAKEMKV_VERSION</code> + Rebuild) wichtig wegen der (Update-Check unten; Version über <code>MAKEMKV_VERSION</code> + Rebuild) neue Versionen bringen
Disc-Schlüssel-Datenbank. vor allem Laufwerks-Unterstützung und Fehlerbehebungen. <strong>Disc-Schlüssel für neue
4K-UHD-Pressungen kommen dagegen NICHT aus einem Update</strong>, sondern nur aus der KEYDB.cfg
im Block darunter.
{' '}<strong className="text-slate-700 dark:text-slate-300">HandBrake</strong> im eingebauten {' '}<strong className="text-slate-700 dark:text-slate-300">HandBrake</strong> im eingebauten
Docker-Worker ist bewusst die stabile <strong>Debian-Version</strong> für die Kompression völlig Docker-Worker ist bewusst die stabile <strong>Debian-Version</strong> für die Kompression völlig
ausreichend und wird <strong>nicht separat aktualisiert</strong>. Native Worker (Windows) holen ausreichend und wird <strong>nicht separat aktualisiert</strong>. Native Worker (Windows) holen
@@ -655,6 +775,140 @@ export default function SettingsPage() {
placeholder="T-… (Forum-Thread „MakeMKV is free while in beta”)" placeholder="T-… (Forum-Thread „MakeMKV is free while in beta”)"
/> />
{/*
MakeMKV-Schluessel (KEYDB.cfg) Befund vom 25.07.2026 auf der Rippy-VM:
MakeMKV versucht bei einer unbekannten UHD-Pressung gar nicht mehr, online
einen Schluessel zu holen, und die dokumentierten Schluessel-Server sind tot.
Der einzige heute funktionierende Weg ist eine Datei KEYDB.cfg (GROSS
geschrieben, unter Linux case-sensitiv) im MakeMKV-Datenverzeichnis.
Rippy stellt nur den Platz dafuer bereit mitgeliefert wird nichts.
*/}
<div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-slate-900 dark:text-slate-200 flex items-center gap-2">
<KeyRound size={16} className="text-amber-500" />
MakeMKV-Schlüssel (KEYDB.cfg)
</p>
<div className="flex items-center gap-2">
{/*
Versteckter Datei-Dialog: der Inhalt wird im Browser gelesen und als
JSON gepostet. Kein Multipart der API fehlt python-multipart.
*/}
<input
type="file"
className="hidden"
ref={keydbInput}
accept=".cfg,text/plain"
onChange={(e) => {
const datei = e.target.files?.[0]
// Wert leeren, damit dieselbe Datei erneut gewählt werden kann.
e.target.value = ''
if (datei) keydbHochladen(datei)
}}
/>
<Button
variant="amber"
size="sm"
onClick={() => keydbInput.current?.click()}
disabled={keydbBusy}
>
<Upload size={14} />
{keydbBusy ? 'Arbeite…' : 'KEYDB.cfg hochladen'}
</Button>
{keydb?.vorhanden && (
<Button variant="danger" size="sm" onClick={keydbEntfernen} disabled={keydbBusy}>
<Trash2 size={14} />
Entfernen
</Button>
)}
</div>
</div>
{keydb?.vorhanden ? (
<div className="p-3 rounded-lg text-sm bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30">
<p className="font-semibold">Eine KEYDB.cfg liegt bereit.</p>
<p className="text-xs mt-1">
{bytesLesbar(keydb.groesse_bytes)} · {keydb.eintraege} Zeilen mit Disc-Kennung ·
zuletzt geändert {zeitLesbar(keydb.geaendert)}
</p>
<p className="text-xs mt-1 font-mono opacity-80">{keydb.pfad}</p>
</div>
) : (
<div className="p-3 rounded-lg text-sm bg-amber-500/15 text-amber-600 dark:text-amber-400 border border-amber-500/30">
<p className="font-semibold">Es liegt keine KEYDB.cfg bereit.</p>
<p className="text-xs mt-1">
DVDs und normale Blu-rays laufen trotzdem. Nur bei 4K-UHD-Discs, deren Schlüssel MakeMKV
nicht kennt, bricht der Rip mit The volume key is unknown for this disc" ab.
</p>
</div>
)}
<p className="text-xs text-slate-500 dark:text-slate-400">
Die <strong className="text-slate-700 dark:text-slate-300">KEYDB.cfg</strong> ist eine reine
Textdatei mit Disc-Schlüsseln für 4K-UHD-Blu-rays. MakeMKV holte solche Schlüssel früher selbst
aus dem Netz das funktioniert heute nicht mehr, die alten Schlüssel-Server sind abgeschaltet.
Ein MakeMKV-Update hilft dagegen nicht.
</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
<strong className="text-slate-700 dark:text-slate-300">Wichtig:</strong> Rippy liefert keine
Schlüssel mit, lädt keine herunter und verteilt keine. Rippy stellt nur den Platz für eine Datei
bereit, die du selbst mitbringst, und zeigt dir ehrlich an, was dort liegt. Die Zahl oben ist
genau das: die gezählten Zeilen mit einer Disc-Kennung in deiner Datei keine von MakeMKV
bestätigte Anzahl brauchbarer Schlüssel.
</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
Eine hochgeladene Datei ersetzt die bisherige und wirkt
<strong className="text-slate-700 dark:text-slate-300"> ab dem nächsten Rip</strong> laufende
Jobs bleiben unberührt, ein Neustart ist nicht nötig. Die Plakette am Worker weiter oben
folgt erst mit dessen nächstem Herzschlag (etwa eine Minute) dieser Kasten hier ist sofort
aktuell.
</p>
</div>
{/*
AACS-Dumps: MakeMKV legt sie bei einer unbekannten UHD-Disc ab (Meldung 3332).
Seit das Datenverzeichnis persistent gemountet ist, ueberleben sie den
Container-Neustart vorher waren sie nach jedem Rip weg.
*/}
<div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-slate-900 dark:text-slate-200">AACS-Dumps</p>
<Button variant="secondary" size="sm" onClick={keydbLaden}>
Liste aktualisieren
</Button>
</div>
{dumps.length === 0 ? (
<p className="text-xs text-slate-500 dark:text-slate-400">
Noch kein Dump vorhanden. MakeMKV legt hier automatisch einen ab, sobald eine 4K-UHD-Disc
an einem unbekannten Schlüssel scheitert.
</p>
) : (
<div className="space-y-1.5">
{dumps.map(d => (
<a
key={d.name}
href={`/api/system/aacs-dumps/${encodeURIComponent(d.name)}`}
download
className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors text-amber-600 dark:text-amber-300 hover:bg-slate-100 dark:hover:bg-slate-800"
>
<Download size={14} className="flex-shrink-0" />
<span className="truncate min-w-0 font-mono">{d.name}</span>
<span className="ml-auto text-xs flex-shrink-0 text-slate-500 dark:text-slate-400 font-mono">
{bytesLesbar(d.groesse_bytes)} · {zeitLesbar(d.geaendert)}
</span>
</a>
))}
</div>
)}
<p className="text-xs text-slate-500 dark:text-slate-400">
Ein AACS-Dump ist das, was das Laufwerk von der Disc gelesen hat, bevor der Schlüssel fehlte.
Er enthält keinen Schlüssel und nützt dir allein nichts aber du kannst ihn herunterladen und
im MakeMKV-Forum im Bereich <strong className="text-slate-700 dark:text-slate-300">Ultra HD
Blu-ray</strong> einreichen. Dort können Leute daraus den Schlüssel für deine Pressung ermitteln;
den trägst du dann in deine KEYDB.cfg ein.
</p>
</div>
<div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3"> <div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm font-semibold text-slate-900 dark:text-slate-200">Update-Check</p> <p className="text-sm font-semibold text-slate-900 dark:text-slate-200">Update-Check</p>
@@ -672,7 +926,9 @@ export default function SettingsPage() {
<p className="text-xs mt-1"> <p className="text-xs mt-1">
Update verfügbar in der .env <code>MAKEMKV_VERSION={updates.makemkv.verfuegbar}</code> setzen, Update verfügbar in der .env <code>MAKEMKV_VERSION={updates.makemkv.verfuegbar}</code> setzen,
dann auf der Rippy-Maschine <code>docker compose build worker &amp;&amp; docker compose up -d worker</code>. dann auf der Rippy-Maschine <code>docker compose build worker &amp;&amp; docker compose up -d worker</code>.
Neue Versionen bringen auch die neueste Disc-Schlüssel-Datenbank mit. {/* 25.07.2026 richtiggestellt: ein Update bringt KEINE Schluessel — die kommen nur aus der KEYDB.cfg. */}
Neue Versionen bringen Laufwerks-Unterstützung und Fehlerbehebungen aber
<strong> keine Disc-Schlüssel</strong>: die kommen ausschließlich aus deiner KEYDB.cfg.
</p> </p>
) )
: <span className="text-xs ml-1 text-emerald-600 dark:text-emerald-400"> aktuell</span>} : <span className="text-xs ml-1 text-emerald-600 dark:text-emerald-400"> aktuell</span>}
+7 -3
View File
@@ -16,9 +16,13 @@ FROM python:3.12-slim-bookworm AS makemkv-build
# 1.18er-Laufwerks-Scan-Hängers (Forum t=38128) und der Flash-Fähigkeit # 1.18er-Laufwerks-Scan-Hängers (Forum t=38128) und der Flash-Fähigkeit
# gepinnt. Beides erledigt: der Scan wird überall mit --noscan umgangen # gepinnt. Beides erledigt: der Scan wird überall mit --noscan umgangen
# (info-Lauf mit 1.18.4 auf der BU40N sauber durchgelaufen, 24.07.), und # (info-Lauf mit 1.18.4 auf der BU40N sauber durchgelaufen, 24.07.), und
# das Laufwerk ist geflasht (LibreDrive v06.3 bestätigt). Die AKTUELLE # das Laufwerk ist geflasht (LibreDrive v06.3 bestätigt).
# Version zählt, weil sie die neueste AACS-Schlüssel-Datenbank mitbringt — # Richtigstellung 25.07.2026: Hier stand, die aktuelle Version zähle, weil sie
# 1.17.7 kannte z. B. den Key der Summer-Wars-UHD (MKB v82) nicht. # "die neueste AACS-Schlüssel-Datenbank mitbringt". Das ist widerlegt — MakeMKV
# bringt gar keine Disc-Schlüssel mit, und der Online-Kanal liefert nichts mehr
# (Messungen im Modul-Kopf von makemkv_daten.py). Aktuell bleiben lohnt sich
# trotzdem: Laufwerks-Unterstützung und Fehlerbehebungen. Schlüssel für neue
# UHD-Pressungen kommen ausschließlich aus der KEYDB.cfg im Datenverzeichnis.
# MAKEMKV_URL_BASE ist übersteuerbar (Build-Arg): /download hat nur die # MAKEMKV_URL_BASE ist übersteuerbar (Build-Arg): /download hat nur die
# aktuelle Version, /download/old die älteren. Wenn Cloudflare BuildKit- # aktuelle Version, /download/old die älteren. Wenn Cloudflare BuildKit-
# Downloads drosselt (24.07.: außerhalb des Builds ging alles, im Build # Downloads drosselt (24.07.: außerhalb des Builds ging alles, im Build
+20
View File
@@ -85,4 +85,24 @@ def werkzeug_versionen() -> dict:
info["makemkv_key"] = "env" info["makemkv_key"] = "env"
else: else:
info["makemkv_key"] = "keiner" info["makemkv_key"] = "keiner"
# Sieht DIESER Worker eine KEYDB.cfg? Die API zeigt ihren eigenen Mount —
# bei einem Remote-Worker kann das etwas ganz anderes sein, und nur das
# Verzeichnis des rippenden Workers zählt (Befund 25.07.2026).
#
# Gemeldet wird die Angabe NUR, wenn das Datenverzeichnis hier wirklich
# eingehängt ist (os.path.ismount). Ein Remote-Transcode-Worker benutzt
# dasselbe Image — makemkvcon ist dort also vorhanden und der entrypoint
# legt den Ordner an —, er bekommt den Mount aber nicht und rippt nie.
# Ohne diese Prüfung trüge er im UI dauerhaft die Warnung "keine
# KEYDB.cfg", obwohl ihn das gar nichts angeht.
try:
import makemkv_daten
daten_dir = makemkv_daten.DATEN_DIR
except Exception:
daten_dir = ""
if shutil.which("makemkvcon") and daten_dir and os.path.ismount(daten_dir):
try:
info["keydb"] = "ja" if makemkv_daten.keydb_status().get("vorhanden") else "nein"
except Exception:
info["keydb"] = "unbekannt"
return info return info
+44 -4
View File
@@ -1,11 +1,51 @@
#!/bin/sh #!/bin/sh
# Schreibt den MakeMKV-Beta-Key aus der Umgebung in die Settings (falls gesetzt). # Bereitet MakeMKVs Datenverzeichnis vor, BEVOR der Worker startet.
# Ohne Key: DVD-Ripping geht immer, Blu-ray läuft im 30-Tage-Testmodus. #
# Wichtig (Befund 25.07.2026): Dieses Verzeichnis ist jetzt ein persistenter
# Mount vom Host (docker-compose.yml). Darin liegen KEYDB.cfg (die einzige
# heute funktionierende Schlüsselquelle für 4K-UHD), die AACS-Dumps
# fehlgeschlagener Discs und MakeMKVs settings.conf. Deshalb wird die
# settings.conf hier ERGÄNZT statt überschrieben — vorher hat dieses Skript
# sie bei jedem Start plattgemacht und dabei alles außer app_Key gelöscht.
#
# Ohne Beta-Key: DVD-Ripping geht immer, Blu-ray läuft im 30-Tage-Testmodus.
set -e set -e
DATEN_DIR="${MAKEMKV_DATA_DIR:-/root/.MakeMKV}"
CONF="${DATEN_DIR}/settings.conf"
# Die ganze Vorbereitung ist BEST EFFORT und läuft deshalb in einer Subshell
# ohne set -e. Grund: Das Datenverzeichnis kommt jetzt vom Host und kann
# schreibgeschützt sein oder einer fremden UID gehören (Freigabe, root_squash).
# Vorher war das unmöglich — geschrieben wurde containerintern. Bräche der
# Start daran ab, ergäbe "restart: unless-stopped" eine Endlosschleife, in der
# auch reines DVD-Rippen tot wäre, obwohl das die Datei gar nicht braucht.
# Denselben Weg geht tasks.py: OSError wird zur Warnung, der Job läuft weiter.
if ! (
set +e
mkdir -p "${DATEN_DIR}" || exit 1
[ -f "${CONF}" ] || touch "${CONF}" || exit 1
# app_Key aus der Umgebung setzen: alte Zeile raus, neue ans Ende. Alles
# andere in der Datei bleibt stehen. (grep -v liefert Exit 1, wenn nichts
# übrig bleibt — das ist hier kein Fehler.)
if [ -n "${MAKEMKV_APP_KEY}" ]; then if [ -n "${MAKEMKV_APP_KEY}" ]; then
mkdir -p /root/.MakeMKV grep -v '^app_Key' "${CONF}" > "${CONF}.neu" 2>/dev/null
printf 'app_Key = "%s"\n' "${MAKEMKV_APP_KEY}" > /root/.MakeMKV/settings.conf printf 'app_Key = "%s"\n' "${MAKEMKV_APP_KEY}" >> "${CONF}.neu" || exit 1
mv "${CONF}.neu" "${CONF}" || exit 1
fi
# Web-Kontakt explizit einschalten. MakeMKV hat das per Default ohnehin an
# (Meldung 5074, am 25.07. im Worker verifiziert) — explizit steht es hier,
# damit die Einstellung nachvollziehbar ist und nicht versehentlich kippt.
# Quelle: forum.makemkv.com/forum/viewtopic.php?t=20364 (headless settings.conf)
grep -q '^app_UpdateEnable' "${CONF}" || printf 'app_UpdateEnable = "1"\n' >> "${CONF}" || exit 1
exit 0
); then
echo "WARNUNG: ${DATEN_DIR} ist nicht beschreibbar." >&2
echo " MakeMKV-Beta-Key und app_UpdateEnable wurden NICHT gesetzt." >&2
echo " Der Worker startet trotzdem. Prüfe Rechte und Eigentümer des" >&2
echo " Host-Verzeichnisses (Standard: /srv/rippy/makemkv)." >&2
fi fi
exec "$@" exec "$@"
+242
View File
@@ -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 ""
+53 -13
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, 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 """Rippt GENAU die gewählten Titel (makemkvcon kann pro Aufruf nur einen
Titel oder 'all' also ein Aufruf je Titel, Fortschritt anteilig).""" Titel oder 'all' also ein Aufruf je Titel, Fortschritt anteilig)."""
gesamt = len(titel_liste) gesamt = len(titel_liste)
@@ -124,7 +124,8 @@ def rip_titel_auswahl(device_path: str, output_dir: str, titel_liste: list,
if progress_cb: if progress_cb:
progress_cb(int((_i * 100 + p) / gesamt)) 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": if ergebnis.get("status") == "cancelled":
return ergebnis return ergebnis
if ergebnis.get("status") != "success": 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 "")) 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: def get_progress_from_prgv(line: str) -> int:
"""Extrahiert Gesamt-Fortschritt (0-100) aus einer PRGV-Zeile. """Extrahiert Gesamt-Fortschritt (0-100) aus einer PRGV-Zeile.
@@ -297,8 +319,17 @@ def write_abcde_config(output_dir: str) -> str:
return tmp.name return tmp.name
def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str = "all") -> dict: def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str = "all",
"""Rippt eine DVD/Blu-ray verlustfrei mit makemkvcon; meldet Fortschritt.""" 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(): if not check_makemkv_installed():
return {"status": "error", "error": "makemkvcon ist nicht installiert"} 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: try:
for line in process.stdout: for line in process.stdout:
progress = get_progress_from_prgv(line) progress = get_progress_from_prgv(line)
if progress >= 0 and progress_cb: if progress >= 0:
if progress_cb:
progress_cb(progress) progress_cb(progress)
elif line.startswith("MSG:"): continue
# MSG:code,flags,count,"message",... — Klartext ist Feld 4 meldung = parse_msg(line)
teile = line.split(",", 4) if meldung is None:
if len(teile) >= 4: continue
letzte_meldung = teile[3].strip('"') code, letzte_meldung = meldung
if any(muster in letzte_meldung for muster in KRITISCH): if any(muster in letzte_meldung for muster in KRITISCH):
kritische_meldungen.append(letzte_meldung) 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: except RipAbbruch:
process.kill() process.kill()
process.wait() 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, def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_cb=None,
output_dir: str = None, nur_hauptfilm: bool = False, 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. """Rippt eine DVD oder Blu-ray verlustfrei mit MakeMKV.
Bewusst KEIN eigener Celery-Task: der einzige Task ist worker.tasks.rip_disc, 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: if titel_liste:
os.makedirs(output_dir, exist_ok=True) 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" titel = "all"
if nur_hauptfilm: 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: if haupt is not None:
titel = str(haupt) titel = str(haupt)
# Kein Titel ermittelbar → ehrlich auf 'all' zurückfallen statt raten # 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: def rip_cd(device_path: str, disc_id: str, progress_cb=None, output_dir: str = None) -> dict:
+64 -18
View File
@@ -20,6 +20,7 @@ import shutil
import requests import requests
import db import db
import makemkv_daten
import medien import medien
import notify import notify
from celery_app import celery_app from celery_app import celery_app
@@ -147,15 +148,26 @@ def _makemkv_key_anwenden(einstellungen: dict) -> None:
Damit ist der Monats-Key ohne Rebuild/Neustart aktualisierbar Damit ist der Monats-Key ohne Rebuild/Neustart aktualisierbar
(Einstellungen System). Format wie entrypoint.sh: settings.conf. (Einstellungen System). Format wie entrypoint.sh: settings.conf.
Ergänzend statt überschreibend (Befund 25.07.2026): das Datenverzeichnis
ist jetzt persistent, und hier stand vorher ein open(..., "w") das warf
vor JEDEM Rip alles andere aus der settings.conf, z. B. app_UpdateEnable
aus dem entrypoint. Beide Schreiber müssen gleich arbeiten, sonst kommt
der Fehler beim nächsten Rip still zurück.
""" """
key = (einstellungen.get("makemkvAppKey") or "").strip() key = (einstellungen.get("makemkvAppKey") or "").strip()
if not key: if not key:
return return
ordner = os.path.expanduser("~/.MakeMKV") pfad = os.path.join(makemkv_daten.DATEN_DIR, "settings.conf")
try: try:
os.makedirs(ordner, exist_ok=True) os.makedirs(makemkv_daten.DATEN_DIR, exist_ok=True)
with open(os.path.join(ordner, "settings.conf"), "w") as f: try:
f.write(f'app_Key = "{key}"\n') with open(pfad, encoding="utf-8", errors="replace") as f:
alt = f.read()
except OSError:
alt = ""
with open(pfad, "w", encoding="utf-8", newline="\n") as f:
f.write(makemkv_daten.settings_conf_zusammenfuehren(alt, key))
except OSError as e: except OSError as e:
db.add_log("warning", "worker", f"MakeMKV-Key konnte nicht gesetzt werden: {e}") db.add_log("warning", "worker", f"MakeMKV-Key konnte nicht gesetzt werden: {e}")
@@ -344,6 +356,22 @@ def rip_disc(self, device_path: str, job_id: str, target_dir: str = None):
) )
db.update_job(job_id, progress=progress) db.update_job(job_id, progress=progress)
# MakeMKV-Meldungen ins Log (Befund 25.07.2026): Bis dahin überlebte NUR
# die letzte Zeile ("Failed to open disc"), und die sagt nichts. Dass
# MakeMKV bei der UHD-Disc nicht einmal versucht, einen Schlüssel zu
# holen, war deshalb nur per Hand-Lauf im Container zu sehen.
# Gedrosselt, weil das UI global nur die letzten 200 Zeilen zeigt: jede
# Meldung höchstens einmal, insgesamt höchstens MAX_MELDUNGEN je Rip.
# Code 1003 ist MakeMKVs eigenes DEBUG-Rauschen (am 25.07. beobachtet).
MAX_MELDUNGEN = 40
gesehen = set()
def melde_makemkv(code: int, text: str):
if code == 1003 or len(gesehen) >= MAX_MELDUNGEN or text in gesehen:
return
gesehen.add(text)
db.add_log("info", "makemkv", f"Job {job_id}: {text[:300]}")
einstellungen = db.get_settings() einstellungen = db.get_settings()
ist_video = disc_type in ("dvd", "bluray", "uhd") ist_video = disc_type in ("dvd", "bluray", "uhd")
transcode_an = ist_video and einstellungen.get("transcodeEnabled", True) transcode_an = ist_video and einstellungen.get("transcodeEnabled", True)
@@ -411,6 +439,7 @@ def rip_disc(self, device_path: str, job_id: str, target_dir: str = None):
output_dir=raw_dir, output_dir=raw_dir,
nur_hauptfilm=nur_hauptfilm, nur_hauptfilm=nur_hauptfilm,
titel_liste=titel_liste, titel_liste=titel_liste,
log_cb=melde_makemkv,
) )
else: else:
ergebnis = rip_video( ergebnis = rip_video(
@@ -418,26 +447,43 @@ def rip_disc(self, device_path: str, job_id: str, target_dir: str = None):
progress_cb=fortschritt, output_dir=final_dir, progress_cb=fortschritt, output_dir=final_dir,
nur_hauptfilm=nur_hauptfilm, nur_hauptfilm=nur_hauptfilm,
titel_liste=titel_liste, titel_liste=titel_liste,
log_cb=melde_makemkv,
) )
# UHD-Fehler in Klartext übersetzen — "Failed to open disc" allein hilft # MakeMKV-Fehler in Klartext übersetzen — "Failed to open disc" allein
# niemandem. Zwei bekannte Ursachen (Befunde 24.07., Summer-Wars-UHD): # hilft niemandem.
if ergebnis.get("status") == "error" and disc_type == "uhd": if ergebnis.get("status") == "error":
fehler_text = ergebnis.get("error") or "" fehler_text = ergebnis.get("error") or ""
if "volume key is unknown" in fehler_text: if "volume key is unknown" in fehler_text:
# LibreDrive lief bereits — MakeMKV kennt nur den Disc-Schlüssel # Befund 25.07.2026, im Worker nachgemessen (Akira UHD, MKB v76,
# nicht: Version zu alt ODER Disc neuer als die Key-Datenbank. # Pressung Dez. 2020): Laufwerk und MakeMKV sind in Ordnung —
# MakeMKV fragt online gar nicht erst nach einem Schlüssel, und
# der Online-Kanal liefert auch nichts mehr. Der alte Text hier
# ("Disc neuer als die Schlüssel-Datenbank, mit einem der
# nächsten Updates rippbar") war schlicht falsch und hat in die
# falsche Richtung geschickt. Details: makemkv_daten.py.
keydb = makemkv_daten.keydb_status()
ergebnis["error"] += ( ergebnis["error"] += (
" — Klartext: Das Laufwerk liest die Disc (LibreDrive OK), " " — Klartext: Laufwerk und Rippy sind in Ordnung, MakeMKV "
"aber MakeMKV kennt den Schlüssel dieser Disc nicht. Erst " "liest die Disc. Es kennt nur den Schlüssel dieser Pressung "
"prüfen: MakeMKV aktuell? (Einstellungen → System; Update = " "nicht und holt ihn auch nicht mehr online nach — MakeMKVs "
"Image-Rebuild). Ist es aktuell, ist die Disc neuer als die " "Schlüssel-Kanal liefert nichts mehr (am 25.07.2026 im "
"Schlüssel-Datenbank — MakeMKV hat einen AACS-Dump unter " "Worker nachgemessen). Abhilfe: eine KEYDB.cfg unter "
"/root/.MakeMKV/ im Worker gespeichert; im MakeMKV-Forum " "Einstellungen → System hochladen; sie wirkt ab dem nächsten "
"(Bereich 'Ultra HD Blu-ray') einreichen, mit einem der " "Rip. "
"nächsten Updates ist die Disc dann rippbar." + (
"Aktuell liegt dort keine KEYDB.cfg."
if not keydb.get("vorhanden")
else "Es liegt bereits eine KEYDB.cfg dort — sie kennt "
"diese Pressung offenbar nicht; eine neuere Fassung "
"kann helfen."
) )
elif "Failed to open disc" in fehler_text: + " Den AACS-Dump dieser Disc bewahrt Rippy jetzt dauerhaft "
"auf; er steht unter Einstellungen → System zum Download "
"bereit (für die Einreichung im MakeMKV-Forum, Bereich "
"'Ultra HD Blu-ray')."
)
elif disc_type == "uhd" and "Failed to open disc" in fehler_text:
ergebnis["error"] += ( ergebnis["error"] += (
" — 4K-UHD erkannt: Das Laufwerk kann UHD-Discs vermutlich " " — 4K-UHD erkannt: Das Laufwerk kann UHD-Discs vermutlich "
"nicht entschlüsseln. Dafür ist eine LibreDrive-Firmware nötig " "nicht entschlüsseln. Dafür ist eine LibreDrive-Firmware nötig "
+181
View File
@@ -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("", "") == ""
+55
View File
@@ -13,6 +13,7 @@ from ripping import (
build_makemkv_cmd, build_makemkv_cmd,
get_progress_from_line, get_progress_from_line,
get_progress_from_prgv, get_progress_from_prgv,
parse_msg,
write_abcde_config, write_abcde_config,
) )
@@ -63,6 +64,60 @@ def test_prgv_parsing_ignoriert_fremde_zeilen():
assert get_progress_from_prgv("PRGV:kaputt") == -1 assert get_progress_from_prgv("PRGV:kaputt") == -1
def test_parse_msg_trennt_code_und_klartext():
"""Echte Zeilen aus einem makemkvcon-Lauf vom 25.07.2026 (Akira UHD).
Feld 4 ist laut https://www.makemkv.com/developers/usage.txt der fertig
zusammengesetzte Klartext genau der landet im Rippy-Log.
"""
assert parse_msg(
'MSG:1005,0,1,"MakeMKV v1.18.4 linux(x64-release) started","%1 started","MakeMKV v1.18.4 linux(x64-release)"'
) == (1005, "MakeMKV v1.18.4 linux(x64-release) started")
assert parse_msg(
'MSG:1011,0,1,"Using LibreDrive mode (v06.3 id=866A98CB9C4E)","%1","Using LibreDrive mode (v06.3 id=866A98CB9C4E)"'
) == (1011, "Using LibreDrive mode (v06.3 id=866A98CB9C4E)")
def test_parse_msg_liest_die_uhd_fehlermeldung():
"""3303 ist der Befund, um den es beim ganzen KEYDB-Thema geht: das
Laufwerk laeuft im LibreDrive-Modus, MakeMKV kennt nur den Schluessel
DIESER Pressung nicht. Ohne diese Zeile im Log raet der Commander."""
assert parse_msg(
'MSG:3303,16777216,0,"The volume key is unknown for this disc - video can\'t be decrypted","The volume key is unknown for this disc - video can\'t be decrypted"'
) == (3303, "The volume key is unknown for this disc - video can't be decrypted")
assert parse_msg('MSG:5010,0,0,"Failed to open disc","Failed to open disc"') == (
5010,
"Failed to open disc",
)
def test_parse_msg_schneidet_meldungen_mit_komma_nicht_ab():
"""DER Grund fuer die Regex (Stand 25.07.2026): vorher stand hier
line.split(",", 4)[3]. Das schnitt jede Meldung ab, die selbst ein Komma
enthaelt und MakeMKV schreibt solche laufend. Im Log stand dann nur noch
ein Satzfragment, das mehr verwirrt als hilft."""
zeile = (
'MSG:3025,0,3,"Title #1 has length of 12 seconds, which is less than '
'minimum title length of 120 seconds and was therefore skipped",'
'"Title #%1 has length of %2 seconds which is less than minimum title '
'length of %3 seconds and was therefore skipped","1","12","120"'
)
code, text = parse_msg(zeile)
assert code == 3025
assert text.endswith("and was therefore skipped")
assert "which is less than" in text
def test_parse_msg_ignoriert_fremde_zeilen():
# Alles ausser MSG muss None liefern, sonst landet Fortschritts-Rauschen
# (PRGV kommt mehrmals pro Sekunde) als Log-Eintrag in der Datenbank.
assert parse_msg("PRGV:100,32768,65536") is None
assert parse_msg('DRV:0,2,999,12,"BD-RE ASUS BW-16D1HT","AKIRA","/dev/sr0"') is None
assert parse_msg("TCOUNT:5") is None
assert parse_msg("") is None
assert parse_msg("irgendwelcher Muell ohne Struktur") is None
def test_abcde_cmd_hat_genau_ein_ausgabeformat(): def test_abcde_cmd_hat_genau_ein_ausgabeformat():
"""Review-Fund 22.07.: '-o' stand doppelt (Format UND Verzeichnis) — abcde """Review-Fund 22.07.: '-o' stand doppelt (Format UND Verzeichnis) — abcde
parste das Verzeichnis als Format, CD-Ripping war nie funktionsfähig.""" parste das Verzeichnis als Format, CD-Ripping war nie funktionsfähig."""