From 0935766f61a5f2ccabb0bc3c8a733e7fc50e6e87 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sat, 25 Jul 2026 01:26:08 +0200 Subject: [PATCH] feat(uhd): KEYDB.cfg-Unterstuetzung - MakeMKVs Schluessel-Kanal liefert nichts mehr 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 --- .env.example | 7 + AGENTS.md | 5 +- KONZEPT.md | 16 ++ README.md | 61 ++++- ROADMAP.md | 59 +++++ SAVEPOINT.md | 83 ++++++- docker-compose.yml | 17 ++ docker/api/main.py | 141 +++++++++++ docker/api/makemkv_daten.py | 242 +++++++++++++++++++ docker/api/makemkv_key.py | 10 +- docker/api/test_api_smoke.py | 8 +- docker/api/test_makemkv_daten.py | 118 +++++++++ docker/ui/nginx.conf | 6 + docker/ui/src/pages/Anleitung.tsx | 33 ++- docker/ui/src/pages/Settings.tsx | 268 ++++++++++++++++++++- docker/worker/Dockerfile | 10 +- docker/worker/caps.py | 20 ++ docker/worker/entrypoint.sh | 50 +++- docker/worker/makemkv_daten.py | 242 +++++++++++++++++++ docker/worker/ripping.py | 72 ++++-- docker/worker/tasks.py | 82 +++++-- docker/worker/test_makemkv_daten_worker.py | 181 ++++++++++++++ docker/worker/test_ripping_helpers.py | 55 +++++ 23 files changed, 1717 insertions(+), 69 deletions(-) create mode 100644 docker/api/makemkv_daten.py create mode 100644 docker/api/test_makemkv_daten.py create mode 100644 docker/worker/makemkv_daten.py create mode 100644 docker/worker/test_makemkv_daten_worker.py diff --git a/.env.example b/.env.example index 3f028a5..6712eb3 100644 --- a/.env.example +++ b/.env.example @@ -33,6 +33,13 @@ OMDB_API_KEY= # nächsten Rip, ohne Neustart, und schlägt diesen Env-Wert). 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). # Update: Version hier anheben, dann auf der Rippy-Maschine # docker compose build worker && docker compose up -d worker diff --git a/AGENTS.md b/AGENTS.md index 5434953..efb0dbc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 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) - ✅ **Etappe 13 (v3.2):** Universal-Komfort-Runde — Media-Server-Integration, echte Benachrichtigungen, SMB-Klartext-Fehler, UHD-Arbeitsverzeichnis, 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 diff --git a/KONZEPT.md b/KONZEPT.md index dc9080c..ae77310 100644 --- a/KONZEPT.md +++ b/KONZEPT.md @@ -127,6 +127,7 @@ Ein modular aufgebautes System, das bei Disc-Einwurf automatisch den Typ erkennt | Risiko | Status | Behandlung | |--------|--------|------------| | 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) | | Pending-Queue / State-Manager | Gelöst | Redis mit AOF-Persistence | | 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 (pro IP) bleibt. Wer Rippy je nach außen öffnet, stellt einen 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 /Season NN plus Episoden-Zuordnung per Laufzeitabgleich (TMDB) — erfüllt Etappe-12-Ziel „Serien-Episoden-Erkennung" in der ersten Ausbaustufe (nur bei diff --git a/README.md b/README.md index b0efc94..874fc03 100644 --- a/README.md +++ b/README.md @@ -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, dann stößt Rippy nach jedem fertigen Rip sofort einen Bibliotheks-Scan an — Disc rein, Film erscheint im Server. -5. **4K-UHD**: braucht ein LibreDrive-fähiges Laufwerk (MakeMKV-Forum: - „Ultimate UHD Drives Flashing Guide"). Normale BD/DVD gehen mit jedem - Laufwerk. ⚠️ UHD-Rohdaten sind bis 100 GB groß — wenn die Platte der +5. **4K-UHD**: braucht **zwei** Dinge — ein LibreDrive-fähiges Laufwerk + (MakeMKV-Forum: „Ultimate UHD Drives Flashing Guide") **und** einen + 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** (Einstellungen → Verarbeitung) auf eine eingehängte Freigabe, z. B. `/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 | | 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 (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 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 offiziellen HandBrake-Releases (12 h gecacht). MakeMKV-Update ohne Code-Änderung: `MAKEMKV_VERSION=x.y.z` in die `.env`, dann -`docker compose build worker && docker compose up -d worker` — neue -Versionen bringen auch die neueste Disc-Schlüssel-Datenbank mit. +`docker compose build worker && docker compose up -d worker`. +⚠️ **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 offiziellen Version hinterher); der native Windows-Worker nutzt die aktuelle Version direkt. @@ -153,6 +196,7 @@ bleibt All-in-one. | `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_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_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 | @@ -181,6 +225,11 @@ Voraussetzungen auf dem Ziel-Host: ``` (reboot-fest als systemd-`.mount`-Unit persistieren.) Braucht einen klassischen 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 --build`, `http://` öffnen — der Einrichtungs-Assistent führt durch diff --git a/ROADMAP.md b/ROADMAP.md index 4a28fe1..49ec459 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 1. **Design 2.0** — an Gemini übergeben (24.07.2026). Vollständiges diff --git a/SAVEPOINT.md b/SAVEPOINT.md index 25bc540..da00148 100644 --- a/SAVEPOINT.md +++ b/SAVEPOINT.md @@ -1,6 +1,73 @@ # SAVEPOINT — Rippy -## Aktueller Stand: v3.9 — Windows-Installer als echte .exe (Icon, kein Konsolenfenster) (24.07.2026) +## Aktueller Stand: v3.10 — 4K-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: .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 erledigt** — makemkvcon meldet „Using LibreDrive mode (v06.3)". Der 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 — - auf der VM mit 1.17.7 UND der aktuellsten 1.18.4 bewiesen. Konsequenzen: + is unknown" — auf der VM mit 1.17.7 UND der aktuellsten 1.18.4 + reproduziert. Konsequenzen: MakeMKV auf **1.18.4** gehoben (Scan-Hänger durch überall genutztes --noscan umgangen, auf der BU40N sauber gelaufen; URL-Base → /download), run_makemkv reicht kritische Meldungen (volume key/Key abgelaufen) in 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/). - Summer Wars bleibt bis zu einem MakeMKV-Key-Update nicht entschlüsselbar - — das ist Stand der Technik, kein Rippy-Bug. + **⚠️ Korrigiert am 25.07.2026 (siehe v3.10 ganz oben):** Die hier + 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 ohne Popup in den passenden Schnellwahl-Ordner (Serie/Film/Musik). - **Job-Verwaltung**: „Neu komprimieren" nur noch, wenn Rohdaten wirklich diff --git a/docker-compose.yml b/docker-compose.yml index 2e3b15f..69d5bd3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,10 @@ services: - TMDB_API_KEY=${TMDB_API_KEY:-} - THETVDB_API_KEY=${THETVDB_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 healthcheck: test: ["CMD-SHELL", "python -c 'import urllib.request; urllib.request.urlopen(\"http://localhost:8000/health\")'"] @@ -47,6 +51,8 @@ services: bind: propagation: rshared - temp:/app/temp + # MakeMKV-Datenverzeichnis (dasselbe wie im Worker, siehe dort). + - ${MAKEMKV_DATA_HOST:-/srv/rippy/makemkv}:/app/makemkv-data devices: - ${OPTICAL_SR:-/dev/sr0}:/dev/sr0 networks: @@ -78,6 +84,10 @@ services: - REDIS_URL=redis://redis:6379/0 - RIP_OUTPUT_DIR=/app/media - 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; # via .env übersteuerbar, z. B. WORKER_NAME=wohnzimmer-vm) - WORKER_NAME=${WORKER_NAME:-rippy-hauptworker} @@ -92,6 +102,13 @@ services: bind: propagation: rslave - 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: # Host-Geräteknoten via .env (OPTICAL_SR/OPTICAL_SG) — je Rechner ANDERS! # MakeMKV spricht Laufwerke über die SCSI-Generic-Schicht an; ohne den diff --git a/docker/api/main.py b/docker/api/main.py index 654046c..b2c0d9f 100644 --- a/docker/api/main.py +++ b/docker/api/main.py @@ -12,6 +12,7 @@ import uuid import db import devices as device_discovery +import makemkv_daten import makemkv_key import mounts as mount_verwaltung import notify @@ -1190,6 +1191,146 @@ async def system_info(): return await asyncio.to_thread(sammle) +# Eigene Wurzel für die Datei-Härtung der AACS-Dumps. Bewusst NICHT die +# MEDIA_ROOT-Helfer (_sicherer_dateiname/_validiere_ziel/_job_ausgabeordner): +# die prüfen hart gegen /app/media und würden hier IMMER 404 liefern. +# Das MakeMKV-Datenverzeichnis liegt woanders (in der API auf +# /app/makemkv-data, im Worker auf /root/.MakeMKV — laut docker-compose.yml +# beides dasselbe Host-Verzeichnis). +MAKEMKV_DATA_ROOT = os.path.realpath(makemkv_daten.DATEN_DIR) + + +class KeydbRequest(BaseModel): + inhalt: str # voller Text der KEYDB.cfg (kein Upload — es gibt kein python-multipart) + + +@app.get("/system/keydb") +async def get_keydb_status(): + """Was liegt gerade als KEYDB.cfg im MakeMKV-Datenverzeichnis? + + Hintergrund (Befund 25.07.2026, live auf der VM nachgemessen): Bei + 4K-UHD-Discs meldet MakeMKV "The volume key is unknown for this disc" und + holt den Schlüssel NICHT mehr online nach — die dokumentierten + Schlüssel-Server lösen weltweit nicht mehr auf. Der einzige heute + funktionierende Weg ist eine KEYDB.cfg, die der Nutzer selbst mitbringt. + Rippy liefert KEINE Schlüssel mit, lädt keine herunter und verteilt keine — + es stellt nur den Platz bereit und zeigt ehrlich an, was dort liegt. + + Fehlendes Verzeichnis oder fehlende Datei ist der NORMALFALL: dann kommt + 200 mit vorhanden=false zurück, niemals 404 oder 500. + """ + def sammle(): + return makemkv_daten.keydb_status() + + return await asyncio.to_thread(sammle) + + +@app.post("/system/keydb") +async def set_keydb(request: KeydbRequest): + """Legt die vom Nutzer mitgebrachte KEYDB.cfg ab (atomar, ersetzt die alte). + + WICHTIG für die Ehrlichkeit: Die Datei wirkt erst beim NÄCHSTEN Rip — + makemkvcon liest sie beim Prozessstart, ein bereits laufender Rip merkt + nichts davon. Genau so steht es auch im Log-Eintrag. + """ + # Reine Prüfung (kein Dateisystem) — fängt den häufigsten Bedienfehler ab: + # statt der KEYDB.cfg landet die HTML-Fehlerseite eines Downloads im Feld. + fehler = makemkv_daten.keydb_pruefen(request.inhalt) + if fehler: + raise HTTPException(status_code=422, detail=fehler) + + def schreibe(): + return makemkv_daten.keydb_schreiben(request.inhalt) + + try: + status = await asyncio.to_thread(schreibe) + except OSError as e: + raise HTTPException( + status_code=500, + detail=( + f"KEYDB.cfg konnte nicht geschrieben werden: {e}. " + "Prüfe, ob das MakeMKV-Datenverzeichnis auf der VM existiert und " + "beschreibbar ist (Standard: /srv/rippy/makemkv)." + ), + ) + await asyncio.to_thread( + db.add_log, "success", "makemkv-keydb", + f"KEYDB.cfg abgelegt: {status['eintraege']} Zeilen mit Disc-Kennung, " + f"{status['groesse_bytes']} Bytes ({status['pfad']}). " + "Wirkt erst beim NÄCHSTEN Rip — MakeMKV liest die Datei beim Start.", + ) + return status + + +@app.delete("/system/keydb") +async def delete_keydb(): + """Entfernt die KEYDB.cfg (z. B. nach einem Fehlgriff beim Einfügen). + + Auch hier gilt: Die Änderung wirkt erst beim NÄCHSTEN Rip. Fehlt die Datei + schon, ist das kein Fehler — es kommt derselbe Zustand mit vorhanden=false. + """ + def loesche(): + return makemkv_daten.keydb_loeschen() + + try: + status = await asyncio.to_thread(loesche) + except OSError as e: + raise HTTPException( + status_code=500, + detail=f"KEYDB.cfg konnte nicht entfernt werden: {e}", + ) + await asyncio.to_thread( + db.add_log, "warning", "makemkv-keydb", + "KEYDB.cfg entfernt. Ab dem NÄCHSTEN Rip fehlen die selbst mitgebrachten " + "Schlüssel wieder — UHD-Discs können dann erneut an " + "'The volume key is unknown for this disc' scheitern.", + ) + return status + + +@app.get("/system/aacs-dumps") +async def get_aacs_dumps(): + """AACS-Dumps, die MakeMKV selbst abgelegt hat (neueste zuerst). + + MakeMKV schreibt sie beim gescheiterten UHD-Versuch ins Datenverzeichnis + (Meldung 3332 "Saved AACS dump file as file:///root/.MakeMKV/.tgz", + am 25.07.2026 so beobachtet). Rippy wertet sie nicht aus und schickt sie + nirgendwohin — es zeigt nur, dass sie da sind, damit der Nutzer selbst + entscheiden kann, was er damit tut. + """ + def liste(): + return {"dumps": makemkv_daten.dumps_auflisten()} + + return await asyncio.to_thread(liste) + + +@app.get("/system/aacs-dumps/{dateiname}") +async def download_aacs_dump(dateiname: str): + """Lädt EINEN AACS-Dump herunter. + + Pfad-Validierung genauso streng wie beim Job-Datei-Download: nackter Name + ohne Pfadtrenner und ohne führenden Punkt (ist_aacs_dump) PLUS realpath, + der das MakeMKV-Datenverzeichnis nicht verlassen darf (kein ..-Ausbruch, + kein Symlink nach draußen). + """ + if not makemkv_daten.ist_aacs_dump(dateiname): + raise HTTPException( + status_code=404, + detail="Kein gültiger Dump-Name — erwartet wird eine .tgz-Datei ohne Pfadangabe.", + ) + pfad = os.path.join(MAKEMKV_DATA_ROOT, dateiname) + + def pruefe(): + return os.path.isfile(pfad) and os.path.realpath(pfad).startswith(MAKEMKV_DATA_ROOT) + + if not await asyncio.to_thread(pruefe): + raise HTTPException( + status_code=404, + detail="Dump nicht gefunden — MakeMKV legt ihn erst beim gescheiterten UHD-Versuch an.", + ) + return FileResponse(pfad, filename=dateiname, media_type="application/gzip") + + class NotificationTestRequest(BaseModel): url: str diff --git a/docker/api/makemkv_daten.py b/docker/api/makemkv_daten.py new file mode 100644 index 0000000..1ce430d --- /dev/null +++ b/docker/api/makemkv_daten.py @@ -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/.tgz" (am 25.07.2026 so beobachtet). + +ZWILLINGSDATEI: liegt identisch unter docker/api/ und docker/worker/ — beide +Images brauchen sie, ein gemeinsames Paket gibt es in diesem Projekt nicht +(gleiche Lage wie bei db.py). Aenderungen IMMER in BEIDEN Dateien nachziehen. +""" + +import os +import re +from datetime import datetime, timezone + +# Container-Pfad aus der Umgebung: /root/.MakeMKV im Worker (nur dort sucht +# makemkvcon), /app/makemkv-data in der API. Beide zeigen laut +# docker-compose.yml auf DASSELBE Host-Verzeichnis. +DATEN_DIR = os.getenv("MAKEMKV_DATA_DIR", "/root/.MakeMKV") + +# GROSS geschrieben — unter Linux case-sensitiv, "keydb.cfg" wird ignoriert. +KEYDB_NAME = "KEYDB.cfg" + +# Obergrenze fuer den Upload. Eine vollstaendige oeffentliche KEYDB.cfg liegt +# im einstelligen MB-Bereich; 64 MB sind reichlich Luft und verhindern, dass +# eine versehentlich hochgeladene Riesendatei den Speicher vollschreibt. +MAX_KEYDB_BYTES = 64 * 1024 * 1024 + +# Eine Disc-Zeile beginnt mit der 40 Zeichen langen Hex-Kennung (optional mit +# 0x davor), danach folgt das Gleichheitszeichen. Alles andere (Kommentare ab +# ";", Leerzeilen, Fortsetzungsfelder) zaehlt nicht als Eintrag. +_DISC_ZEILE = re.compile(r"^\s*(?:0x)?[0-9a-fA-F]{40}\s*=") + + +def _iso(zeitstempel: float) -> str: + """Unix-Zeit -> ISO-8601 in UTC (sekundengenau), wie db.utcnow() es tut.""" + return datetime.fromtimestamp(zeitstempel, timezone.utc).isoformat(timespec="seconds") + + +def keydb_pfad(daten_dir: str = None) -> str: + """Voller Pfad zur KEYDB.cfg im Datenverzeichnis.""" + return os.path.join(daten_dir or DATEN_DIR, KEYDB_NAME) + + +def zaehle_disc_eintraege(inhalt: str) -> int: + """Zeilen mit Disc-Kennung zaehlen (pure Funktion, testbar). + + Bewusst eine Heuristik und keine vollstaendige Auswertung: MakeMKV liest + die Datei mit seinem eigenen Parser, und wie viele Eintraege es daraus + macht, ist von aussen nicht sichtbar. Die Zahl dient nur dazu, im UI + "da liegt wirklich etwas drin" von "leere oder falsche Datei" zu + unterscheiden — sie wird deshalb auch genau so beschriftet. + """ + return sum(1 for zeile in inhalt.splitlines() if _DISC_ZEILE.match(zeile)) + + +def keydb_pruefen(inhalt: str) -> str: + """Prueft hochgeladenen Inhalt; gibt deutschen Fehlertext oder "" zurueck. + + Verhindert den haeufigsten Bedienfehler: statt der KEYDB.cfg landet die + HTML-Fehlerseite eines Downloads oder eine leere Datei im Verzeichnis — + MakeMKV wuerde dann still weiter "volume key is unknown" melden. + """ + if not inhalt.strip(): + return "Die Datei ist leer." + if len(inhalt.encode("utf-8")) > MAX_KEYDB_BYTES: + return ( + "Die Datei ist groesser als " + f"{MAX_KEYDB_BYTES // (1024 * 1024)} MB — das ist keine KEYDB.cfg." + ) + if inhalt.lstrip()[:1] == "<": + return ( + "Das sieht nach HTML aus, nicht nach einer KEYDB.cfg — " + "vermutlich wurde eine Fehlerseite statt der Datei geladen." + ) + if zaehle_disc_eintraege(inhalt) == 0: + return ( + "Keine einzige Zeile mit Disc-Kennung gefunden (40 Hex-Zeichen, " + "dann ein Gleichheitszeichen). Das ist keine KEYDB.cfg." + ) + return "" + + +def keydb_status(daten_dir: str = None) -> dict: + """Zustand der KEYDB.cfg. Fehlt sie, ist das der Normalfall, kein Fehler.""" + pfad = keydb_pfad(daten_dir) + try: + angaben = os.stat(pfad) + except OSError: + return { + "vorhanden": False, + "pfad": pfad, + "groesse_bytes": 0, + "eintraege": 0, + "geaendert": "", + } + eintraege = 0 + try: + with open(pfad, encoding="utf-8", errors="replace") as datei: + eintraege = zaehle_disc_eintraege(datei.read()) + except OSError: + pass # Datei da, aber unlesbar: Groesse/Datum stimmen trotzdem + return { + "vorhanden": True, + "pfad": pfad, + "groesse_bytes": angaben.st_size, + "eintraege": eintraege, + "geaendert": _iso(angaben.st_mtime), + } + + +def keydb_schreiben(inhalt: str, daten_dir: str = None) -> dict: + """Schreibt die KEYDB.cfg atomar und meldet den neuen Zustand. + + Erst in eine Nebendatei, dann os.replace: waehrend ein Rip laeuft, darf + makemkvcon niemals eine halb geschriebene Datei zu sehen bekommen. + """ + pfad = keydb_pfad(daten_dir) + os.makedirs(os.path.dirname(pfad), exist_ok=True) + neben = pfad + ".neu" + try: + with open(neben, "w", encoding="utf-8", newline="\n") as datei: + datei.write(inhalt) + os.replace(neben, pfad) + except OSError: + # Die Nebendatei nie liegen lassen: eine halb geschriebene + # KEYDB.cfg.neu verwirrt jeden, der ins Verzeichnis schaut, und + # belegt im Extremfall 64 MB, die niemand mehr aufräumt. + try: + os.remove(neben) + except OSError: + pass + raise + return keydb_status(daten_dir) + + +def keydb_loeschen(daten_dir: str = None) -> dict: + """Entfernt die KEYDB.cfg (z. B. nach einem Fehlgriff beim Hochladen). + + Nur "Datei war schon weg" wird geschluckt — das ist das gewünschte + Ergebnis. Jeder andere Fehler (schreibgeschützter Mount, fremder + Eigentümer) MUSS nach oben durch: sonst meldete die API einen Erfolg, + den es nicht gab, und die Datei wirkte beim nächsten Rip weiter. + """ + try: + os.remove(keydb_pfad(daten_dir)) + except FileNotFoundError: + pass + return keydb_status(daten_dir) + + +def ist_aacs_dump(name: str) -> bool: + """Dateiname eines AACS-Dumps? (pure Funktion, testbar) + + MakeMKV legt ihn als .tgz direkt im Datenverzeichnis ab + (Meldung 3332). Pfadtrenner und fuehrende Punkte werden hier schon + ausgeschlossen, damit der Download-Endpunkt keine Pfad-Tricks erlaubt. + """ + return ( + name.endswith(".tgz") + and "/" not in name + and "\\" not in name + and not name.startswith(".") + ) + + +def dumps_auflisten(daten_dir: str = None) -> list: + """Alle AACS-Dumps im Datenverzeichnis, neueste zuerst.""" + ordner = daten_dir or DATEN_DIR + try: + namen = os.listdir(ordner) + except OSError: + return [] + liste = [] + for name in namen: + if not ist_aacs_dump(name): + continue + try: + angaben = os.stat(os.path.join(ordner, name)) + except OSError: + continue + liste.append( + { + "name": name, + "groesse_bytes": angaben.st_size, + "geaendert": _iso(angaben.st_mtime), + "_sort": angaben.st_mtime, + } + ) + liste.sort(key=lambda eintrag: eintrag["_sort"], reverse=True) + for eintrag in liste: + del eintrag["_sort"] + return liste + + +def settings_conf_zusammenfuehren(inhalt: str, key: str) -> str: + """app_Key setzen, ohne den Rest der settings.conf zu verlieren (pure). + + Bis zum 25.07.2026 haben entrypoint.sh UND tasks.py die Datei komplett + ueberschrieben. Mit dem jetzt persistenten Datenverzeichnis waere damit + bei jedem Containerstart und vor jedem Rip alles andere weg — z. B. + app_UpdateEnable. Leerer Key laesst die vorhandene Zeile ebenfalls fallen, + damit ein bewusst geleerter Key nicht heimlich weiterwirkt. + """ + zeilen = [z for z in inhalt.splitlines() if not z.lstrip().startswith("app_Key")] + if key: + zeilen.append('app_Key = "{}"'.format(key)) + text = "\n".join(zeilen).strip("\n") + return text + "\n" if text else "" diff --git a/docker/api/makemkv_key.py b/docker/api/makemkv_key.py index 6ae9953..b65f44e 100644 --- a/docker/api/makemkv_key.py +++ b/docker/api/makemkv_key.py @@ -11,8 +11,14 @@ Dieses Modul holt den aktuellen Key vom Forum und schreibt ihn in die Settings Rebuild/Neustart, ab dem naechsten Rip. Wichtig: Das ist die kostenlose, oeffentliche Beta-LIZENZ der Software selbst — -NICHT das Entschluesseln oder Verteilen von Disc-Schluesseln. Den AACS-Schluessel -zieht MakeMKV via LibreDrive ohnehin selbst aus dem Laufwerk. +NICHT das Entschluesseln oder Verteilen von Disc-Schluesseln. Diese Grenze gilt +unveraendert; Rippy liefert keine Disc-Schluessel mit und verteilt keine. + +Richtigstellung 25.07.2026: Hier stand frueher, MakeMKV ziehe den AACS-Schluessel +via LibreDrive ohnehin selbst aus dem Laufwerk. Das stimmt fuer Blu-ray, aber +NICHT fuer 4K-UHD — dort braucht MakeMKV den Volume-Key der jeweiligen Pressung, +und den bekommt es weder aus dem Laufwerk noch (heute) aus dem Netz. Belege und +Messungen stehen im Modul-Kopf von makemkv_daten.py. Quelle/Format dokumentiert (AGENTS Regel D — nicht geraten): - Forum-Thread: https://forum.makemkv.com/forum/viewtopic.php?t=1053 diff --git a/docker/api/test_api_smoke.py b/docker/api/test_api_smoke.py index 79d13e7..ef683bc 100644 --- a/docker/api/test_api_smoke.py +++ b/docker/api/test_api_smoke.py @@ -18,7 +18,13 @@ def test_main_importierbar_und_routen_verdrahtet(): from main import app routen = {route.path for route in app.routes} - for pfad in ("/health", "/jobs", "/devices", "/logs", "/settings", "/prescan"): + for pfad in ( + "/health", "/jobs", "/devices", "/logs", "/settings", "/prescan", + # KEYDB.cfg + AACS-Dumps: der Platz für die selbst mitgebrachte + # Schlüsseldatei (Befund 25.07.2026 — MakeMKV holt UHD-Schlüssel nicht + # mehr online nach). Ohne diese Routen ist die Seite im UI tot. + "/system/keydb", "/system/aacs-dumps", "/system/aacs-dumps/{dateiname}", + ): assert pfad in routen, f"Route {pfad} fehlt" diff --git a/docker/api/test_makemkv_daten.py b/docker/api/test_makemkv_daten.py new file mode 100644 index 0000000..b32cd3f --- /dev/null +++ b/docker/api/test_makemkv_daten.py @@ -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("\n404 Not Found\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("", "") == "" diff --git a/docker/ui/nginx.conf b/docker/ui/nginx.conf index 6a593df..e20130f 100644 --- a/docker/ui/nginx.conf +++ b/docker/ui/nginx.conf @@ -17,6 +17,12 @@ server { # SSE (/api/stream/jobs) braucht ungepufferte, lange Verbindungen: proxy_buffering off; 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 / { diff --git a/docker/ui/src/pages/Anleitung.tsx b/docker/ui/src/pages/Anleitung.tsx index 4b9cd32..9a8ba96 100644 --- a/docker/ui/src/pages/Anleitung.tsx +++ b/docker/ui/src/pages/Anleitung.tsx @@ -134,12 +134,15 @@ export default function AnleitungPage() { 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.

+ {/* 25.07.2026 richtiggestellt: Updates bringen keine Disc-Schluessel mit — siehe UHD-Absatz unten. */}

Updates: „Auf Updates prüfen" (ebenfalls Einstellungen → System) vergleicht die installierten Versionen mit makemkv.com und den offiziellen HandBrake-Releases. Gibt es ein MakeMKV-Update, zeigt Rippy den fertigen - Update-Befehl an — wichtig, weil neue Versionen auch die neueste - Disc-Schlüssel-Datenbank mitbringen. + Update-Befehl an — neue Versionen bringen bessere Laufwerks-Unterstützung und + Fehlerbehebungen. Disc-Schlüssel für 4K-UHD kommen dagegen nicht + aus einem Update, sondern nur aus der Datei KEYDB.cfg, die du selbst + unter Einstellungen → System hochlädst (mehr dazu unter „Häufige Fragen").

@@ -157,11 +160,31 @@ export default function AnleitungPage() { „Diese Disc wurde bereits gerippt"? Rippy erkennt Discs am Fingerabdruck. Nochmal rippen geht trotzdem — der Hinweis verhindert nur Versehen.

+ {/* + 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. + */}

4K-UHD schlägt fehl mit „volume key is unknown"? Das Laufwerk - liest die Disc (LibreDrive), aber MakeMKV kennt den Schlüssel dieser (zu neuen) Pressung - noch nicht. Den automatisch gespeicherten AACS-Dump im MakeMKV-Forum einreichen — mit einem - der nächsten Updates ist die Disc rippbar. + liest die Disc einwandfrei (LibreDrive) — MakeMKV fehlt nur der Schlüssel dieser Pressung. + Früher holte MakeMKV solche Schlüssel selbst aus dem Netz; dieser Kanal liefert heute nichts + mehr, und ein MakeMKV-Update ändert daran nichts. +

+

+ Der einzige Weg, der heute funktioniert, ist eine Datei namens KEYDB.cfg — + eine Textliste mit Disc-Schlüsseln, die du selbst mitbringst. Unter Einstellungen → System + hochladen, sie wirkt ab dem nächsten Rip. Rippy liefert keine + Schlüssel mit und lädt auch keine herunter — Rippy stellt nur den Platz für deine + Datei bereit und zeigt dir an, was dort liegt. +

+

+ Der AACS-Dump 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 KEYDB.cfg einträgst.

„Neu komprimieren" fehlt bei einem Fehl-Job? Der Knopf diff --git a/docker/ui/src/pages/Settings.tsx b/docker/ui/src/pages/Settings.tsx index 227d617..80487d4 100644 --- a/docker/ui/src/pages/Settings.tsx +++ b/docker/ui/src/pages/Settings.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react' -import { Save, Disc, Cpu, Database, Globe, AlertCircle, CheckCircle, HardDrive, Wrench, Send, Tv } from 'lucide-react' +import { useState, useEffect, useRef } from '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 { useToast } from '../context/ToastContext' import StorageMounts from '../components/StorageMounts' @@ -62,7 +62,26 @@ type SettingsTab = 'ripping' | 'verarbeitung' | 'worker' | 'speicherziele' | 'ap interface WorkerInfo { name: 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 { @@ -73,6 +92,24 @@ interface SystemInfo { 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() { const [settings, setSettings] = useState(defaultSettings) const [activeTab, setActiveTab] = useState('ripping') @@ -86,6 +123,13 @@ export default function SettingsPage() { const [quellenBusy, setQuellenBusy] = useState(false) const [updates, setUpdates] = useState | null>(null) 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(null) + const [keydbBusy, setKeydbBusy] = useState(false) + const [dumps, setDumps] = useState([]) + const keydbInput = useRef(null) 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 () => { setQuellenBusy(true) try { @@ -131,6 +218,12 @@ export default function SettingsPage() { api.get('/system/info') .then(r => setSystemInfo(r.data)) .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 () => { @@ -633,13 +726,40 @@ export default function SettingsPage() { HandBrake {w.info.handbrake} )} + {/* Nur das Verzeichnis des rippenden Workers zaehlt — deshalb je Worker. */} + {w.info?.keydb === 'ja' && ( + + KEYDB.cfg vorhanden + + )} + {w.info?.keydb === 'nein' && ( + + keine KEYDB.cfg + + )} + {w.info?.keydb === 'unbekannt' && ( + + KEYDB.cfg unbekannt + + )} )) )} + {/* + 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. + */}

MakeMKV lässt sich aktuell halten - (Update-Check unten; Version über MAKEMKV_VERSION + Rebuild) — wichtig wegen der - Disc-Schlüssel-Datenbank. + (Update-Check unten; Version über MAKEMKV_VERSION + Rebuild) — neue Versionen bringen + vor allem Laufwerks-Unterstützung und Fehlerbehebungen. Disc-Schlüssel für neue + 4K-UHD-Pressungen kommen dagegen NICHT aus einem Update, sondern nur aus der KEYDB.cfg + im Block darunter. {' '}HandBrake im eingebauten Docker-Worker ist bewusst die stabile Debian-Version — für die Kompression völlig ausreichend und wird nicht separat aktualisiert. Native Worker (Windows) holen @@ -655,6 +775,140 @@ export default function SettingsPage() { 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. + */} +

+
+

+ + MakeMKV-Schlüssel (KEYDB.cfg) +

+
+ {/* + Versteckter Datei-Dialog: der Inhalt wird im Browser gelesen und als + JSON gepostet. Kein Multipart — der API fehlt python-multipart. + */} + { + const datei = e.target.files?.[0] + // Wert leeren, damit dieselbe Datei erneut gewählt werden kann. + e.target.value = '' + if (datei) keydbHochladen(datei) + }} + /> + + {keydb?.vorhanden && ( + + )} +
+
+ + {keydb?.vorhanden ? ( +
+

Eine KEYDB.cfg liegt bereit.

+

+ {bytesLesbar(keydb.groesse_bytes)} · {keydb.eintraege} Zeilen mit Disc-Kennung · + zuletzt geändert {zeitLesbar(keydb.geaendert)} +

+

{keydb.pfad}

+
+ ) : ( +
+

Es liegt keine KEYDB.cfg bereit.

+

+ 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. +

+
+ )} + +

+ Die KEYDB.cfg 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. +

+

+ Wichtig: 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. +

+

+ Eine hochgeladene Datei ersetzt die bisherige und wirkt + ab dem nächsten Rip — 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. +

+
+ + {/* + 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. + */} +
+
+

AACS-Dumps

+ +
+ {dumps.length === 0 ? ( +

+ Noch kein Dump vorhanden. MakeMKV legt hier automatisch einen ab, sobald eine 4K-UHD-Disc + an einem unbekannten Schlüssel scheitert. +

+ ) : ( + + )} +

+ 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 Ultra HD + Blu-ray einreichen. Dort können Leute daraus den Schlüssel für deine Pressung ermitteln; + den trägst du dann in deine KEYDB.cfg ein. +

+
+

Update-Check

@@ -672,7 +926,9 @@ export default function SettingsPage() {

⚠️ Update verfügbar — in der .env MAKEMKV_VERSION={updates.makemkv.verfuegbar} setzen, dann auf der Rippy-Maschine docker compose build worker && docker compose up -d worker. - 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 + keine Disc-Schlüssel: die kommen ausschließlich aus deiner KEYDB.cfg.

) : ✓ aktuell} diff --git a/docker/worker/Dockerfile b/docker/worker/Dockerfile index 02d383e..5356f6f 100644 --- a/docker/worker/Dockerfile +++ b/docker/worker/Dockerfile @@ -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 # gepinnt. Beides erledigt: der Scan wird überall mit --noscan umgangen # (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 -# Version zählt, weil sie die neueste AACS-Schlüssel-Datenbank mitbringt — -# 1.17.7 kannte z. B. den Key der Summer-Wars-UHD (MKB v82) nicht. +# das Laufwerk ist geflasht (LibreDrive v06.3 bestätigt). +# Richtigstellung 25.07.2026: Hier stand, die aktuelle Version zähle, weil sie +# "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 # aktuelle Version, /download/old die älteren. Wenn Cloudflare BuildKit- # Downloads drosselt (24.07.: außerhalb des Builds ging alles, im Build diff --git a/docker/worker/caps.py b/docker/worker/caps.py index 749b923..1a56688 100644 --- a/docker/worker/caps.py +++ b/docker/worker/caps.py @@ -85,4 +85,24 @@ def werkzeug_versionen() -> dict: info["makemkv_key"] = "env" else: 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 diff --git a/docker/worker/entrypoint.sh b/docker/worker/entrypoint.sh index c76d54d..9e6fa8e 100644 --- a/docker/worker/entrypoint.sh +++ b/docker/worker/entrypoint.sh @@ -1,11 +1,51 @@ #!/bin/sh -# Schreibt den MakeMKV-Beta-Key aus der Umgebung in die Settings (falls gesetzt). -# Ohne Key: DVD-Ripping geht immer, Blu-ray läuft im 30-Tage-Testmodus. +# Bereitet MakeMKVs Datenverzeichnis vor, BEVOR der Worker startet. +# +# 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 -if [ -n "${MAKEMKV_APP_KEY}" ]; then - mkdir -p /root/.MakeMKV - printf 'app_Key = "%s"\n' "${MAKEMKV_APP_KEY}" > /root/.MakeMKV/settings.conf +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 + grep -v '^app_Key' "${CONF}" > "${CONF}.neu" 2>/dev/null + 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 exec "$@" diff --git a/docker/worker/makemkv_daten.py b/docker/worker/makemkv_daten.py new file mode 100644 index 0000000..1ce430d --- /dev/null +++ b/docker/worker/makemkv_daten.py @@ -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/.tgz" (am 25.07.2026 so beobachtet). + +ZWILLINGSDATEI: liegt identisch unter docker/api/ und docker/worker/ — beide +Images brauchen sie, ein gemeinsames Paket gibt es in diesem Projekt nicht +(gleiche Lage wie bei db.py). Aenderungen IMMER in BEIDEN Dateien nachziehen. +""" + +import os +import re +from datetime import datetime, timezone + +# Container-Pfad aus der Umgebung: /root/.MakeMKV im Worker (nur dort sucht +# makemkvcon), /app/makemkv-data in der API. Beide zeigen laut +# docker-compose.yml auf DASSELBE Host-Verzeichnis. +DATEN_DIR = os.getenv("MAKEMKV_DATA_DIR", "/root/.MakeMKV") + +# GROSS geschrieben — unter Linux case-sensitiv, "keydb.cfg" wird ignoriert. +KEYDB_NAME = "KEYDB.cfg" + +# Obergrenze fuer den Upload. Eine vollstaendige oeffentliche KEYDB.cfg liegt +# im einstelligen MB-Bereich; 64 MB sind reichlich Luft und verhindern, dass +# eine versehentlich hochgeladene Riesendatei den Speicher vollschreibt. +MAX_KEYDB_BYTES = 64 * 1024 * 1024 + +# Eine Disc-Zeile beginnt mit der 40 Zeichen langen Hex-Kennung (optional mit +# 0x davor), danach folgt das Gleichheitszeichen. Alles andere (Kommentare ab +# ";", Leerzeilen, Fortsetzungsfelder) zaehlt nicht als Eintrag. +_DISC_ZEILE = re.compile(r"^\s*(?:0x)?[0-9a-fA-F]{40}\s*=") + + +def _iso(zeitstempel: float) -> str: + """Unix-Zeit -> ISO-8601 in UTC (sekundengenau), wie db.utcnow() es tut.""" + return datetime.fromtimestamp(zeitstempel, timezone.utc).isoformat(timespec="seconds") + + +def keydb_pfad(daten_dir: str = None) -> str: + """Voller Pfad zur KEYDB.cfg im Datenverzeichnis.""" + return os.path.join(daten_dir or DATEN_DIR, KEYDB_NAME) + + +def zaehle_disc_eintraege(inhalt: str) -> int: + """Zeilen mit Disc-Kennung zaehlen (pure Funktion, testbar). + + Bewusst eine Heuristik und keine vollstaendige Auswertung: MakeMKV liest + die Datei mit seinem eigenen Parser, und wie viele Eintraege es daraus + macht, ist von aussen nicht sichtbar. Die Zahl dient nur dazu, im UI + "da liegt wirklich etwas drin" von "leere oder falsche Datei" zu + unterscheiden — sie wird deshalb auch genau so beschriftet. + """ + return sum(1 for zeile in inhalt.splitlines() if _DISC_ZEILE.match(zeile)) + + +def keydb_pruefen(inhalt: str) -> str: + """Prueft hochgeladenen Inhalt; gibt deutschen Fehlertext oder "" zurueck. + + Verhindert den haeufigsten Bedienfehler: statt der KEYDB.cfg landet die + HTML-Fehlerseite eines Downloads oder eine leere Datei im Verzeichnis — + MakeMKV wuerde dann still weiter "volume key is unknown" melden. + """ + if not inhalt.strip(): + return "Die Datei ist leer." + if len(inhalt.encode("utf-8")) > MAX_KEYDB_BYTES: + return ( + "Die Datei ist groesser als " + f"{MAX_KEYDB_BYTES // (1024 * 1024)} MB — das ist keine KEYDB.cfg." + ) + if inhalt.lstrip()[:1] == "<": + return ( + "Das sieht nach HTML aus, nicht nach einer KEYDB.cfg — " + "vermutlich wurde eine Fehlerseite statt der Datei geladen." + ) + if zaehle_disc_eintraege(inhalt) == 0: + return ( + "Keine einzige Zeile mit Disc-Kennung gefunden (40 Hex-Zeichen, " + "dann ein Gleichheitszeichen). Das ist keine KEYDB.cfg." + ) + return "" + + +def keydb_status(daten_dir: str = None) -> dict: + """Zustand der KEYDB.cfg. Fehlt sie, ist das der Normalfall, kein Fehler.""" + pfad = keydb_pfad(daten_dir) + try: + angaben = os.stat(pfad) + except OSError: + return { + "vorhanden": False, + "pfad": pfad, + "groesse_bytes": 0, + "eintraege": 0, + "geaendert": "", + } + eintraege = 0 + try: + with open(pfad, encoding="utf-8", errors="replace") as datei: + eintraege = zaehle_disc_eintraege(datei.read()) + except OSError: + pass # Datei da, aber unlesbar: Groesse/Datum stimmen trotzdem + return { + "vorhanden": True, + "pfad": pfad, + "groesse_bytes": angaben.st_size, + "eintraege": eintraege, + "geaendert": _iso(angaben.st_mtime), + } + + +def keydb_schreiben(inhalt: str, daten_dir: str = None) -> dict: + """Schreibt die KEYDB.cfg atomar und meldet den neuen Zustand. + + Erst in eine Nebendatei, dann os.replace: waehrend ein Rip laeuft, darf + makemkvcon niemals eine halb geschriebene Datei zu sehen bekommen. + """ + pfad = keydb_pfad(daten_dir) + os.makedirs(os.path.dirname(pfad), exist_ok=True) + neben = pfad + ".neu" + try: + with open(neben, "w", encoding="utf-8", newline="\n") as datei: + datei.write(inhalt) + os.replace(neben, pfad) + except OSError: + # Die Nebendatei nie liegen lassen: eine halb geschriebene + # KEYDB.cfg.neu verwirrt jeden, der ins Verzeichnis schaut, und + # belegt im Extremfall 64 MB, die niemand mehr aufräumt. + try: + os.remove(neben) + except OSError: + pass + raise + return keydb_status(daten_dir) + + +def keydb_loeschen(daten_dir: str = None) -> dict: + """Entfernt die KEYDB.cfg (z. B. nach einem Fehlgriff beim Hochladen). + + Nur "Datei war schon weg" wird geschluckt — das ist das gewünschte + Ergebnis. Jeder andere Fehler (schreibgeschützter Mount, fremder + Eigentümer) MUSS nach oben durch: sonst meldete die API einen Erfolg, + den es nicht gab, und die Datei wirkte beim nächsten Rip weiter. + """ + try: + os.remove(keydb_pfad(daten_dir)) + except FileNotFoundError: + pass + return keydb_status(daten_dir) + + +def ist_aacs_dump(name: str) -> bool: + """Dateiname eines AACS-Dumps? (pure Funktion, testbar) + + MakeMKV legt ihn als .tgz direkt im Datenverzeichnis ab + (Meldung 3332). Pfadtrenner und fuehrende Punkte werden hier schon + ausgeschlossen, damit der Download-Endpunkt keine Pfad-Tricks erlaubt. + """ + return ( + name.endswith(".tgz") + and "/" not in name + and "\\" not in name + and not name.startswith(".") + ) + + +def dumps_auflisten(daten_dir: str = None) -> list: + """Alle AACS-Dumps im Datenverzeichnis, neueste zuerst.""" + ordner = daten_dir or DATEN_DIR + try: + namen = os.listdir(ordner) + except OSError: + return [] + liste = [] + for name in namen: + if not ist_aacs_dump(name): + continue + try: + angaben = os.stat(os.path.join(ordner, name)) + except OSError: + continue + liste.append( + { + "name": name, + "groesse_bytes": angaben.st_size, + "geaendert": _iso(angaben.st_mtime), + "_sort": angaben.st_mtime, + } + ) + liste.sort(key=lambda eintrag: eintrag["_sort"], reverse=True) + for eintrag in liste: + del eintrag["_sort"] + return liste + + +def settings_conf_zusammenfuehren(inhalt: str, key: str) -> str: + """app_Key setzen, ohne den Rest der settings.conf zu verlieren (pure). + + Bis zum 25.07.2026 haben entrypoint.sh UND tasks.py die Datei komplett + ueberschrieben. Mit dem jetzt persistenten Datenverzeichnis waere damit + bei jedem Containerstart und vor jedem Rip alles andere weg — z. B. + app_UpdateEnable. Leerer Key laesst die vorhandene Zeile ebenfalls fallen, + damit ein bewusst geleerter Key nicht heimlich weiterwirkt. + """ + zeilen = [z for z in inhalt.splitlines() if not z.lstrip().startswith("app_Key")] + if key: + zeilen.append('app_Key = "{}"'.format(key)) + text = "\n".join(zeilen).strip("\n") + return text + "\n" if text else "" diff --git a/docker/worker/ripping.py b/docker/worker/ripping.py index ddefa2e..b843971 100644 --- a/docker/worker/ripping.py +++ b/docker/worker/ripping.py @@ -114,7 +114,7 @@ def lies_titel_info(device_path: str, timeout: int = 300) -> list: def rip_titel_auswahl(device_path: str, output_dir: str, titel_liste: list, - progress_cb=None) -> dict: + progress_cb=None, log_cb=None) -> dict: """Rippt GENAU die gewählten Titel (makemkvcon kann pro Aufruf nur einen Titel oder 'all' — also ein Aufruf je Titel, Fortschritt anteilig).""" gesamt = len(titel_liste) @@ -124,7 +124,8 @@ def rip_titel_auswahl(device_path: str, output_dir: str, titel_liste: list, if progress_cb: progress_cb(int((_i * 100 + p) / gesamt)) - ergebnis = run_makemkv(device_path, output_dir, progress_cb=anteilig, titel=str(nr)) + ergebnis = run_makemkv(device_path, output_dir, progress_cb=anteilig, titel=str(nr), + log_cb=log_cb) if ergebnis.get("status") == "cancelled": return ergebnis if ergebnis.get("status") != "success": @@ -181,6 +182,27 @@ def lies_datei_dauer(pfad: str, timeout: int = 120) -> int: return parse_scan_dauer((ergebnis.stdout or "") + (ergebnis.stderr or "")) +_MSG_RE = re.compile(r'^MSG:(\d+),\d+,\d+,"((?:[^"\\]|\\.)*)"') + + +def parse_msg(zeile: str): + """MSG-Zeile -> (code, klartext) oder None (pure Funktion, testbar). + + Format laut https://www.makemkv.com/developers/usage.txt: + MSG:code,flags,count,"message","format","param0",... — Feld 4 ist der + fertig zusammengesetzte Klartext. + + Bis zum 25.07.2026 stand hier line.split(",", 4)[3]: das schnitt JEDE + Meldung ab, die selbst ein Komma enthaelt — und MakeMKV schreibt solche + laufend ("Title #1 has length of 12 seconds, which is less than ..."). + Deshalb eine Regex, die die Anfuehrungszeichen respektiert. + """ + treffer = _MSG_RE.match(zeile.strip()) + if not treffer: + return None + return int(treffer.group(1)), treffer.group(2).replace('\\"', '"') + + def get_progress_from_prgv(line: str) -> int: """Extrahiert Gesamt-Fortschritt (0-100) aus einer PRGV-Zeile. @@ -297,8 +319,17 @@ def write_abcde_config(output_dir: str) -> str: return tmp.name -def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str = "all") -> dict: - """Rippt eine DVD/Blu-ray verlustfrei mit makemkvcon; meldet Fortschritt.""" +def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str = "all", + log_cb=None) -> dict: + """Rippt eine DVD/Blu-ray verlustfrei mit makemkvcon; meldet Fortschritt. + + log_cb(code, text) bekommt JEDE MakeMKV-Meldung. Bewusst ein eigener + Kanal statt progress_cb: der Fortschritts-Callback in tasks.py verwirft + Aufrufe, bei denen sich die Prozentzahl nicht geaendert hat — Meldungen + waeren dort also grossteils verschwunden. Ohne diesen Kanal war am + 25.07.2026 nicht von aussen erkennbar, dass MakeMKV bei der UHD-Disc + nicht einmal versucht, einen Schluessel zu holen (siehe makemkv_daten). + """ if not check_makemkv_installed(): return {"status": "error", "error": "makemkvcon ist nicht installiert"} @@ -328,15 +359,23 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str try: for line in process.stdout: progress = get_progress_from_prgv(line) - if progress >= 0 and progress_cb: - progress_cb(progress) - elif line.startswith("MSG:"): - # MSG:code,flags,count,"message",... — Klartext ist Feld 4 - teile = line.split(",", 4) - if len(teile) >= 4: - letzte_meldung = teile[3].strip('"') - if any(muster in letzte_meldung for muster in KRITISCH): - kritische_meldungen.append(letzte_meldung) + if progress >= 0: + if progress_cb: + progress_cb(progress) + continue + meldung = parse_msg(line) + if meldung is None: + continue + code, letzte_meldung = meldung + if any(muster in letzte_meldung for muster in KRITISCH): + kritische_meldungen.append(letzte_meldung) + if log_cb: + try: + log_cb(code, letzte_meldung) + except RipAbbruch: + raise + except Exception: + pass # Protokollieren darf einen laufenden Rip nie beenden except RipAbbruch: process.kill() process.wait() @@ -373,7 +412,7 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_cb=None, output_dir: str = None, nur_hauptfilm: bool = False, - titel_liste: list = None) -> dict: + titel_liste: list = None, log_cb=None) -> dict: """Rippt eine DVD oder Blu-ray verlustfrei mit MakeMKV. Bewusst KEIN eigener Celery-Task: der einzige Task ist worker.tasks.rip_disc, @@ -388,7 +427,7 @@ def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_c if titel_liste: os.makedirs(output_dir, exist_ok=True) - return rip_titel_auswahl(device_path, output_dir, titel_liste, progress_cb) + return rip_titel_auswahl(device_path, output_dir, titel_liste, progress_cb, log_cb) titel = "all" if nur_hauptfilm: @@ -397,7 +436,8 @@ def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_c if haupt is not None: titel = str(haupt) # Kein Titel ermittelbar → ehrlich auf 'all' zurückfallen statt raten - return run_makemkv(device_path, output_dir, progress_cb=progress_cb, titel=titel) + return run_makemkv(device_path, output_dir, progress_cb=progress_cb, titel=titel, + log_cb=log_cb) def rip_cd(device_path: str, disc_id: str, progress_cb=None, output_dir: str = None) -> dict: diff --git a/docker/worker/tasks.py b/docker/worker/tasks.py index 3ba9124..a95e0a6 100644 --- a/docker/worker/tasks.py +++ b/docker/worker/tasks.py @@ -20,6 +20,7 @@ import shutil import requests import db +import makemkv_daten import medien import notify 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 (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() if not key: return - ordner = os.path.expanduser("~/.MakeMKV") + pfad = os.path.join(makemkv_daten.DATEN_DIR, "settings.conf") try: - os.makedirs(ordner, exist_ok=True) - with open(os.path.join(ordner, "settings.conf"), "w") as f: - f.write(f'app_Key = "{key}"\n') + os.makedirs(makemkv_daten.DATEN_DIR, exist_ok=True) + try: + 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: 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) + # 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() ist_video = disc_type in ("dvd", "bluray", "uhd") 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, nur_hauptfilm=nur_hauptfilm, titel_liste=titel_liste, + log_cb=melde_makemkv, ) else: 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, nur_hauptfilm=nur_hauptfilm, titel_liste=titel_liste, + log_cb=melde_makemkv, ) - # UHD-Fehler in Klartext übersetzen — "Failed to open disc" allein hilft - # niemandem. Zwei bekannte Ursachen (Befunde 24.07., Summer-Wars-UHD): - if ergebnis.get("status") == "error" and disc_type == "uhd": + # MakeMKV-Fehler in Klartext übersetzen — "Failed to open disc" allein + # hilft niemandem. + if ergebnis.get("status") == "error": fehler_text = ergebnis.get("error") or "" if "volume key is unknown" in fehler_text: - # LibreDrive lief bereits — MakeMKV kennt nur den Disc-Schlüssel - # nicht: Version zu alt ODER Disc neuer als die Key-Datenbank. + # Befund 25.07.2026, im Worker nachgemessen (Akira UHD, MKB v76, + # 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"] += ( - " — Klartext: Das Laufwerk liest die Disc (LibreDrive OK), " - "aber MakeMKV kennt den Schlüssel dieser Disc nicht. Erst " - "prüfen: MakeMKV aktuell? (Einstellungen → System; Update = " - "Image-Rebuild). Ist es aktuell, ist die Disc neuer als die " - "Schlüssel-Datenbank — MakeMKV hat einen AACS-Dump unter " - "/root/.MakeMKV/ im Worker gespeichert; im MakeMKV-Forum " - "(Bereich 'Ultra HD Blu-ray') einreichen, mit einem der " - "nächsten Updates ist die Disc dann rippbar." + " — Klartext: Laufwerk und Rippy sind in Ordnung, MakeMKV " + "liest die Disc. Es kennt nur den Schlüssel dieser Pressung " + "nicht und holt ihn auch nicht mehr online nach — MakeMKVs " + "Schlüssel-Kanal liefert nichts mehr (am 25.07.2026 im " + "Worker nachgemessen). Abhilfe: eine KEYDB.cfg unter " + "Einstellungen → System hochladen; sie wirkt ab dem nächsten " + "Rip. " + + ( + "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." + ) + + " 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 "Failed to open disc" in fehler_text: + elif disc_type == "uhd" and "Failed to open disc" in fehler_text: ergebnis["error"] += ( " — 4K-UHD erkannt: Das Laufwerk kann UHD-Discs vermutlich " "nicht entschlüsseln. Dafür ist eine LibreDrive-Firmware nötig " diff --git a/docker/worker/test_makemkv_daten_worker.py b/docker/worker/test_makemkv_daten_worker.py new file mode 100644 index 0000000..51f0cdc --- /dev/null +++ b/docker/worker/test_makemkv_daten_worker.py @@ -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 = "\n

404 Not Found

\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("", "") == "" diff --git a/docker/worker/test_ripping_helpers.py b/docker/worker/test_ripping_helpers.py index 8479a5d..d927a8c 100644 --- a/docker/worker/test_ripping_helpers.py +++ b/docker/worker/test_ripping_helpers.py @@ -13,6 +13,7 @@ from ripping import ( build_makemkv_cmd, get_progress_from_line, get_progress_from_prgv, + parse_msg, write_abcde_config, ) @@ -63,6 +64,60 @@ def test_prgv_parsing_ignoriert_fremde_zeilen(): 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(): """Review-Fund 22.07.: '-o' stand doppelt (Format UND Verzeichnis) — abcde parste das Verzeichnis als Format, CD-Ripping war nie funktionsfähig."""