feat(uhd): KEYDB.cfg-Unterstuetzung - MakeMKVs Schluessel-Kanal liefert nichts mehr
Ampel / ampel (push) Successful in 28s
Ampel / ampel (push) Successful in 28s
4K-UHD scheiterte an "The volume key is unknown for this disc". Am 25.07.2026
im Worker-Container nachgemessen: Laufwerk und MakeMKV sind in Ordnung
(LibreDrive v06.3 / Meldung 1011, Disc wird gelesen, AACS-Dump geschrieben) -
MakeMKV versucht gar nicht erst, online einen Schluessel zu holen. Belege:
_private_data.tar enthaelt nur die Index-Datei und KEINE hkd_*.bin; auch mit
geloeschter update.conf (Meldung 5074 belegt den Web-Kontakt) und mit
app_UpdateEnable="1" kam keiner; hkdata.fairuse.org und hkdata.crabdance.com
loesen weltweit nicht mehr auf (NXDOMAIN gegen Fritz!Box, 8.8.8.8, 1.1.1.1).
Betroffen war Akira UHD (MKB v76, Pressung Dez. 2020) - also gerade KEINE
Neuerscheinung. Der bisherige Fehlertext ("Disc neuer als die
Schluessel-Datenbank, mit einem der naechsten Updates rippbar") war falsch.
Einziger heute funktionierender Weg ist eine vom Nutzer selbst mitgebrachte
KEYDB.cfg. Rippy liefert KEINE Schluessel mit, laedt keine herunter und
verteilt keine - es stellt nur den Platz bereit und zeigt an, was dort liegt.
- Datenverzeichnis persistent gemountet (MAKEMKV_DATA_HOST, Default
/srv/rippy/makemkv): KEYDB.cfg und AACS-Dumps ueberleben jeden Rebuild.
Vorher loeschte jeder "up -d --build" beides - inklusive des Dumps, auf den
die Fehlermeldung selbst verwies.
- entrypoint.sh und tasks.py schreiben settings.conf ergaenzend statt
zerstoerend. Der entrypoint bricht bei nicht beschreibbarem Verzeichnis
nicht mehr ab - mit "restart: unless-stopped" waere das ein Crashloop
gewesen, in dem auch reines DVD-Rippen tot ist.
- Neues Zwillings-Modul makemkv_daten.py (docker/api + docker/worker,
byteweise identisch; test_zwillinge_sind_byteweise_identisch wacht darueber
und wurde durch absichtliches Verstellen als wirksam nachgewiesen).
- API: GET/POST/DELETE /system/keydb, GET /system/aacs-dumps(/{dateiname}).
JSON-Body statt Multipart - python-multipart ist bewusst nicht installiert
und wuerde die API beim Import toeten. nginx client_max_body_size 64m,
sonst scheitert der Upload mit 413, bevor die API ihn sieht.
- UI (Einstellungen -> System): Status, Hochladen per Datei-Dialog, Entfernen,
Dump-Download, KEYDB-Plakette je Worker (nur wo das Verzeichnis wirklich
gemountet ist - ein Remote-Transcode-Worker truege sonst eine Warnung,
die ihn nichts angeht).
- parse_msg() + log_cb: MakeMKV-Meldungen landen im Rippy-Log (gedrosselt:
Code 1003 raus, keine Wiederholungen, max. 40 je Rip). Nebenbei behoben:
der alte Parser (split(",", 4)[3]) schnitt jede Meldung am ersten Komma ab.
- Fuenf Stellen richtiggestellt, die behaupteten, MakeMKV-Updates braechten
die neueste Disc-Schluessel-Datenbank mit (UI, Anleitung, README,
Worker-Dockerfile, makemkv_key.py).
NICHT bewiesen: ein erfolgreicher UHD-Rip - es lag keine KEYDB.cfg mit dem
Akira-Schluessel vor. Belegt sind der Befund und die neue Mechanik. So steht
es auch im SAVEPOINT und in der ROADMAP.
Quellen (AGENTS Regel D):
- Datenverzeichnis + Dateiname GROSS/case-sensitiv:
https://forum.makemkv.com/forum/viewtopic.php?t=30636
- hkd_*.bin in _private_data.tar:
https://forum.makemkv.com/forum/viewtopic.php?t=32675
- headless settings.conf / app_UpdateEnable:
https://forum.makemkv.com/forum/viewtopic.php?t=20364
- KEYDB.cfg-Zeilenformat (libaacs):
https://github.com/ShiftMediaProject/libaacs/blob/master/KEYDB.cfg
- MSG-/PRGV-Format: https://www.makemkv.com/developers/usage.txt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,13 @@ OMDB_API_KEY=
|
|||||||
# nächsten Rip, ohne Neustart, und schlägt diesen Env-Wert).
|
# nächsten Rip, ohne Neustart, und schlägt diesen Env-Wert).
|
||||||
MAKEMKV_APP_KEY=
|
MAKEMKV_APP_KEY=
|
||||||
|
|
||||||
|
# MakeMKV-Datenverzeichnis auf dem HOST (bleibt über Rebuilds hinweg erhalten).
|
||||||
|
# Hier hinein gehört die KEYDB.cfg für 4K-UHD-Discs; hier landen auch die
|
||||||
|
# AACS-Dumps fehlgeschlagener Discs. Beides ist ab Einstellungen → System
|
||||||
|
# im Browser erreichbar — dieser Pfad ist nur für den Fall, dass du die
|
||||||
|
# Dateien direkt auf der Maschine anfassen willst.
|
||||||
|
#MAKEMKV_DATA_HOST=/srv/rippy/makemkv
|
||||||
|
|
||||||
# MakeMKV-Version fürs Worker-Image (Einstellungen → System meldet Updates).
|
# MakeMKV-Version fürs Worker-Image (Einstellungen → System meldet Updates).
|
||||||
# Update: Version hier anheben, dann auf der Rippy-Maschine
|
# Update: Version hier anheben, dann auf der Rippy-Maschine
|
||||||
# docker compose build worker && docker compose up -d worker
|
# docker compose build worker && docker compose up -d worker
|
||||||
|
|||||||
@@ -55,10 +55,13 @@ Bibliotheks-APIs: `--help`/Doku prüfen und die Fundstelle im Commit nennen.
|
|||||||
- Kein Proxmox-LXC-Nesting-Workaround — das Projekt lebt bewusst in normalem Docker.
|
- Kein Proxmox-LXC-Nesting-Workaround — das Projekt lebt bewusst in normalem Docker.
|
||||||
- Kein ARM-Fork — Rippy ist ein Eigenbau von Grund auf.
|
- Kein ARM-Fork — Rippy ist ein Eigenbau von Grund auf.
|
||||||
|
|
||||||
## Aktueller Stand (24.07.2026)
|
## Aktueller Stand (25.07.2026)
|
||||||
|
|
||||||
- ✅ **E2E bewiesen (v3.1):** BD-50 komplett durch die Kette (43 GB → 4,8 GB)
|
- ✅ **E2E bewiesen (v3.1):** BD-50 komplett durch die Kette (43 GB → 4,8 GB)
|
||||||
- ✅ **Etappe 13 (v3.2):** Universal-Komfort-Runde — Media-Server-Integration,
|
- ✅ **Etappe 13 (v3.2):** Universal-Komfort-Runde — Media-Server-Integration,
|
||||||
echte Benachrichtigungen, SMB-Klartext-Fehler, UHD-Arbeitsverzeichnis,
|
echte Benachrichtigungen, SMB-Klartext-Fehler, UHD-Arbeitsverzeichnis,
|
||||||
MakeMKV-Key via UI, Job-Detail-Popup, Toast-Feedback, Ampel-Blocker behoben
|
MakeMKV-Key via UI, Job-Detail-Popup, Toast-Feedback, Ampel-Blocker behoben
|
||||||
|
- ✅ **Etappe 17 (v3.10):** 4K-UHD-Disc-Schlüssel — persistentes
|
||||||
|
MakeMKV-Datenverzeichnis + `KEYDB.cfg` im UI (der Nachweis mit einer
|
||||||
|
echten Schlüssel-Datei steht noch aus)
|
||||||
- 📝 **Details immer in SAVEPOINT.md** — diese Sektion nennt nur die Etappe
|
- 📝 **Details immer in SAVEPOINT.md** — diese Sektion nennt nur die Etappe
|
||||||
|
|||||||
+16
@@ -127,6 +127,7 @@ Ein modular aufgebautes System, das bei Disc-Einwurf automatisch den Typ erkennt
|
|||||||
| Risiko | Status | Behandlung |
|
| Risiko | Status | Behandlung |
|
||||||
|--------|--------|------------|
|
|--------|--------|------------|
|
||||||
| MakeMKV-Beta-Key-Management | Gelöst | Key-Erneuerung als Cron-Job; DMCA-Ausnahme in DE |
|
| MakeMKV-Beta-Key-Management | Gelöst | Key-Erneuerung als Cron-Job; DMCA-Ausnahme in DE |
|
||||||
|
| **Disc-Schlüssel für 4K-UHD (AACS 2.0)** | **Offen — durch Rippy nicht lösbar** | Am 25.07.2026 auf der VM gemessen: MakeMKV holt für unbekannte UHD-Discs keinen Schlüssel mehr (kein Netz-Versuch im Log, keine `hkd_*.bin` im `_private_data.tar`), die dokumentierten Schlüssel-Server sind weltweit tot. Rippy stellt NUR ein persistentes Datenverzeichnis für eine vom Nutzer selbst mitgebrachte `KEYDB.cfg` bereit und gibt die AACS-Dumps heraus — es liefert, lädt und verteilt keine Schlüssel. Siehe §10 (25.07.2026) |
|
||||||
| LXC-Device-Node-Änderungen | Gelöst | udev-Resolver (UUID/Serial) |
|
| LXC-Device-Node-Änderungen | Gelöst | udev-Resolver (UUID/Serial) |
|
||||||
| Pending-Queue / State-Manager | Gelöst | Redis mit AOF-Persistence |
|
| Pending-Queue / State-Manager | Gelöst | Redis mit AOF-Persistence |
|
||||||
| Hybrid-Discs | Offen | MVP erkennt nur Standard; als "Kann" notiert |
|
| Hybrid-Discs | Offen | MVP erkennt nur Standard; als "Kann" notiert |
|
||||||
@@ -182,6 +183,21 @@ Ein modular aufgebautes System, das bei Disc-Einwurf automatisch den Typ erkennt
|
|||||||
passlib/bcrypt-Abhängigkeit hat die CI-Ampel gebrochen. Rate-Limiting
|
passlib/bcrypt-Abhängigkeit hat die CI-Ampel gebrochen. Rate-Limiting
|
||||||
(pro IP) bleibt. Wer Rippy je nach außen öffnet, stellt einen
|
(pro IP) bleibt. Wer Rippy je nach außen öffnet, stellt einen
|
||||||
Reverse-Proxy mit eigener Auth davor (z. B. Authelia/Caddy basicauth).
|
Reverse-Proxy mit eigener Auth davor (z. B. Authelia/Caddy basicauth).
|
||||||
|
- **25.07.2026 — 4K-UHD-Disc-Schlüssel: Rippy stellt Platz bereit, keine
|
||||||
|
Schlüssel:** Das Muss-Feature „MakeMKV-Ripping (lossless)" bleibt
|
||||||
|
unverändert; ergänzt wird nur ein **persistentes MakeMKV-Datenverzeichnis**
|
||||||
|
(`MAKEMKV_DATA_HOST`, im Worker `/root/.MakeMKV`, in der API
|
||||||
|
`/app/makemkv-data`) samt Bedienung im UI. Begründung: Am 25.07.2026 auf
|
||||||
|
der VM nachgemessen — MakeMKV versucht bei einer unbekannten UHD-Disc gar
|
||||||
|
keinen Online-Abruf mehr, und die früher genutzten Schlüssel-Server sind
|
||||||
|
weltweit tot; der einzige heute funktionierende Weg ist eine `KEYDB.cfg`
|
||||||
|
im Datenverzeichnis. **Rippy liefert und verteilt KEINE Disc-Schlüssel und
|
||||||
|
lädt auch keine herunter** — es hält nur den Platz für eine Datei bereit,
|
||||||
|
die der Nutzer selbst mitbringt, zeigt ehrlich an, was dort liegt, und gibt
|
||||||
|
die AACS-Dumps heraus, die MakeMKV ohnehin selbst schreibt. Das ist genau
|
||||||
|
die Grenze, die `docker/api/makemkv_key.py` in Zeile 14 zieht: die
|
||||||
|
kostenlose Beta-LIZENZ der Software ist etwas anderes als das
|
||||||
|
Entschlüsseln oder Verteilen von Disc-Schlüsseln.
|
||||||
- **24.07.2026 — Serien-Flow:** Staffel-Ablage <Serie>/Season NN plus
|
- **24.07.2026 — Serien-Flow:** Staffel-Ablage <Serie>/Season NN plus
|
||||||
Episoden-Zuordnung per Laufzeitabgleich (TMDB) — erfüllt Etappe-12-Ziel
|
Episoden-Zuordnung per Laufzeitabgleich (TMDB) — erfüllt Etappe-12-Ziel
|
||||||
„Serien-Episoden-Erkennung" in der ersten Ausbaustufe (nur bei
|
„Serien-Episoden-Erkennung" in der ersten Ausbaustufe (nur bei
|
||||||
|
|||||||
@@ -74,9 +74,26 @@ NICHT als emuliertes CD-ROM (`media=cdrom`), das kann keine SCSI-Kommandos.
|
|||||||
nur die Benennung. **Jellyfin/Emby**: Server-URL + API-Key eintragen,
|
nur die Benennung. **Jellyfin/Emby**: Server-URL + API-Key eintragen,
|
||||||
dann stößt Rippy nach jedem fertigen Rip sofort einen Bibliotheks-Scan
|
dann stößt Rippy nach jedem fertigen Rip sofort einen Bibliotheks-Scan
|
||||||
an — Disc rein, Film erscheint im Server.
|
an — Disc rein, Film erscheint im Server.
|
||||||
5. **4K-UHD**: braucht ein LibreDrive-fähiges Laufwerk (MakeMKV-Forum:
|
5. **4K-UHD**: braucht **zwei** Dinge — ein LibreDrive-fähiges Laufwerk
|
||||||
„Ultimate UHD Drives Flashing Guide"). Normale BD/DVD gehen mit jedem
|
(MakeMKV-Forum: „Ultimate UHD Drives Flashing Guide") **und** einen
|
||||||
Laufwerk. ⚠️ UHD-Rohdaten sind bis 100 GB groß — wenn die Platte der
|
passenden Disc-Schlüssel. Normale BD/DVD gehen mit jedem Laufwerk und
|
||||||
|
ohne Schlüssel-Datei.
|
||||||
|
**Der Schlüssel ist heute die eigentliche Hürde.** Am 25.07.2026 auf
|
||||||
|
der Rippy-VM gemessen (Akira UHD, MKB v76, Pressung Dezember 2020):
|
||||||
|
Laufwerk und MakeMKV waren in Ordnung — „Using LibreDrive mode
|
||||||
|
(v06.3)", die Disc wurde gelesen — und trotzdem kam „The volume key is
|
||||||
|
unknown for this disc". MakeMKV versucht dabei **gar nicht mehr**,
|
||||||
|
online einen Schlüssel zu holen, und die früher genutzten
|
||||||
|
Schlüssel-Server (`hkdata.fairuse.org`, `hkdata.crabdance.com`) lösen
|
||||||
|
weltweit nicht mehr auf. Ein MakeMKV-Update ändert daran nichts.
|
||||||
|
Der einzige Weg, der heute funktioniert, ist eine Datei **`KEYDB.cfg`**
|
||||||
|
(GROSS geschrieben — Linux unterscheidet Groß- und Kleinschreibung) im
|
||||||
|
MakeMKV-Datenverzeichnis. Die legst du unter **Einstellungen → System**
|
||||||
|
ab (siehe unten).
|
||||||
|
⚠️ **Rippy liefert keine Schlüssel mit, lädt keine herunter und
|
||||||
|
verteilt keine.** Rippy stellt nur den Platz für eine Datei bereit, die
|
||||||
|
du selbst mitbringst, und zeigt dir ehrlich an, was dort liegt.
|
||||||
|
⚠️ UHD-Rohdaten sind bis 100 GB groß — wenn die Platte der
|
||||||
Rippy-Maschine dafür zu klein ist, lege das **Arbeitsverzeichnis**
|
Rippy-Maschine dafür zu klein ist, lege das **Arbeitsverzeichnis**
|
||||||
(Einstellungen → Verarbeitung) auf eine eingehängte Freigabe, z. B.
|
(Einstellungen → Verarbeitung) auf eine eingehängte Freigabe, z. B.
|
||||||
`/app/media/nas-arbeit`. Rippy bricht sonst VOR dem Rip mit einer
|
`/app/media/nas-arbeit`. Rippy bricht sonst VOR dem Rip mit einer
|
||||||
@@ -106,7 +123,7 @@ fehlgeschlagen, abgebrochen) und erkennt den Dienst an der URL selbst:
|
|||||||
| ntfy (Handy-Push) | `https://ntfy.sh/mein-geheimes-thema` | Roh-Text + Titel |
|
| ntfy (Handy-Push) | `https://ntfy.sh/mein-geheimes-thema` | Roh-Text + Titel |
|
||||||
| Eigenes (HA, n8n, …) | beliebige HTTPS-URL | `{"title","message","level"}` |
|
| Eigenes (HA, n8n, …) | beliebige HTTPS-URL | `{"title","message","level"}` |
|
||||||
|
|
||||||
## System, MakeMKV-Beta-Key & Updates
|
## System, MakeMKV-Beta-Key, Disc-Schlüssel & Updates
|
||||||
|
|
||||||
**Einstellungen → System** zeigt die Werkzeug-Versionen jedes Workers
|
**Einstellungen → System** zeigt die Werkzeug-Versionen jedes Workers
|
||||||
(MakeMKV, HandBrake), den Key-Status und den freien Speicherplatz. Der
|
(MakeMKV, HandBrake), den Key-Status und den freien Speicherplatz. Der
|
||||||
@@ -114,11 +131,37 @@ MakeMKV-Beta-Key (wechselt ~monatlich, Forum-Thread t=1053) wird hier im
|
|||||||
UI eingetragen und gilt ab dem **nächsten Rip** — ohne Rebuild, ohne
|
UI eingetragen und gilt ab dem **nächsten Rip** — ohne Rebuild, ohne
|
||||||
Neustart; er schlägt den Key aus der `.env`.
|
Neustart; er schlägt den Key aus der `.env`.
|
||||||
|
|
||||||
|
**Disc-Schlüssel (`KEYDB.cfg`)** — im selben Tab, nur für 4K-UHD nötig:
|
||||||
|
|
||||||
|
- **Status**: Rippy zeigt, ob eine `KEYDB.cfg` im MakeMKV-Datenverzeichnis
|
||||||
|
liegt, mit Pfad, Größe, Anzahl der Disc-Einträge und Änderungsdatum.
|
||||||
|
- **Hochladen**: Knopf „KEYDB.cfg hochladen", dann deine eigene Datei im
|
||||||
|
Datei-Dialog auswählen — mehr ist nicht zu tun. Rippy prüft den Inhalt
|
||||||
|
auf Plausibilität und lehnt Unsinn (leere Datei, versehentlich geladene
|
||||||
|
HTML-Fehlerseite) mit einer deutschen Klartext-Meldung ab, statt ihn
|
||||||
|
stillschweigend zu speichern.
|
||||||
|
- **Entfernen**: ein Knopf, die Datei ist wieder weg.
|
||||||
|
- **AACS-Dumps herunterladen**: MakeMKV legt bei jeder unbekannten Disc
|
||||||
|
selbst einen Dump ab. Genau den braucht man, wenn man im MakeMKV-Forum
|
||||||
|
um den Schlüssel für eine neue Pressung bittet — hier holst du ihn dir
|
||||||
|
aus dem Container, ohne SSH.
|
||||||
|
|
||||||
|
Der Beta-Key ist die **Lizenz für die Software**; die `KEYDB.cfg` sind
|
||||||
|
**Disc-Schlüssel** — zwei völlig verschiedene Dinge. **Rippy liefert und
|
||||||
|
lädt keine Disc-Schlüssel**, es hält nur ein persistentes Verzeichnis
|
||||||
|
dafür bereit (`MAKEMKV_DATA_HOST`, siehe Tabelle unten) — rebuild-fest,
|
||||||
|
damit deine Datei einen `docker compose build` überlebt.
|
||||||
|
|
||||||
**Updates:** „Auf Updates prüfen" vergleicht mit makemkv.com und den
|
**Updates:** „Auf Updates prüfen" vergleicht mit makemkv.com und den
|
||||||
offiziellen HandBrake-Releases (12 h gecacht). MakeMKV-Update ohne
|
offiziellen HandBrake-Releases (12 h gecacht). MakeMKV-Update ohne
|
||||||
Code-Änderung: `MAKEMKV_VERSION=x.y.z` in die `.env`, dann
|
Code-Änderung: `MAKEMKV_VERSION=x.y.z` in die `.env`, dann
|
||||||
`docker compose build worker && docker compose up -d worker` — neue
|
`docker compose build worker && docker compose up -d worker`.
|
||||||
Versionen bringen auch die neueste Disc-Schlüssel-Datenbank mit.
|
⚠️ **Richtigstellung (25.07.2026):** Hier stand früher, neue MakeMKV-
|
||||||
|
Versionen brächten „auch die neueste Disc-Schlüssel-Datenbank mit". Das
|
||||||
|
ist widerlegt — auf der VM nachgemessen: MakeMKV holt für eine unbekannte
|
||||||
|
UHD-Disc keinen Schlüssel mehr, weder mitgeliefert noch aus dem Netz. Ein
|
||||||
|
Update lohnt für Programmfehler und neue Laufwerke, hilft aber NICHT
|
||||||
|
gegen „The volume key is unknown". Dafür brauchst du die `KEYDB.cfg`.
|
||||||
HandBrake kommt im Docker-Worker bewusst aus Debian (stabil, hinkt der
|
HandBrake kommt im Docker-Worker bewusst aus Debian (stabil, hinkt der
|
||||||
offiziellen Version hinterher); der native Windows-Worker nutzt die
|
offiziellen Version hinterher); der native Windows-Worker nutzt die
|
||||||
aktuelle Version direkt.
|
aktuelle Version direkt.
|
||||||
@@ -153,6 +196,7 @@ bleibt All-in-one.
|
|||||||
| `THETVDB_API_KEY` | optional | Serien-Fallback |
|
| `THETVDB_API_KEY` | optional | Serien-Fallback |
|
||||||
| `MAKEMKV_APP_KEY` | optional | MakeMKV-Beta-Key (Forum); DVDs gehen ohne — bequemer: im UI unter Einstellungen → System pflegen |
|
| `MAKEMKV_APP_KEY` | optional | MakeMKV-Beta-Key (Forum); DVDs gehen ohne — bequemer: im UI unter Einstellungen → System pflegen |
|
||||||
| `MAKEMKV_URL_BASE` | optional | alternative Download-Quelle für den Image-Build |
|
| `MAKEMKV_URL_BASE` | optional | alternative Download-Quelle für den Image-Build |
|
||||||
|
| `MAKEMKV_DATA_HOST` | optional | Host-Verzeichnis für MakeMKVs Daten (Default `/srv/rippy/makemkv`) — hier liegen deine `KEYDB.cfg` und die AACS-Dumps; persistent, überlebt jeden Rebuild |
|
||||||
| `OPTICAL_SR` | je Host | Host-Pfad des CD-ROM-Knotens (Default `/dev/sr0`) — siehe „Laufwerk anpassen" |
|
| `OPTICAL_SR` | je Host | Host-Pfad des CD-ROM-Knotens (Default `/dev/sr0`) — siehe „Laufwerk anpassen" |
|
||||||
| `OPTICAL_SG` | je Host | Host-Pfad des sg-Knotens des Laufwerks (Default `/dev/sg1`; oft `/dev/sg0`) |
|
| `OPTICAL_SG` | je Host | Host-Pfad des sg-Knotens des Laufwerks (Default `/dev/sg1`; oft `/dev/sg0`) |
|
||||||
| `POSTGRES_PASSWORD` | optional | DB-Passwort (Default `rippy`) — für exponierte Umgebungen ein starkes Passwort setzen |
|
| `POSTGRES_PASSWORD` | optional | DB-Passwort (Default `rippy`) — für exponierte Umgebungen ein starkes Passwort setzen |
|
||||||
@@ -181,6 +225,11 @@ Voraussetzungen auf dem Ziel-Host:
|
|||||||
```
|
```
|
||||||
(reboot-fest als systemd-`.mount`-Unit persistieren.) Braucht einen klassischen
|
(reboot-fest als systemd-`.mount`-Unit persistieren.) Braucht einen klassischen
|
||||||
Linux-Docker-Host mit `SYS_ADMIN` — nicht Docker-Desktop/rootless/Podman.
|
Linux-Docker-Host mit `SYS_ADMIN` — nicht Docker-Desktop/rootless/Podman.
|
||||||
|
4. MakeMKV-Datenverzeichnis anlegen: `mkdir -p /srv/rippy/makemkv` (oder
|
||||||
|
eigenen Pfad über `MAKEMKV_DATA_HOST` in der `.env`). Dort liegen
|
||||||
|
MakeMKVs Programmzustand, die AACS-Dumps und — falls du 4K-UHD rippst —
|
||||||
|
deine eigene `KEYDB.cfg`. Das Verzeichnis gehört dem Host, damit ein
|
||||||
|
`docker compose build` deine Datei nicht wegwirft.
|
||||||
|
|
||||||
Dann wie im Schnellstart: klonen, `.env` füllen, `docker compose up -d
|
Dann wie im Schnellstart: klonen, `.env` füllen, `docker compose up -d
|
||||||
--build`, `http://<host>` öffnen — der Einrichtungs-Assistent führt durch
|
--build`, `http://<host>` öffnen — der Einrichtungs-Assistent führt durch
|
||||||
|
|||||||
+59
@@ -414,6 +414,65 @@ irrelevant und kann komplett raus."
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Etappe 17 (25.07.2026): 4K-UHD-Schlüssel (KEYDB.cfg)
|
||||||
|
|
||||||
|
**Quelle:** Messung am 25.07. live auf der Rippy-VM im Worker-Container.
|
||||||
|
Eine 4K-UHD (Akira, MKB v76, Pressung Dezember 2020) scheitert an „The
|
||||||
|
volume key is unknown for this disc" — obwohl makemkvcon „Using LibreDrive
|
||||||
|
mode (v06.3)" und „Using direct disc access mode" meldet, die Disc liest
|
||||||
|
und den AACS-Dump ablegt (Meldung 3332). Das Debug-Log geht ohne einen
|
||||||
|
einzigen Netz-Versuch von „Loaded content hash table" direkt auf den
|
||||||
|
Fehler; `_private_data.tar` enthielt nur die Index-Datei und keine einzige
|
||||||
|
`hkd_*.bin`; auch mit gelöschter `update.conf` (Meldung 5074 belegt den
|
||||||
|
Web-Kontakt) und `app_UpdateEnable = "1"` kam kein Schlüssel; die
|
||||||
|
Forum-Schlüssel-Server `hkdata.fairuse.org` und `hkdata.crabdance.com`
|
||||||
|
lösen weltweit nicht mehr auf. **Fazit: Ein MakeMKV-Update löst das
|
||||||
|
nicht** (das behauptete v3.3 — dort richtiggestellt). Der einzige heute
|
||||||
|
funktionierende Weg ist eine `KEYDB.cfg` im MakeMKV-Datenverzeichnis.
|
||||||
|
|
||||||
|
**Gebaut:**
|
||||||
|
- [x] **Persistentes Datenverzeichnis**: `${MAKEMKV_DATA_HOST:-/srv/rippy/makemkv}`
|
||||||
|
vom Host gemountet — Worker `/root/.MakeMKV`, API `/app/makemkv-data`,
|
||||||
|
beide mit `MAKEMKV_DATA_DIR`. `KEYDB.cfg` und AACS-Dumps überleben
|
||||||
|
jeden Rebuild. `.env.example` erklärt die Variable.
|
||||||
|
- [x] **entrypoint.sh entschärft**: `settings.conf` wird ergänzt statt
|
||||||
|
überschrieben (der Beta-Key hatte sonst alles andere gelöscht),
|
||||||
|
`app_UpdateEnable = "1"` gesetzt.
|
||||||
|
- [x] **Zwillings-Modul `makemkv_daten.py`** (identisch in `docker/api/`
|
||||||
|
und `docker/worker/`) als einzige Wahrheit über das Verzeichnis:
|
||||||
|
Status lesen, Inhalt prüfen, atomar schreiben, löschen, Dumps
|
||||||
|
auflisten — alles reine Funktionen, damit die Ampel sie ohne
|
||||||
|
Postgres/Redis testen kann.
|
||||||
|
- [x] **API**: `GET/POST/DELETE /system/keydb`, `GET /system/aacs-dumps`
|
||||||
|
und `GET /system/aacs-dumps/{dateiname}`. Der Inhalt kommt bewusst
|
||||||
|
als JSON-Body — es gibt kein `python-multipart`, ein Endpunkt mit
|
||||||
|
`UploadFile`/`File()` würde die API beim Import töten.
|
||||||
|
- [x] **UI (Einstellungen → System)**: Status der `KEYDB.cfg` (Pfad,
|
||||||
|
Größe, Anzahl Disc-Einträge, Datum), Inhalt einfügen, entfernen,
|
||||||
|
AACS-Dumps herunterladen — ohne SSH auf die VM.
|
||||||
|
- [x] **MakeMKV redet endlich**: `parse_msg()` in `ripping.py` + Log-Callback
|
||||||
|
in `tasks.py` schreiben MakeMKV-Meldungen ins Rippy-Log (gedrosselt:
|
||||||
|
Code 1003 raus, keine Wiederholungen, max. 40 je Rip). UHD-Fehlertext
|
||||||
|
ehrlich neu geschrieben; `caps.py` meldet `keydb: ja | nein | unbekannt`.
|
||||||
|
- [x] **Doku nachgezogen**: README (UHD-Voraussetzungen, System-Panel,
|
||||||
|
`MAKEMKV_DATA_HOST`, Bereitstellung), SAVEPOINT v3.10 inkl.
|
||||||
|
Richtigstellung von v3.3, KONZEPT §8 + §10.
|
||||||
|
|
||||||
|
**Offen aus dieser Runde:**
|
||||||
|
- [ ] **Der Nachweis mit einer echten `KEYDB.cfg` steht aus.** Zum
|
||||||
|
Zeitpunkt der Änderung lag keine Datei vor, die den Akira-Schlüssel
|
||||||
|
enthält — belegt sind der Befund und die Mechanik, NICHT ein
|
||||||
|
erfolgreicher UHD-Rip.
|
||||||
|
- [ ] Damit bleibt auch der volle Transcode-E2E an einer UHD weiter offen
|
||||||
|
(siehe SAVEPOINT v3.7) — an normalen BD/DVD ist die Kette bewiesen.
|
||||||
|
|
||||||
|
**Ausdrücklich nicht gebaut (und wird es auch nicht):** Rippy liefert
|
||||||
|
keine Disc-Schlüssel mit, lädt keine herunter und verteilt keine. Es
|
||||||
|
stellt nur den Platz für eine Datei bereit, die der Nutzer selbst
|
||||||
|
mitbringt, und zeigt ehrlich an, was dort liegt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Ideen-Katalog (Rest) — bewusst offen
|
## Ideen-Katalog (Rest) — bewusst offen
|
||||||
|
|
||||||
1. **Design 2.0** — an Gemini übergeben (24.07.2026). Vollständiges
|
1. **Design 2.0** — an Gemini übergeben (24.07.2026). Vollständiges
|
||||||
|
|||||||
+77
-6
@@ -1,6 +1,73 @@
|
|||||||
# SAVEPOINT — Rippy
|
# 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:
|
- **RippyWorkerSetup.exe** ersetzt den .vbs/.bat-Weg (Commander-Einwand:
|
||||||
.vbs ist abgekündigt + wird als gefährlich geflaggt). install-gui.ps1 via
|
.vbs ist abgekündigt + wird als gefährlich geflaggt). install-gui.ps1 via
|
||||||
@@ -183,16 +250,20 @@ Konvertierung (Infrastruktur steht), AI-Box-NFS-Export, Kodi-Refresh.
|
|||||||
- **UHD-Kette diagnostiziert (Nachtrag, gleicher Tag): Der BU40N-Flash IST
|
- **UHD-Kette diagnostiziert (Nachtrag, gleicher Tag): Der BU40N-Flash IST
|
||||||
erledigt** — makemkvcon meldet „Using LibreDrive mode (v06.3)". Der
|
erledigt** — makemkvcon meldet „Using LibreDrive mode (v06.3)". Der
|
||||||
Summer-Wars-Fehlschlag lag NICHT am Laufwerk, sondern an „The volume key
|
Summer-Wars-Fehlschlag lag NICHT am Laufwerk, sondern an „The volume key
|
||||||
is unknown": die Disc (MKB v82) ist neuer als MakeMKVs Schlüssel-DB —
|
is unknown" — auf der VM mit 1.17.7 UND der aktuellsten 1.18.4
|
||||||
auf der VM mit 1.17.7 UND der aktuellsten 1.18.4 bewiesen. Konsequenzen:
|
reproduziert. Konsequenzen:
|
||||||
MakeMKV auf **1.18.4** gehoben (Scan-Hänger durch überall genutztes
|
MakeMKV auf **1.18.4** gehoben (Scan-Hänger durch überall genutztes
|
||||||
--noscan umgangen, auf der BU40N sauber gelaufen; URL-Base → /download),
|
--noscan umgangen, auf der BU40N sauber gelaufen; URL-Base → /download),
|
||||||
run_makemkv reicht kritische Meldungen (volume key/Key abgelaufen) in
|
run_makemkv reicht kritische Meldungen (volume key/Key abgelaufen) in
|
||||||
den Fehlertext durch, UHD-Klartext unterscheidet jetzt „Laufwerk kann
|
den Fehlertext durch, UHD-Klartext unterscheidet jetzt „Laufwerk kann
|
||||||
kein UHD" von „Disc neuer als Key-DB" inkl. Forum-Dump-Hinweis
|
kein UHD" von „Disc-Schlüssel fehlt" inkl. Forum-Dump-Hinweis
|
||||||
(MakeMKV speichert den AACS-Dump automatisch unter /root/.MakeMKV/).
|
(MakeMKV speichert den AACS-Dump automatisch unter /root/.MakeMKV/).
|
||||||
Summer Wars bleibt bis zu einem MakeMKV-Key-Update nicht entschlüsselbar
|
**⚠️ Korrigiert am 25.07.2026 (siehe v3.10 ganz oben):** Die hier
|
||||||
— das ist Stand der Technik, kein Rippy-Bug.
|
ursprünglich genannte Erklärung „die Disc ist neuer als MakeMKVs
|
||||||
|
Schlüssel-DB, ein MakeMKV-Update löst das" ist FALSCH. Nachgemessen:
|
||||||
|
MakeMKV hat gar keine nachladbare Schlüssel-DB mehr und versucht auch
|
||||||
|
keinen Online-Abruf; die alten Schlüssel-Server sind tot. Der Weg zum
|
||||||
|
Ziel heißt `KEYDB.cfg`, nicht „warten auf die nächste Version".
|
||||||
- **Vollautomatik** (Einstellungen → Ripping): Disc erkannt → Rip startet
|
- **Vollautomatik** (Einstellungen → Ripping): Disc erkannt → Rip startet
|
||||||
ohne Popup in den passenden Schnellwahl-Ordner (Serie/Film/Musik).
|
ohne Popup in den passenden Schnellwahl-Ordner (Serie/Film/Musik).
|
||||||
- **Job-Verwaltung**: „Neu komprimieren" nur noch, wenn Rohdaten wirklich
|
- **Job-Verwaltung**: „Neu komprimieren" nur noch, wenn Rohdaten wirklich
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ services:
|
|||||||
- TMDB_API_KEY=${TMDB_API_KEY:-}
|
- TMDB_API_KEY=${TMDB_API_KEY:-}
|
||||||
- THETVDB_API_KEY=${THETVDB_API_KEY:-}
|
- THETVDB_API_KEY=${THETVDB_API_KEY:-}
|
||||||
- OMDB_API_KEY=${OMDB_API_KEY:-}
|
- OMDB_API_KEY=${OMDB_API_KEY:-}
|
||||||
|
# Dasselbe Host-Verzeichnis wie beim Worker, nur an neutraler Stelle:
|
||||||
|
# die API liest den KEYDB-Status und die AACS-Dumps und nimmt eine
|
||||||
|
# hochgeladene KEYDB.cfg entgegen (Einstellungen → System).
|
||||||
|
- MAKEMKV_DATA_DIR=/app/makemkv-data
|
||||||
- LOG_LEVEL=INFO
|
- LOG_LEVEL=INFO
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "python -c 'import urllib.request; urllib.request.urlopen(\"http://localhost:8000/health\")'"]
|
test: ["CMD-SHELL", "python -c 'import urllib.request; urllib.request.urlopen(\"http://localhost:8000/health\")'"]
|
||||||
@@ -47,6 +51,8 @@ services:
|
|||||||
bind:
|
bind:
|
||||||
propagation: rshared
|
propagation: rshared
|
||||||
- temp:/app/temp
|
- temp:/app/temp
|
||||||
|
# MakeMKV-Datenverzeichnis (dasselbe wie im Worker, siehe dort).
|
||||||
|
- ${MAKEMKV_DATA_HOST:-/srv/rippy/makemkv}:/app/makemkv-data
|
||||||
devices:
|
devices:
|
||||||
- ${OPTICAL_SR:-/dev/sr0}:/dev/sr0
|
- ${OPTICAL_SR:-/dev/sr0}:/dev/sr0
|
||||||
networks:
|
networks:
|
||||||
@@ -78,6 +84,10 @@ services:
|
|||||||
- REDIS_URL=redis://redis:6379/0
|
- REDIS_URL=redis://redis:6379/0
|
||||||
- RIP_OUTPUT_DIR=/app/media
|
- RIP_OUTPUT_DIR=/app/media
|
||||||
- MAKEMKV_APP_KEY=${MAKEMKV_APP_KEY}
|
- MAKEMKV_APP_KEY=${MAKEMKV_APP_KEY}
|
||||||
|
# MakeMKVs Datenverzeichnis im Container — fest verdrahtet in MakeMKV
|
||||||
|
# selbst (HOME/.MakeMKV, HOME ist /root). Steht hier, damit Worker-Code
|
||||||
|
# und Mount dieselbe Wahrheit benutzen.
|
||||||
|
- MAKEMKV_DATA_DIR=/root/.MakeMKV
|
||||||
# Anzeigename in Einstellungen → Worker (stabil über Rebuilds hinweg;
|
# Anzeigename in Einstellungen → Worker (stabil über Rebuilds hinweg;
|
||||||
# via .env übersteuerbar, z. B. WORKER_NAME=wohnzimmer-vm)
|
# via .env übersteuerbar, z. B. WORKER_NAME=wohnzimmer-vm)
|
||||||
- WORKER_NAME=${WORKER_NAME:-rippy-hauptworker}
|
- WORKER_NAME=${WORKER_NAME:-rippy-hauptworker}
|
||||||
@@ -92,6 +102,13 @@ services:
|
|||||||
bind:
|
bind:
|
||||||
propagation: rslave
|
propagation: rslave
|
||||||
- temp:/app/temp
|
- temp:/app/temp
|
||||||
|
# MakeMKV-Datenverzeichnis PERSISTENT (Befund 25.07.2026, live gemessen):
|
||||||
|
# Hier liegen die KEYDB.cfg — heute die EINZIGE funktionierende
|
||||||
|
# Schlüsselquelle für 4K-UHD, weil MakeMKVs Online-Kanal nichts mehr
|
||||||
|
# liefert —, die AACS-Dumps fehlgeschlagener Discs und settings.conf.
|
||||||
|
# Ohne diesen Mount löschte JEDER `up -d --build` beides; die
|
||||||
|
# Fehlermeldung schickte den Nutzer zu einer Datei, die es nicht mehr gab.
|
||||||
|
- ${MAKEMKV_DATA_HOST:-/srv/rippy/makemkv}:/root/.MakeMKV
|
||||||
devices:
|
devices:
|
||||||
# Host-Geräteknoten via .env (OPTICAL_SR/OPTICAL_SG) — je Rechner ANDERS!
|
# Host-Geräteknoten via .env (OPTICAL_SR/OPTICAL_SG) — je Rechner ANDERS!
|
||||||
# MakeMKV spricht Laufwerke über die SCSI-Generic-Schicht an; ohne den
|
# MakeMKV spricht Laufwerke über die SCSI-Generic-Schicht an; ohne den
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import uuid
|
|||||||
|
|
||||||
import db
|
import db
|
||||||
import devices as device_discovery
|
import devices as device_discovery
|
||||||
|
import makemkv_daten
|
||||||
import makemkv_key
|
import makemkv_key
|
||||||
import mounts as mount_verwaltung
|
import mounts as mount_verwaltung
|
||||||
import notify
|
import notify
|
||||||
@@ -1190,6 +1191,146 @@ async def system_info():
|
|||||||
return await asyncio.to_thread(sammle)
|
return await asyncio.to_thread(sammle)
|
||||||
|
|
||||||
|
|
||||||
|
# Eigene Wurzel für die Datei-Härtung der AACS-Dumps. Bewusst NICHT die
|
||||||
|
# MEDIA_ROOT-Helfer (_sicherer_dateiname/_validiere_ziel/_job_ausgabeordner):
|
||||||
|
# die prüfen hart gegen /app/media und würden hier IMMER 404 liefern.
|
||||||
|
# Das MakeMKV-Datenverzeichnis liegt woanders (in der API auf
|
||||||
|
# /app/makemkv-data, im Worker auf /root/.MakeMKV — laut docker-compose.yml
|
||||||
|
# beides dasselbe Host-Verzeichnis).
|
||||||
|
MAKEMKV_DATA_ROOT = os.path.realpath(makemkv_daten.DATEN_DIR)
|
||||||
|
|
||||||
|
|
||||||
|
class KeydbRequest(BaseModel):
|
||||||
|
inhalt: str # voller Text der KEYDB.cfg (kein Upload — es gibt kein python-multipart)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/system/keydb")
|
||||||
|
async def get_keydb_status():
|
||||||
|
"""Was liegt gerade als KEYDB.cfg im MakeMKV-Datenverzeichnis?
|
||||||
|
|
||||||
|
Hintergrund (Befund 25.07.2026, live auf der VM nachgemessen): Bei
|
||||||
|
4K-UHD-Discs meldet MakeMKV "The volume key is unknown for this disc" und
|
||||||
|
holt den Schlüssel NICHT mehr online nach — die dokumentierten
|
||||||
|
Schlüssel-Server lösen weltweit nicht mehr auf. Der einzige heute
|
||||||
|
funktionierende Weg ist eine KEYDB.cfg, die der Nutzer selbst mitbringt.
|
||||||
|
Rippy liefert KEINE Schlüssel mit, lädt keine herunter und verteilt keine —
|
||||||
|
es stellt nur den Platz bereit und zeigt ehrlich an, was dort liegt.
|
||||||
|
|
||||||
|
Fehlendes Verzeichnis oder fehlende Datei ist der NORMALFALL: dann kommt
|
||||||
|
200 mit vorhanden=false zurück, niemals 404 oder 500.
|
||||||
|
"""
|
||||||
|
def sammle():
|
||||||
|
return makemkv_daten.keydb_status()
|
||||||
|
|
||||||
|
return await asyncio.to_thread(sammle)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/system/keydb")
|
||||||
|
async def set_keydb(request: KeydbRequest):
|
||||||
|
"""Legt die vom Nutzer mitgebrachte KEYDB.cfg ab (atomar, ersetzt die alte).
|
||||||
|
|
||||||
|
WICHTIG für die Ehrlichkeit: Die Datei wirkt erst beim NÄCHSTEN Rip —
|
||||||
|
makemkvcon liest sie beim Prozessstart, ein bereits laufender Rip merkt
|
||||||
|
nichts davon. Genau so steht es auch im Log-Eintrag.
|
||||||
|
"""
|
||||||
|
# Reine Prüfung (kein Dateisystem) — fängt den häufigsten Bedienfehler ab:
|
||||||
|
# statt der KEYDB.cfg landet die HTML-Fehlerseite eines Downloads im Feld.
|
||||||
|
fehler = makemkv_daten.keydb_pruefen(request.inhalt)
|
||||||
|
if fehler:
|
||||||
|
raise HTTPException(status_code=422, detail=fehler)
|
||||||
|
|
||||||
|
def schreibe():
|
||||||
|
return makemkv_daten.keydb_schreiben(request.inhalt)
|
||||||
|
|
||||||
|
try:
|
||||||
|
status = await asyncio.to_thread(schreibe)
|
||||||
|
except OSError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=(
|
||||||
|
f"KEYDB.cfg konnte nicht geschrieben werden: {e}. "
|
||||||
|
"Prüfe, ob das MakeMKV-Datenverzeichnis auf der VM existiert und "
|
||||||
|
"beschreibbar ist (Standard: /srv/rippy/makemkv)."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await asyncio.to_thread(
|
||||||
|
db.add_log, "success", "makemkv-keydb",
|
||||||
|
f"KEYDB.cfg abgelegt: {status['eintraege']} Zeilen mit Disc-Kennung, "
|
||||||
|
f"{status['groesse_bytes']} Bytes ({status['pfad']}). "
|
||||||
|
"Wirkt erst beim NÄCHSTEN Rip — MakeMKV liest die Datei beim Start.",
|
||||||
|
)
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/system/keydb")
|
||||||
|
async def delete_keydb():
|
||||||
|
"""Entfernt die KEYDB.cfg (z. B. nach einem Fehlgriff beim Einfügen).
|
||||||
|
|
||||||
|
Auch hier gilt: Die Änderung wirkt erst beim NÄCHSTEN Rip. Fehlt die Datei
|
||||||
|
schon, ist das kein Fehler — es kommt derselbe Zustand mit vorhanden=false.
|
||||||
|
"""
|
||||||
|
def loesche():
|
||||||
|
return makemkv_daten.keydb_loeschen()
|
||||||
|
|
||||||
|
try:
|
||||||
|
status = await asyncio.to_thread(loesche)
|
||||||
|
except OSError as e:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail=f"KEYDB.cfg konnte nicht entfernt werden: {e}",
|
||||||
|
)
|
||||||
|
await asyncio.to_thread(
|
||||||
|
db.add_log, "warning", "makemkv-keydb",
|
||||||
|
"KEYDB.cfg entfernt. Ab dem NÄCHSTEN Rip fehlen die selbst mitgebrachten "
|
||||||
|
"Schlüssel wieder — UHD-Discs können dann erneut an "
|
||||||
|
"'The volume key is unknown for this disc' scheitern.",
|
||||||
|
)
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/system/aacs-dumps")
|
||||||
|
async def get_aacs_dumps():
|
||||||
|
"""AACS-Dumps, die MakeMKV selbst abgelegt hat (neueste zuerst).
|
||||||
|
|
||||||
|
MakeMKV schreibt sie beim gescheiterten UHD-Versuch ins Datenverzeichnis
|
||||||
|
(Meldung 3332 "Saved AACS dump file as file:///root/.MakeMKV/<name>.tgz",
|
||||||
|
am 25.07.2026 so beobachtet). Rippy wertet sie nicht aus und schickt sie
|
||||||
|
nirgendwohin — es zeigt nur, dass sie da sind, damit der Nutzer selbst
|
||||||
|
entscheiden kann, was er damit tut.
|
||||||
|
"""
|
||||||
|
def liste():
|
||||||
|
return {"dumps": makemkv_daten.dumps_auflisten()}
|
||||||
|
|
||||||
|
return await asyncio.to_thread(liste)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/system/aacs-dumps/{dateiname}")
|
||||||
|
async def download_aacs_dump(dateiname: str):
|
||||||
|
"""Lädt EINEN AACS-Dump herunter.
|
||||||
|
|
||||||
|
Pfad-Validierung genauso streng wie beim Job-Datei-Download: nackter Name
|
||||||
|
ohne Pfadtrenner und ohne führenden Punkt (ist_aacs_dump) PLUS realpath,
|
||||||
|
der das MakeMKV-Datenverzeichnis nicht verlassen darf (kein ..-Ausbruch,
|
||||||
|
kein Symlink nach draußen).
|
||||||
|
"""
|
||||||
|
if not makemkv_daten.ist_aacs_dump(dateiname):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Kein gültiger Dump-Name — erwartet wird eine .tgz-Datei ohne Pfadangabe.",
|
||||||
|
)
|
||||||
|
pfad = os.path.join(MAKEMKV_DATA_ROOT, dateiname)
|
||||||
|
|
||||||
|
def pruefe():
|
||||||
|
return os.path.isfile(pfad) and os.path.realpath(pfad).startswith(MAKEMKV_DATA_ROOT)
|
||||||
|
|
||||||
|
if not await asyncio.to_thread(pruefe):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Dump nicht gefunden — MakeMKV legt ihn erst beim gescheiterten UHD-Versuch an.",
|
||||||
|
)
|
||||||
|
return FileResponse(pfad, filename=dateiname, media_type="application/gzip")
|
||||||
|
|
||||||
|
|
||||||
class NotificationTestRequest(BaseModel):
|
class NotificationTestRequest(BaseModel):
|
||||||
url: str
|
url: str
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
"""MakeMKV-Datenverzeichnis: KEYDB.cfg, AACS-Dumps, settings.conf.
|
||||||
|
|
||||||
|
WARUM ES DIESE DATEI GIBT (Befund 25.07.2026, live im Worker nachgemessen):
|
||||||
|
Bei 4K-UHD-Discs meldet MakeMKV "The volume key is unknown for this disc".
|
||||||
|
Das Laufwerk ist dabei in Ordnung — makemkvcon meldet "Using LibreDrive mode
|
||||||
|
(v06.3)" und liest die Disc — MakeMKV kennt nur den Schluessel DIESER Pressung
|
||||||
|
nicht. Und es holt ihn NICHT mehr online nach:
|
||||||
|
|
||||||
|
* /root/.MakeMKV/_private_data.tar enthielt ausser der Index-Datei keine
|
||||||
|
einzige hkd_*.bin, also nie einen heruntergeladenen Schluessel;
|
||||||
|
* auch mit erzwungener frischer Pruefung (update.conf geloescht, Meldung
|
||||||
|
5074 belegt den Web-Kontakt) und mit app_UpdateEnable = "1" kam keiner;
|
||||||
|
* die im MakeMKV-Forum dokumentierten Schluessel-Server hkdata.fairuse.org
|
||||||
|
und hkdata.crabdance.com loesen weltweit nicht mehr auf (gegen Fritz!Box,
|
||||||
|
8.8.8.8 und 1.1.1.1 geprueft: NXDOMAIN).
|
||||||
|
|
||||||
|
Betroffen war Akira UHD (MKB v76, Pressung von Dezember 2020) — also gerade
|
||||||
|
KEINE Neuerscheinung. Die bisherige Fehlermeldung ("Disc neuer als die
|
||||||
|
Schluessel-Datenbank, mit einem der naechsten Updates rippbar") war damit
|
||||||
|
schlicht falsch.
|
||||||
|
|
||||||
|
Der einzige heute funktionierende Weg ist eine KEYDB.cfg im
|
||||||
|
MakeMKV-Datenverzeichnis. Rippy liefert KEINE Schluessel mit, laedt keine
|
||||||
|
herunter und verteilt keine — es stellt nur den Platz dafuer bereit und zeigt
|
||||||
|
ehrlich an, was dort liegt. Die Datei bringt der Nutzer selbst mit.
|
||||||
|
|
||||||
|
QUELLEN (AGENTS Regel D — externe Schnittstellen nie aus dem Kopf):
|
||||||
|
* Datenverzeichnis und Dateiname GROSS geschrieben (unter Linux
|
||||||
|
case-sensitiv), dort geloest mit "cp KEYDB.cfg ~/.MakeMKV/":
|
||||||
|
https://forum.makemkv.com/forum/viewtopic.php?t=30636
|
||||||
|
* Heruntergeladene Schluessel liegen als hkd_*.bin in _private_data.tar:
|
||||||
|
https://forum.makemkv.com/forum/viewtopic.php?t=32675
|
||||||
|
* Zeilenformat der KEYDB.cfg (libaacs): Disc-Kennung = 40 Hex-Zeichen,
|
||||||
|
optional mit 0x-Praefix, dann "= Titel", danach optionale Felder wie
|
||||||
|
"| V | <32 Hex>"; Zeilen ab ";" sind Kommentare:
|
||||||
|
https://github.com/ShiftMediaProject/libaacs/blob/master/KEYDB.cfg
|
||||||
|
* Den AACS-Dump legt MakeMKV selbst ab, Meldung 3332 "Saved AACS dump file
|
||||||
|
as file:///root/.MakeMKV/<name>.tgz" (am 25.07.2026 so beobachtet).
|
||||||
|
|
||||||
|
ZWILLINGSDATEI: liegt identisch unter docker/api/ und docker/worker/ — beide
|
||||||
|
Images brauchen sie, ein gemeinsames Paket gibt es in diesem Projekt nicht
|
||||||
|
(gleiche Lage wie bei db.py). Aenderungen IMMER in BEIDEN Dateien nachziehen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
# Container-Pfad aus der Umgebung: /root/.MakeMKV im Worker (nur dort sucht
|
||||||
|
# makemkvcon), /app/makemkv-data in der API. Beide zeigen laut
|
||||||
|
# docker-compose.yml auf DASSELBE Host-Verzeichnis.
|
||||||
|
DATEN_DIR = os.getenv("MAKEMKV_DATA_DIR", "/root/.MakeMKV")
|
||||||
|
|
||||||
|
# GROSS geschrieben — unter Linux case-sensitiv, "keydb.cfg" wird ignoriert.
|
||||||
|
KEYDB_NAME = "KEYDB.cfg"
|
||||||
|
|
||||||
|
# Obergrenze fuer den Upload. Eine vollstaendige oeffentliche KEYDB.cfg liegt
|
||||||
|
# im einstelligen MB-Bereich; 64 MB sind reichlich Luft und verhindern, dass
|
||||||
|
# eine versehentlich hochgeladene Riesendatei den Speicher vollschreibt.
|
||||||
|
MAX_KEYDB_BYTES = 64 * 1024 * 1024
|
||||||
|
|
||||||
|
# Eine Disc-Zeile beginnt mit der 40 Zeichen langen Hex-Kennung (optional mit
|
||||||
|
# 0x davor), danach folgt das Gleichheitszeichen. Alles andere (Kommentare ab
|
||||||
|
# ";", Leerzeilen, Fortsetzungsfelder) zaehlt nicht als Eintrag.
|
||||||
|
_DISC_ZEILE = re.compile(r"^\s*(?:0x)?[0-9a-fA-F]{40}\s*=")
|
||||||
|
|
||||||
|
|
||||||
|
def _iso(zeitstempel: float) -> str:
|
||||||
|
"""Unix-Zeit -> ISO-8601 in UTC (sekundengenau), wie db.utcnow() es tut."""
|
||||||
|
return datetime.fromtimestamp(zeitstempel, timezone.utc).isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
|
||||||
|
def keydb_pfad(daten_dir: str = None) -> str:
|
||||||
|
"""Voller Pfad zur KEYDB.cfg im Datenverzeichnis."""
|
||||||
|
return os.path.join(daten_dir or DATEN_DIR, KEYDB_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def zaehle_disc_eintraege(inhalt: str) -> int:
|
||||||
|
"""Zeilen mit Disc-Kennung zaehlen (pure Funktion, testbar).
|
||||||
|
|
||||||
|
Bewusst eine Heuristik und keine vollstaendige Auswertung: MakeMKV liest
|
||||||
|
die Datei mit seinem eigenen Parser, und wie viele Eintraege es daraus
|
||||||
|
macht, ist von aussen nicht sichtbar. Die Zahl dient nur dazu, im UI
|
||||||
|
"da liegt wirklich etwas drin" von "leere oder falsche Datei" zu
|
||||||
|
unterscheiden — sie wird deshalb auch genau so beschriftet.
|
||||||
|
"""
|
||||||
|
return sum(1 for zeile in inhalt.splitlines() if _DISC_ZEILE.match(zeile))
|
||||||
|
|
||||||
|
|
||||||
|
def keydb_pruefen(inhalt: str) -> str:
|
||||||
|
"""Prueft hochgeladenen Inhalt; gibt deutschen Fehlertext oder "" zurueck.
|
||||||
|
|
||||||
|
Verhindert den haeufigsten Bedienfehler: statt der KEYDB.cfg landet die
|
||||||
|
HTML-Fehlerseite eines Downloads oder eine leere Datei im Verzeichnis —
|
||||||
|
MakeMKV wuerde dann still weiter "volume key is unknown" melden.
|
||||||
|
"""
|
||||||
|
if not inhalt.strip():
|
||||||
|
return "Die Datei ist leer."
|
||||||
|
if len(inhalt.encode("utf-8")) > MAX_KEYDB_BYTES:
|
||||||
|
return (
|
||||||
|
"Die Datei ist groesser als "
|
||||||
|
f"{MAX_KEYDB_BYTES // (1024 * 1024)} MB — das ist keine KEYDB.cfg."
|
||||||
|
)
|
||||||
|
if inhalt.lstrip()[:1] == "<":
|
||||||
|
return (
|
||||||
|
"Das sieht nach HTML aus, nicht nach einer KEYDB.cfg — "
|
||||||
|
"vermutlich wurde eine Fehlerseite statt der Datei geladen."
|
||||||
|
)
|
||||||
|
if zaehle_disc_eintraege(inhalt) == 0:
|
||||||
|
return (
|
||||||
|
"Keine einzige Zeile mit Disc-Kennung gefunden (40 Hex-Zeichen, "
|
||||||
|
"dann ein Gleichheitszeichen). Das ist keine KEYDB.cfg."
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def keydb_status(daten_dir: str = None) -> dict:
|
||||||
|
"""Zustand der KEYDB.cfg. Fehlt sie, ist das der Normalfall, kein Fehler."""
|
||||||
|
pfad = keydb_pfad(daten_dir)
|
||||||
|
try:
|
||||||
|
angaben = os.stat(pfad)
|
||||||
|
except OSError:
|
||||||
|
return {
|
||||||
|
"vorhanden": False,
|
||||||
|
"pfad": pfad,
|
||||||
|
"groesse_bytes": 0,
|
||||||
|
"eintraege": 0,
|
||||||
|
"geaendert": "",
|
||||||
|
}
|
||||||
|
eintraege = 0
|
||||||
|
try:
|
||||||
|
with open(pfad, encoding="utf-8", errors="replace") as datei:
|
||||||
|
eintraege = zaehle_disc_eintraege(datei.read())
|
||||||
|
except OSError:
|
||||||
|
pass # Datei da, aber unlesbar: Groesse/Datum stimmen trotzdem
|
||||||
|
return {
|
||||||
|
"vorhanden": True,
|
||||||
|
"pfad": pfad,
|
||||||
|
"groesse_bytes": angaben.st_size,
|
||||||
|
"eintraege": eintraege,
|
||||||
|
"geaendert": _iso(angaben.st_mtime),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def keydb_schreiben(inhalt: str, daten_dir: str = None) -> dict:
|
||||||
|
"""Schreibt die KEYDB.cfg atomar und meldet den neuen Zustand.
|
||||||
|
|
||||||
|
Erst in eine Nebendatei, dann os.replace: waehrend ein Rip laeuft, darf
|
||||||
|
makemkvcon niemals eine halb geschriebene Datei zu sehen bekommen.
|
||||||
|
"""
|
||||||
|
pfad = keydb_pfad(daten_dir)
|
||||||
|
os.makedirs(os.path.dirname(pfad), exist_ok=True)
|
||||||
|
neben = pfad + ".neu"
|
||||||
|
try:
|
||||||
|
with open(neben, "w", encoding="utf-8", newline="\n") as datei:
|
||||||
|
datei.write(inhalt)
|
||||||
|
os.replace(neben, pfad)
|
||||||
|
except OSError:
|
||||||
|
# Die Nebendatei nie liegen lassen: eine halb geschriebene
|
||||||
|
# KEYDB.cfg.neu verwirrt jeden, der ins Verzeichnis schaut, und
|
||||||
|
# belegt im Extremfall 64 MB, die niemand mehr aufräumt.
|
||||||
|
try:
|
||||||
|
os.remove(neben)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
return keydb_status(daten_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def keydb_loeschen(daten_dir: str = None) -> dict:
|
||||||
|
"""Entfernt die KEYDB.cfg (z. B. nach einem Fehlgriff beim Hochladen).
|
||||||
|
|
||||||
|
Nur "Datei war schon weg" wird geschluckt — das ist das gewünschte
|
||||||
|
Ergebnis. Jeder andere Fehler (schreibgeschützter Mount, fremder
|
||||||
|
Eigentümer) MUSS nach oben durch: sonst meldete die API einen Erfolg,
|
||||||
|
den es nicht gab, und die Datei wirkte beim nächsten Rip weiter.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
os.remove(keydb_pfad(daten_dir))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
return keydb_status(daten_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def ist_aacs_dump(name: str) -> bool:
|
||||||
|
"""Dateiname eines AACS-Dumps? (pure Funktion, testbar)
|
||||||
|
|
||||||
|
MakeMKV legt ihn als <MKB..._NAME_....>.tgz direkt im Datenverzeichnis ab
|
||||||
|
(Meldung 3332). Pfadtrenner und fuehrende Punkte werden hier schon
|
||||||
|
ausgeschlossen, damit der Download-Endpunkt keine Pfad-Tricks erlaubt.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
name.endswith(".tgz")
|
||||||
|
and "/" not in name
|
||||||
|
and "\\" not in name
|
||||||
|
and not name.startswith(".")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dumps_auflisten(daten_dir: str = None) -> list:
|
||||||
|
"""Alle AACS-Dumps im Datenverzeichnis, neueste zuerst."""
|
||||||
|
ordner = daten_dir or DATEN_DIR
|
||||||
|
try:
|
||||||
|
namen = os.listdir(ordner)
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
liste = []
|
||||||
|
for name in namen:
|
||||||
|
if not ist_aacs_dump(name):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
angaben = os.stat(os.path.join(ordner, name))
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
liste.append(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"groesse_bytes": angaben.st_size,
|
||||||
|
"geaendert": _iso(angaben.st_mtime),
|
||||||
|
"_sort": angaben.st_mtime,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
liste.sort(key=lambda eintrag: eintrag["_sort"], reverse=True)
|
||||||
|
for eintrag in liste:
|
||||||
|
del eintrag["_sort"]
|
||||||
|
return liste
|
||||||
|
|
||||||
|
|
||||||
|
def settings_conf_zusammenfuehren(inhalt: str, key: str) -> str:
|
||||||
|
"""app_Key setzen, ohne den Rest der settings.conf zu verlieren (pure).
|
||||||
|
|
||||||
|
Bis zum 25.07.2026 haben entrypoint.sh UND tasks.py die Datei komplett
|
||||||
|
ueberschrieben. Mit dem jetzt persistenten Datenverzeichnis waere damit
|
||||||
|
bei jedem Containerstart und vor jedem Rip alles andere weg — z. B.
|
||||||
|
app_UpdateEnable. Leerer Key laesst die vorhandene Zeile ebenfalls fallen,
|
||||||
|
damit ein bewusst geleerter Key nicht heimlich weiterwirkt.
|
||||||
|
"""
|
||||||
|
zeilen = [z for z in inhalt.splitlines() if not z.lstrip().startswith("app_Key")]
|
||||||
|
if key:
|
||||||
|
zeilen.append('app_Key = "{}"'.format(key))
|
||||||
|
text = "\n".join(zeilen).strip("\n")
|
||||||
|
return text + "\n" if text else ""
|
||||||
@@ -11,8 +11,14 @@ Dieses Modul holt den aktuellen Key vom Forum und schreibt ihn in die Settings
|
|||||||
Rebuild/Neustart, ab dem naechsten Rip.
|
Rebuild/Neustart, ab dem naechsten Rip.
|
||||||
|
|
||||||
Wichtig: Das ist die kostenlose, oeffentliche Beta-LIZENZ der Software selbst —
|
Wichtig: Das ist die kostenlose, oeffentliche Beta-LIZENZ der Software selbst —
|
||||||
NICHT das Entschluesseln oder Verteilen von Disc-Schluesseln. Den AACS-Schluessel
|
NICHT das Entschluesseln oder Verteilen von Disc-Schluesseln. Diese Grenze gilt
|
||||||
zieht MakeMKV via LibreDrive ohnehin selbst aus dem Laufwerk.
|
unveraendert; Rippy liefert keine Disc-Schluessel mit und verteilt keine.
|
||||||
|
|
||||||
|
Richtigstellung 25.07.2026: Hier stand frueher, MakeMKV ziehe den AACS-Schluessel
|
||||||
|
via LibreDrive ohnehin selbst aus dem Laufwerk. Das stimmt fuer Blu-ray, aber
|
||||||
|
NICHT fuer 4K-UHD — dort braucht MakeMKV den Volume-Key der jeweiligen Pressung,
|
||||||
|
und den bekommt es weder aus dem Laufwerk noch (heute) aus dem Netz. Belege und
|
||||||
|
Messungen stehen im Modul-Kopf von makemkv_daten.py.
|
||||||
|
|
||||||
Quelle/Format dokumentiert (AGENTS Regel D — nicht geraten):
|
Quelle/Format dokumentiert (AGENTS Regel D — nicht geraten):
|
||||||
- Forum-Thread: https://forum.makemkv.com/forum/viewtopic.php?t=1053
|
- Forum-Thread: https://forum.makemkv.com/forum/viewtopic.php?t=1053
|
||||||
|
|||||||
@@ -18,7 +18,13 @@ def test_main_importierbar_und_routen_verdrahtet():
|
|||||||
from main import app
|
from main import app
|
||||||
|
|
||||||
routen = {route.path for route in app.routes}
|
routen = {route.path for route in app.routes}
|
||||||
for pfad in ("/health", "/jobs", "/devices", "/logs", "/settings", "/prescan"):
|
for pfad in (
|
||||||
|
"/health", "/jobs", "/devices", "/logs", "/settings", "/prescan",
|
||||||
|
# KEYDB.cfg + AACS-Dumps: der Platz für die selbst mitgebrachte
|
||||||
|
# Schlüsseldatei (Befund 25.07.2026 — MakeMKV holt UHD-Schlüssel nicht
|
||||||
|
# mehr online nach). Ohne diese Routen ist die Seite im UI tot.
|
||||||
|
"/system/keydb", "/system/aacs-dumps", "/system/aacs-dumps/{dateiname}",
|
||||||
|
):
|
||||||
assert pfad in routen, f"Route {pfad} fehlt"
|
assert pfad in routen, f"Route {pfad} fehlt"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
"""Tests fuer die puren Helfer aus makemkv_daten.
|
||||||
|
|
||||||
|
Bewusst OHNE Dateisystem, DB und fcntl — deshalb laufen sie auch auf Windows
|
||||||
|
und nicht nur in der Ampel. Geprueft wird genau das, was ohne Container und
|
||||||
|
ohne echte Disc entscheidbar ist: das Zeilenformat der KEYDB.cfg, die
|
||||||
|
Plausibilitaetspruefung beim Hochladen, die Namenshaerte der AACS-Dumps und
|
||||||
|
das Zusammenfuehren der settings.conf.
|
||||||
|
"""
|
||||||
|
from makemkv_daten import (
|
||||||
|
ist_aacs_dump,
|
||||||
|
keydb_pruefen,
|
||||||
|
settings_conf_zusammenfuehren,
|
||||||
|
zaehle_disc_eintraege,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Echte Beispielzeilen im libaacs-Format: 40 Hex-Zeichen Disc-Kennung, dann
|
||||||
|
# "= Titel". Zweite Zeile mit 0x-Praefix, weil die oeffentlichen Dateien beide
|
||||||
|
# Schreibweisen mischen (Fundstelle steht im Modul-Docstring von makemkv_daten).
|
||||||
|
_GUELTIG = """; KEYDB.cfg — Beispiel
|
||||||
|
|
||||||
|
0123456789ABCDEF0123456789ABCDEF01234567 = Akira
|
||||||
|
0xFEDCBA9876543210FEDCBA9876543210FEDCBA98 = Blade Runner | V | 00112233445566778899AABBCCDDEEFF
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_zaehle_disc_eintraege_zaehlt_nur_echte_disc_zeilen():
|
||||||
|
# Kommentar- und Leerzeilen duerfen NICHT mitgezaehlt werden, sonst meldet
|
||||||
|
# das UI "da liegt was drin", obwohl die Datei keinen Schluessel enthaelt.
|
||||||
|
assert zaehle_disc_eintraege(_GUELTIG) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_zaehle_disc_eintraege_ohne_disc_zeile_ist_null():
|
||||||
|
nur_kommentare = "; nur ein Kommentar\n\n;noch einer\n"
|
||||||
|
assert zaehle_disc_eintraege(nur_kommentare) == 0
|
||||||
|
assert zaehle_disc_eintraege("") == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_zaehle_disc_eintraege_akzeptiert_0x_praefix_einzeln():
|
||||||
|
assert zaehle_disc_eintraege("0x0123456789abcdef0123456789abcdef01234567 = Tenet") == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_zaehle_disc_eintraege_lehnt_zu_kurze_kennung_ab():
|
||||||
|
# 39 statt 40 Hex-Zeichen: das ist keine Disc-Kennung, sondern Tippfehler
|
||||||
|
# oder eine abgeschnittene Datei — darf nicht als Eintrag durchgehen.
|
||||||
|
assert zaehle_disc_eintraege("0123456789ABCDEF0123456789ABCDEF0123456 = Kurz") == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_keydb_pruefen_meldet_leere_datei():
|
||||||
|
assert keydb_pruefen("") != ""
|
||||||
|
assert keydb_pruefen(" \n\n ") != ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_keydb_pruefen_erkennt_html():
|
||||||
|
# Haeufigster Bedienfehler: statt der Datei landet die HTML-Fehlerseite
|
||||||
|
# eines Downloads im Feld. MakeMKV wuerde dann still weiter meckern.
|
||||||
|
fehler = keydb_pruefen("<!DOCTYPE html>\n<html><body>404 Not Found</body></html>\n")
|
||||||
|
assert "HTML" in fehler
|
||||||
|
|
||||||
|
|
||||||
|
def test_keydb_pruefen_meldet_datei_ohne_disc_zeile():
|
||||||
|
# Text ist da, aber keine einzige Disc-Kennung — z. B. eine Liesmich-Datei.
|
||||||
|
assert keydb_pruefen("Das hier ist irgendein Text ohne Schluessel.\n") != ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_keydb_pruefen_laesst_gueltige_datei_durch():
|
||||||
|
# "" heisst laut Vertrag: alles in Ordnung, darf geschrieben werden.
|
||||||
|
assert keydb_pruefen(_GUELTIG) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_ist_aacs_dump_erkennt_echten_namen():
|
||||||
|
# So heisst der Dump, den MakeMKV am 25.07.2026 fuer Akira UHD abgelegt hat
|
||||||
|
# (Meldung 3332) — dieser Name MUSS zum Download durchkommen.
|
||||||
|
assert ist_aacs_dump("MKB20_v76_UHD_AKIRA_C02B.tgz") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_ist_aacs_dump_blockt_pfad_tricks():
|
||||||
|
# Der Download-Endpunkt haengt den Namen an das Datenverzeichnis — ein
|
||||||
|
# durchgelassenes ".." oder ein Pfadtrenner waere ein Ausbruch.
|
||||||
|
assert ist_aacs_dump("../x.tgz") is False
|
||||||
|
assert ist_aacs_dump("../../etc/passwd.tgz") is False
|
||||||
|
assert ist_aacs_dump("unter/ordner.tgz") is False
|
||||||
|
assert ist_aacs_dump("unter\\ordner.tgz") is False
|
||||||
|
assert ist_aacs_dump(".versteckt.tgz") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_ist_aacs_dump_lehnt_andere_endungen_ab():
|
||||||
|
# Nur die Dumps sollen abholbar sein — nicht settings.conf, nicht
|
||||||
|
# _private_data.tar und schon gar nicht die KEYDB.cfg selbst.
|
||||||
|
assert ist_aacs_dump("KEYDB.cfg") is False
|
||||||
|
assert ist_aacs_dump("settings.conf") is False
|
||||||
|
assert ist_aacs_dump("_private_data.tar") is False
|
||||||
|
assert ist_aacs_dump("") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_conf_ersetzt_alten_key_und_behaelt_den_rest():
|
||||||
|
# Regression: bis 25.07.2026 wurde die Datei komplett ueberschrieben. Mit
|
||||||
|
# dem jetzt persistenten Datenverzeichnis waere app_UpdateEnable vor jedem
|
||||||
|
# Rip weg gewesen.
|
||||||
|
alt = 'app_UpdateEnable = "1"\napp_Key = "T-alt"\napp_DestinationDir = "/tmp"\n'
|
||||||
|
neu = settings_conf_zusammenfuehren(alt, "T-neu")
|
||||||
|
assert 'app_Key = "T-neu"' in neu
|
||||||
|
assert "T-alt" not in neu
|
||||||
|
assert 'app_UpdateEnable = "1"' in neu
|
||||||
|
assert 'app_DestinationDir = "/tmp"' in neu
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_conf_leerer_key_entfernt_die_zeile():
|
||||||
|
# Ein bewusst geleerter Key darf nicht heimlich weiterwirken.
|
||||||
|
neu = settings_conf_zusammenfuehren('app_Key = "T-alt"\napp_UpdateEnable = "1"\n', "")
|
||||||
|
assert "app_Key" not in neu
|
||||||
|
assert 'app_UpdateEnable = "1"' in neu
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_conf_aus_dem_nichts_endet_mit_zeilenumbruch():
|
||||||
|
# Erster Start: es gibt noch keine settings.conf. MakeMKV erwartet eine
|
||||||
|
# Datei mit abschliessendem Zeilenumbruch.
|
||||||
|
assert settings_conf_zusammenfuehren("", "T-neu") == 'app_Key = "T-neu"\n'
|
||||||
|
assert settings_conf_zusammenfuehren("", "") == ""
|
||||||
@@ -17,6 +17,12 @@ server {
|
|||||||
# SSE (/api/stream/jobs) braucht ungepufferte, lange Verbindungen:
|
# SSE (/api/stream/jobs) braucht ungepufferte, lange Verbindungen:
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
proxy_read_timeout 3600s;
|
proxy_read_timeout 3600s;
|
||||||
|
# Der nginx-Default ist 1 MB. Eine echte KEYDB.cfg ist deutlich groesser,
|
||||||
|
# der Upload ueber POST /api/system/keydb wuerde also schon hier mit
|
||||||
|
# 413 abgewiesen — die API bekaeme die Anfrage nie zu sehen und das UI
|
||||||
|
# haette keinen detail-Text, den es anzeigen koennte. 64m entspricht dem
|
||||||
|
# Limit MAX_KEYDB_BYTES in makemkv_daten.py.
|
||||||
|
client_max_body_size 64m;
|
||||||
}
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
|||||||
@@ -134,12 +134,15 @@ export default function AnleitungPage() {
|
|||||||
Einstellungen → System eintragen — gilt ab dem nächsten Rip, ohne Neustart. DVDs gehen
|
Einstellungen → System eintragen — gilt ab dem nächsten Rip, ohne Neustart. DVDs gehen
|
||||||
immer auch ohne Key. Dort stehen auch die Werkzeug-Versionen und der freie Speicherplatz.
|
immer auch ohne Key. Dort stehen auch die Werkzeug-Versionen und der freie Speicherplatz.
|
||||||
</p>
|
</p>
|
||||||
|
{/* 25.07.2026 richtiggestellt: Updates bringen keine Disc-Schluessel mit — siehe UHD-Absatz unten. */}
|
||||||
<p>
|
<p>
|
||||||
<span className={fett}>Updates:</span> „Auf Updates prüfen" (ebenfalls Einstellungen →
|
<span className={fett}>Updates:</span> „Auf Updates prüfen" (ebenfalls Einstellungen →
|
||||||
System) vergleicht die installierten Versionen mit makemkv.com und den offiziellen
|
System) vergleicht die installierten Versionen mit makemkv.com und den offiziellen
|
||||||
HandBrake-Releases. Gibt es ein MakeMKV-Update, zeigt Rippy den fertigen
|
HandBrake-Releases. Gibt es ein MakeMKV-Update, zeigt Rippy den fertigen
|
||||||
Update-Befehl an — wichtig, weil neue Versionen auch die neueste
|
Update-Befehl an — neue Versionen bringen bessere Laufwerks-Unterstützung und
|
||||||
Disc-Schlüssel-Datenbank mitbringen.
|
Fehlerbehebungen. <span className={fett}>Disc-Schlüssel für 4K-UHD kommen dagegen nicht
|
||||||
|
aus einem Update</span>, sondern nur aus der Datei <code>KEYDB.cfg</code>, die du selbst
|
||||||
|
unter Einstellungen → System hochlädst (mehr dazu unter „Häufige Fragen").
|
||||||
</p>
|
</p>
|
||||||
</Abschnitt>
|
</Abschnitt>
|
||||||
|
|
||||||
@@ -157,11 +160,31 @@ export default function AnleitungPage() {
|
|||||||
<span className={fett}>„Diese Disc wurde bereits gerippt"?</span> Rippy erkennt Discs am
|
<span className={fett}>„Diese Disc wurde bereits gerippt"?</span> Rippy erkennt Discs am
|
||||||
Fingerabdruck. Nochmal rippen geht trotzdem — der Hinweis verhindert nur Versehen.
|
Fingerabdruck. Nochmal rippen geht trotzdem — der Hinweis verhindert nur Versehen.
|
||||||
</p>
|
</p>
|
||||||
|
{/*
|
||||||
|
25.07.2026 auf der Rippy-VM nachgemessen und komplett neu geschrieben:
|
||||||
|
Hier stand vorher, ein MakeMKV-Update mache die Disc rippbar. Das stimmt
|
||||||
|
nicht — MakeMKV versucht bei einer unbekannten UHD-Pressung gar nicht mehr,
|
||||||
|
online einen Schluessel zu holen, und die dokumentierten Schluessel-Server
|
||||||
|
(hkdata.fairuse.org, hkdata.crabdance.com) loesen weltweit nicht mehr auf.
|
||||||
|
*/}
|
||||||
<p>
|
<p>
|
||||||
<span className={fett}>4K-UHD schlägt fehl mit „volume key is unknown"?</span> Das Laufwerk
|
<span className={fett}>4K-UHD schlägt fehl mit „volume key is unknown"?</span> Das Laufwerk
|
||||||
liest die Disc (LibreDrive), aber MakeMKV kennt den Schlüssel dieser (zu neuen) Pressung
|
liest die Disc einwandfrei (LibreDrive) — MakeMKV fehlt nur der Schlüssel dieser Pressung.
|
||||||
noch nicht. Den automatisch gespeicherten AACS-Dump im MakeMKV-Forum einreichen — mit einem
|
Früher holte MakeMKV solche Schlüssel selbst aus dem Netz; dieser Kanal liefert heute nichts
|
||||||
der nächsten Updates ist die Disc rippbar.
|
mehr, und ein MakeMKV-Update ändert daran nichts.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Der einzige Weg, der heute funktioniert, ist eine Datei namens <code>KEYDB.cfg</code> —
|
||||||
|
eine Textliste mit Disc-Schlüsseln, die du selbst mitbringst. Unter Einstellungen → System
|
||||||
|
hochladen, sie wirkt ab dem nächsten Rip. <span className={fett}>Rippy liefert keine
|
||||||
|
Schlüssel mit und lädt auch keine herunter</span> — Rippy stellt nur den Platz für deine
|
||||||
|
Datei bereit und zeigt dir an, was dort liegt.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Der <span className={fett}>AACS-Dump</span> zu einer gescheiterten Disc bleibt jetzt
|
||||||
|
erhalten und steht unter Einstellungen → System zum Herunterladen. Ihn kannst du im
|
||||||
|
MakeMKV-Forum im Bereich „Ultra HD Blu-ray" einreichen — daraus lässt sich der Schlüssel
|
||||||
|
für deine Pressung ermitteln, den du dann in deine <code>KEYDB.cfg</code> einträgst.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className={fett}>„Neu komprimieren" fehlt bei einem Fehl-Job?</span> Der Knopf
|
<span className={fett}>„Neu komprimieren" fehlt bei einem Fehl-Job?</span> Der Knopf
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import { Save, Disc, Cpu, Database, Globe, AlertCircle, CheckCircle, HardDrive, Wrench, Send, Tv } from 'lucide-react'
|
import { Save, Disc, Cpu, Database, Globe, AlertCircle, CheckCircle, HardDrive, Wrench, Send, Tv, KeyRound, Upload, Trash2, Download } from 'lucide-react'
|
||||||
import { api } from '../lib/api'
|
import { api } from '../lib/api'
|
||||||
import { useToast } from '../context/ToastContext'
|
import { useToast } from '../context/ToastContext'
|
||||||
import StorageMounts from '../components/StorageMounts'
|
import StorageMounts from '../components/StorageMounts'
|
||||||
@@ -62,7 +62,26 @@ type SettingsTab = 'ripping' | 'verarbeitung' | 'worker' | 'speicherziele' | 'ap
|
|||||||
interface WorkerInfo {
|
interface WorkerInfo {
|
||||||
name: string
|
name: string
|
||||||
encoders: string[]
|
encoders: string[]
|
||||||
info?: { makemkv?: string, handbrake?: string, makemkv_key?: string }
|
// keydb: "ja" | "nein" | "unbekannt" — sagt, ob DIESER Worker eine KEYDB.cfg
|
||||||
|
// in seinem MakeMKV-Datenverzeichnis sieht. Nur der Worker, der wirklich
|
||||||
|
// rippt, zaehlt, deshalb steht die Angabe pro Worker und nicht global.
|
||||||
|
info?: { makemkv?: string, handbrake?: string, makemkv_key?: string, keydb?: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Antwort von GET/POST/DELETE /system/keydb (Feldnamen exakt wie die API sie liefert).
|
||||||
|
interface KeydbStatus {
|
||||||
|
vorhanden: boolean
|
||||||
|
pfad: string
|
||||||
|
groesse_bytes: number
|
||||||
|
eintraege: number
|
||||||
|
geaendert: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ein AACS-Dump aus GET /system/aacs-dumps.
|
||||||
|
interface AacsDump {
|
||||||
|
name: string
|
||||||
|
groesse_bytes: number
|
||||||
|
geaendert: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SystemInfo {
|
interface SystemInfo {
|
||||||
@@ -73,6 +92,24 @@ interface SystemInfo {
|
|||||||
webhook_gesetzt: boolean
|
webhook_gesetzt: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Byte-Zahl menschenlesbar — eine echte KEYDB.cfg ist mehrere MB gross.
|
||||||
|
function bytesLesbar(bytes: number): string {
|
||||||
|
if (!bytes) return '0 B'
|
||||||
|
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||||
|
if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`
|
||||||
|
return `${bytes} B`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Die API liefert ISO-8601 in UTC (oder einen leeren String). Ohne Zonen-Kennung
|
||||||
|
// wuerde der Browser den Zeitstempel als Ortszeit lesen und die Uhrzeit um den
|
||||||
|
// Zonen-Versatz verschieben — deshalb notfalls ein Z anhaengen.
|
||||||
|
function zeitLesbar(iso: string): string {
|
||||||
|
if (!iso) return 'unbekannt'
|
||||||
|
const mitZone = /[Zz]$|[+-]\d{2}:?\d{2}$/.test(iso) ? iso : `${iso}Z`
|
||||||
|
const d = new Date(mitZone)
|
||||||
|
return isNaN(d.getTime()) ? 'unbekannt' : d.toLocaleString('de-DE')
|
||||||
|
}
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const [settings, setSettings] = useState<SettingsState>(defaultSettings)
|
const [settings, setSettings] = useState<SettingsState>(defaultSettings)
|
||||||
const [activeTab, setActiveTab] = useState<SettingsTab>('ripping')
|
const [activeTab, setActiveTab] = useState<SettingsTab>('ripping')
|
||||||
@@ -86,6 +123,13 @@ export default function SettingsPage() {
|
|||||||
const [quellenBusy, setQuellenBusy] = useState(false)
|
const [quellenBusy, setQuellenBusy] = useState(false)
|
||||||
const [updates, setUpdates] = useState<Record<string, { installiert?: string, verfuegbar?: string, update?: boolean }> | null>(null)
|
const [updates, setUpdates] = useState<Record<string, { installiert?: string, verfuegbar?: string, update?: boolean }> | null>(null)
|
||||||
const [updatesBusy, setUpdatesBusy] = useState(false)
|
const [updatesBusy, setUpdatesBusy] = useState(false)
|
||||||
|
// KEYDB.cfg und AACS-Dumps sind DATEIEN im MakeMKV-Datenverzeichnis, keine
|
||||||
|
// Einstellungen — deshalb bewusst NICHT in SettingsState: der globale
|
||||||
|
// Speichern-Knopf postet dieses Objekt komplett und wuerde sie mitschleifen.
|
||||||
|
const [keydb, setKeydb] = useState<KeydbStatus | null>(null)
|
||||||
|
const [keydbBusy, setKeydbBusy] = useState(false)
|
||||||
|
const [dumps, setDumps] = useState<AacsDump[]>([])
|
||||||
|
const keydbInput = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
|
||||||
@@ -106,6 +150,49 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status der KEYDB.cfg + Liste der AACS-Dumps frisch holen. Wird beim Laden
|
||||||
|
// der Seite und nach jeder Aktion aufgerufen — die API ist die Wahrheit,
|
||||||
|
// nicht das, was wir gerade hochgeschickt haben.
|
||||||
|
const keydbLaden = () => {
|
||||||
|
api.get('/system/keydb').then(r => setKeydb(r.data)).catch(() => setKeydb(null))
|
||||||
|
api.get('/system/aacs-dumps').then(r => setDumps(r.data?.dumps || [])).catch(() => setDumps([]))
|
||||||
|
// Die Worker-Plaketten oben kommen aus /capabilities und stammen aus dem
|
||||||
|
// letzten Herzschlag des Workers (minuetlich) — ohne dieses Nachladen
|
||||||
|
// widerspraeche die Kachel nach einem Upload sichtbar der Erfolgsmeldung.
|
||||||
|
// Ganz sofort ist sie trotzdem nicht: der Worker meldet erst beim
|
||||||
|
// naechsten Herzschlag neu. Genau so steht es auch im Erklaertext.
|
||||||
|
api.get('/capabilities').then(r => setWorkers(r.data.workers || [])).catch(() => setWorkers([]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const keydbHochladen = async (datei: File) => {
|
||||||
|
setKeydbBusy(true)
|
||||||
|
try {
|
||||||
|
// Bewusst als JSON-Text und nicht als Multipart-Upload: der API fehlt
|
||||||
|
// python-multipart, ein File()-Endpunkt wuerde sie beim Import killen.
|
||||||
|
const inhalt = await datei.text()
|
||||||
|
await api.post('/system/keydb', { inhalt })
|
||||||
|
keydbLaden()
|
||||||
|
toast('success', 'KEYDB.cfg gespeichert — sie wirkt ab dem nächsten Rip.')
|
||||||
|
} catch (e: any) {
|
||||||
|
toast('error', e?.response?.data?.detail || 'Hochladen fehlgeschlagen — ist das wirklich eine KEYDB.cfg, und läuft die API?')
|
||||||
|
} finally {
|
||||||
|
setKeydbBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const keydbEntfernen = async () => {
|
||||||
|
setKeydbBusy(true)
|
||||||
|
try {
|
||||||
|
await api.delete('/system/keydb')
|
||||||
|
keydbLaden()
|
||||||
|
toast('success', 'KEYDB.cfg entfernt — Rippy nutzt jetzt keine Schlüsseldatei mehr.')
|
||||||
|
} catch (e: any) {
|
||||||
|
toast('error', e?.response?.data?.detail || 'Entfernen fehlgeschlagen — läuft die API?')
|
||||||
|
} finally {
|
||||||
|
setKeydbBusy(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const quellenPruefen = async () => {
|
const quellenPruefen = async () => {
|
||||||
setQuellenBusy(true)
|
setQuellenBusy(true)
|
||||||
try {
|
try {
|
||||||
@@ -131,6 +218,12 @@ export default function SettingsPage() {
|
|||||||
api.get('/system/info')
|
api.get('/system/info')
|
||||||
.then(r => setSystemInfo(r.data))
|
.then(r => setSystemInfo(r.data))
|
||||||
.catch(() => setSystemInfo(null))
|
.catch(() => setSystemInfo(null))
|
||||||
|
api.get('/system/keydb')
|
||||||
|
.then(r => setKeydb(r.data))
|
||||||
|
.catch(() => setKeydb(null))
|
||||||
|
api.get('/system/aacs-dumps')
|
||||||
|
.then(r => setDumps(r.data?.dumps || []))
|
||||||
|
.catch(() => setDumps([]))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const webhookTesten = async () => {
|
const webhookTesten = async () => {
|
||||||
@@ -633,13 +726,40 @@ export default function SettingsPage() {
|
|||||||
HandBrake {w.info.handbrake}
|
HandBrake {w.info.handbrake}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{/* Nur das Verzeichnis des rippenden Workers zaehlt — deshalb je Worker. */}
|
||||||
|
{w.info?.keydb === 'ja' && (
|
||||||
|
<span className="text-xs px-2.5 py-0.5 rounded-md font-medium bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30">
|
||||||
|
KEYDB.cfg vorhanden
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{w.info?.keydb === 'nein' && (
|
||||||
|
<span className="text-xs px-2.5 py-0.5 rounded-md font-medium bg-amber-500/15 text-amber-600 dark:text-amber-400 border border-amber-500/30">
|
||||||
|
keine KEYDB.cfg
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{w.info?.keydb === 'unbekannt' && (
|
||||||
|
<span className="text-xs px-2.5 py-0.5 rounded-md font-medium bg-slate-200 dark:bg-slate-800 text-slate-500 dark:text-slate-400 border border-slate-300 dark:border-slate-700">
|
||||||
|
KEYDB.cfg unbekannt
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
{/*
|
||||||
|
Bis 25.07.2026 stand hier, MakeMKV-Updates braechten die neueste
|
||||||
|
Disc-Schluessel-Datenbank mit. Auf der Rippy-VM nachgemessen: falsch.
|
||||||
|
MakeMKV hat gar keine mitgelieferte Schluessel-Datenbank, und der
|
||||||
|
Online-Kanal liefert nichts mehr (die Server hkdata.fairuse.org und
|
||||||
|
hkdata.crabdance.com loesen weltweit nicht mehr auf). Updates bringen
|
||||||
|
Laufwerks-Firmware-Unterstuetzung und Fehlerbehebungen — Schluessel
|
||||||
|
fuer neue UHD-Pressungen kommen ausschliesslich aus der KEYDB.cfg.
|
||||||
|
*/}
|
||||||
<p className="text-xs mt-3 pt-3 border-t border-slate-200/60 dark:border-slate-800 text-slate-500 dark:text-slate-400">
|
<p className="text-xs mt-3 pt-3 border-t border-slate-200/60 dark:border-slate-800 text-slate-500 dark:text-slate-400">
|
||||||
<strong className="text-slate-700 dark:text-slate-300">MakeMKV</strong> lässt sich aktuell halten
|
<strong className="text-slate-700 dark:text-slate-300">MakeMKV</strong> lässt sich aktuell halten
|
||||||
(Update-Check unten; Version über <code>MAKEMKV_VERSION</code> + Rebuild) — wichtig wegen der
|
(Update-Check unten; Version über <code>MAKEMKV_VERSION</code> + Rebuild) — neue Versionen bringen
|
||||||
Disc-Schlüssel-Datenbank.
|
vor allem Laufwerks-Unterstützung und Fehlerbehebungen. <strong>Disc-Schlüssel für neue
|
||||||
|
4K-UHD-Pressungen kommen dagegen NICHT aus einem Update</strong>, sondern nur aus der KEYDB.cfg
|
||||||
|
im Block darunter.
|
||||||
{' '}<strong className="text-slate-700 dark:text-slate-300">HandBrake</strong> im eingebauten
|
{' '}<strong className="text-slate-700 dark:text-slate-300">HandBrake</strong> im eingebauten
|
||||||
Docker-Worker ist bewusst die stabile <strong>Debian-Version</strong> — für die Kompression völlig
|
Docker-Worker ist bewusst die stabile <strong>Debian-Version</strong> — für die Kompression völlig
|
||||||
ausreichend und wird <strong>nicht separat aktualisiert</strong>. Native Worker (Windows) holen
|
ausreichend und wird <strong>nicht separat aktualisiert</strong>. Native Worker (Windows) holen
|
||||||
@@ -655,6 +775,140 @@ export default function SettingsPage() {
|
|||||||
placeholder="T-… (Forum-Thread „MakeMKV is free while in beta”)"
|
placeholder="T-… (Forum-Thread „MakeMKV is free while in beta”)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
MakeMKV-Schluessel (KEYDB.cfg) — Befund vom 25.07.2026 auf der Rippy-VM:
|
||||||
|
MakeMKV versucht bei einer unbekannten UHD-Pressung gar nicht mehr, online
|
||||||
|
einen Schluessel zu holen, und die dokumentierten Schluessel-Server sind tot.
|
||||||
|
Der einzige heute funktionierende Weg ist eine Datei KEYDB.cfg (GROSS
|
||||||
|
geschrieben, unter Linux case-sensitiv) im MakeMKV-Datenverzeichnis.
|
||||||
|
Rippy stellt nur den Platz dafuer bereit — mitgeliefert wird nichts.
|
||||||
|
*/}
|
||||||
|
<div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-sm font-semibold text-slate-900 dark:text-slate-200 flex items-center gap-2">
|
||||||
|
<KeyRound size={16} className="text-amber-500" />
|
||||||
|
MakeMKV-Schlüssel (KEYDB.cfg)
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/*
|
||||||
|
Versteckter Datei-Dialog: der Inhalt wird im Browser gelesen und als
|
||||||
|
JSON gepostet. Kein Multipart — der API fehlt python-multipart.
|
||||||
|
*/}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
ref={keydbInput}
|
||||||
|
accept=".cfg,text/plain"
|
||||||
|
onChange={(e) => {
|
||||||
|
const datei = e.target.files?.[0]
|
||||||
|
// Wert leeren, damit dieselbe Datei erneut gewählt werden kann.
|
||||||
|
e.target.value = ''
|
||||||
|
if (datei) keydbHochladen(datei)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="amber"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => keydbInput.current?.click()}
|
||||||
|
disabled={keydbBusy}
|
||||||
|
>
|
||||||
|
<Upload size={14} />
|
||||||
|
{keydbBusy ? 'Arbeite…' : 'KEYDB.cfg hochladen'}
|
||||||
|
</Button>
|
||||||
|
{keydb?.vorhanden && (
|
||||||
|
<Button variant="danger" size="sm" onClick={keydbEntfernen} disabled={keydbBusy}>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
Entfernen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{keydb?.vorhanden ? (
|
||||||
|
<div className="p-3 rounded-lg text-sm bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 border border-emerald-500/30">
|
||||||
|
<p className="font-semibold">Eine KEYDB.cfg liegt bereit.</p>
|
||||||
|
<p className="text-xs mt-1">
|
||||||
|
{bytesLesbar(keydb.groesse_bytes)} · {keydb.eintraege} Zeilen mit Disc-Kennung ·
|
||||||
|
zuletzt geändert {zeitLesbar(keydb.geaendert)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs mt-1 font-mono opacity-80">{keydb.pfad}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="p-3 rounded-lg text-sm bg-amber-500/15 text-amber-600 dark:text-amber-400 border border-amber-500/30">
|
||||||
|
<p className="font-semibold">Es liegt keine KEYDB.cfg bereit.</p>
|
||||||
|
<p className="text-xs mt-1">
|
||||||
|
DVDs und normale Blu-rays laufen trotzdem. Nur bei 4K-UHD-Discs, deren Schlüssel MakeMKV
|
||||||
|
nicht kennt, bricht der Rip mit „The volume key is unknown for this disc" ab.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
Die <strong className="text-slate-700 dark:text-slate-300">KEYDB.cfg</strong> ist eine reine
|
||||||
|
Textdatei mit Disc-Schlüsseln für 4K-UHD-Blu-rays. MakeMKV holte solche Schlüssel früher selbst
|
||||||
|
aus dem Netz — das funktioniert heute nicht mehr, die alten Schlüssel-Server sind abgeschaltet.
|
||||||
|
Ein MakeMKV-Update hilft dagegen nicht.
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
<strong className="text-slate-700 dark:text-slate-300">Wichtig:</strong> Rippy liefert keine
|
||||||
|
Schlüssel mit, lädt keine herunter und verteilt keine. Rippy stellt nur den Platz für eine Datei
|
||||||
|
bereit, die du selbst mitbringst, und zeigt dir ehrlich an, was dort liegt. Die Zahl oben ist
|
||||||
|
genau das: die gezählten Zeilen mit einer Disc-Kennung in deiner Datei — keine von MakeMKV
|
||||||
|
bestätigte Anzahl brauchbarer Schlüssel.
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
Eine hochgeladene Datei ersetzt die bisherige und wirkt
|
||||||
|
<strong className="text-slate-700 dark:text-slate-300"> ab dem nächsten Rip</strong> — laufende
|
||||||
|
Jobs bleiben unberührt, ein Neustart ist nicht nötig. Die Plakette am Worker weiter oben
|
||||||
|
folgt erst mit dessen nächstem Herzschlag (etwa eine Minute) — dieser Kasten hier ist sofort
|
||||||
|
aktuell.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
AACS-Dumps: MakeMKV legt sie bei einer unbekannten UHD-Disc ab (Meldung 3332).
|
||||||
|
Seit das Datenverzeichnis persistent gemountet ist, ueberleben sie den
|
||||||
|
Container-Neustart — vorher waren sie nach jedem Rip weg.
|
||||||
|
*/}
|
||||||
|
<div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-sm font-semibold text-slate-900 dark:text-slate-200">AACS-Dumps</p>
|
||||||
|
<Button variant="secondary" size="sm" onClick={keydbLaden}>
|
||||||
|
Liste aktualisieren
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{dumps.length === 0 ? (
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
Noch kein Dump vorhanden. MakeMKV legt hier automatisch einen ab, sobald eine 4K-UHD-Disc
|
||||||
|
an einem unbekannten Schlüssel scheitert.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
{dumps.map(d => (
|
||||||
|
<a
|
||||||
|
key={d.name}
|
||||||
|
href={`/api/system/aacs-dumps/${encodeURIComponent(d.name)}`}
|
||||||
|
download
|
||||||
|
className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors text-amber-600 dark:text-amber-300 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||||
|
>
|
||||||
|
<Download size={14} className="flex-shrink-0" />
|
||||||
|
<span className="truncate min-w-0 font-mono">{d.name}</span>
|
||||||
|
<span className="ml-auto text-xs flex-shrink-0 text-slate-500 dark:text-slate-400 font-mono">
|
||||||
|
{bytesLesbar(d.groesse_bytes)} · {zeitLesbar(d.geaendert)}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
Ein AACS-Dump ist das, was das Laufwerk von der Disc gelesen hat, bevor der Schlüssel fehlte.
|
||||||
|
Er enthält keinen Schlüssel und nützt dir allein nichts — aber du kannst ihn herunterladen und
|
||||||
|
im MakeMKV-Forum im Bereich <strong className="text-slate-700 dark:text-slate-300">Ultra HD
|
||||||
|
Blu-ray</strong> einreichen. Dort können Leute daraus den Schlüssel für deine Pressung ermitteln;
|
||||||
|
den trägst du dann in deine KEYDB.cfg ein.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
|
<div className="p-4 rounded-xl border border-slate-200/80 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-950/80 space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="text-sm font-semibold text-slate-900 dark:text-slate-200">Update-Check</p>
|
<p className="text-sm font-semibold text-slate-900 dark:text-slate-200">Update-Check</p>
|
||||||
@@ -672,7 +926,9 @@ export default function SettingsPage() {
|
|||||||
<p className="text-xs mt-1">
|
<p className="text-xs mt-1">
|
||||||
⚠️ Update verfügbar — in der .env <code>MAKEMKV_VERSION={updates.makemkv.verfuegbar}</code> setzen,
|
⚠️ Update verfügbar — in der .env <code>MAKEMKV_VERSION={updates.makemkv.verfuegbar}</code> setzen,
|
||||||
dann auf der Rippy-Maschine <code>docker compose build worker && docker compose up -d worker</code>.
|
dann auf der Rippy-Maschine <code>docker compose build worker && docker compose up -d worker</code>.
|
||||||
Neue Versionen bringen auch die neueste Disc-Schlüssel-Datenbank mit.
|
{/* 25.07.2026 richtiggestellt: ein Update bringt KEINE Schluessel — die kommen nur aus der KEYDB.cfg. */}
|
||||||
|
Neue Versionen bringen Laufwerks-Unterstützung und Fehlerbehebungen — aber
|
||||||
|
<strong> keine Disc-Schlüssel</strong>: die kommen ausschließlich aus deiner KEYDB.cfg.
|
||||||
</p>
|
</p>
|
||||||
)
|
)
|
||||||
: <span className="text-xs ml-1 text-emerald-600 dark:text-emerald-400">✓ aktuell</span>}
|
: <span className="text-xs ml-1 text-emerald-600 dark:text-emerald-400">✓ aktuell</span>}
|
||||||
|
|||||||
@@ -16,9 +16,13 @@ FROM python:3.12-slim-bookworm AS makemkv-build
|
|||||||
# 1.18er-Laufwerks-Scan-Hängers (Forum t=38128) und der Flash-Fähigkeit
|
# 1.18er-Laufwerks-Scan-Hängers (Forum t=38128) und der Flash-Fähigkeit
|
||||||
# gepinnt. Beides erledigt: der Scan wird überall mit --noscan umgangen
|
# gepinnt. Beides erledigt: der Scan wird überall mit --noscan umgangen
|
||||||
# (info-Lauf mit 1.18.4 auf der BU40N sauber durchgelaufen, 24.07.), und
|
# (info-Lauf mit 1.18.4 auf der BU40N sauber durchgelaufen, 24.07.), und
|
||||||
# das Laufwerk ist geflasht (LibreDrive v06.3 bestätigt). Die AKTUELLE
|
# das Laufwerk ist geflasht (LibreDrive v06.3 bestätigt).
|
||||||
# Version zählt, weil sie die neueste AACS-Schlüssel-Datenbank mitbringt —
|
# Richtigstellung 25.07.2026: Hier stand, die aktuelle Version zähle, weil sie
|
||||||
# 1.17.7 kannte z. B. den Key der Summer-Wars-UHD (MKB v82) nicht.
|
# "die neueste AACS-Schlüssel-Datenbank mitbringt". Das ist widerlegt — MakeMKV
|
||||||
|
# bringt gar keine Disc-Schlüssel mit, und der Online-Kanal liefert nichts mehr
|
||||||
|
# (Messungen im Modul-Kopf von makemkv_daten.py). Aktuell bleiben lohnt sich
|
||||||
|
# trotzdem: Laufwerks-Unterstützung und Fehlerbehebungen. Schlüssel für neue
|
||||||
|
# UHD-Pressungen kommen ausschließlich aus der KEYDB.cfg im Datenverzeichnis.
|
||||||
# MAKEMKV_URL_BASE ist übersteuerbar (Build-Arg): /download hat nur die
|
# MAKEMKV_URL_BASE ist übersteuerbar (Build-Arg): /download hat nur die
|
||||||
# aktuelle Version, /download/old die älteren. Wenn Cloudflare BuildKit-
|
# aktuelle Version, /download/old die älteren. Wenn Cloudflare BuildKit-
|
||||||
# Downloads drosselt (24.07.: außerhalb des Builds ging alles, im Build
|
# Downloads drosselt (24.07.: außerhalb des Builds ging alles, im Build
|
||||||
|
|||||||
@@ -85,4 +85,24 @@ def werkzeug_versionen() -> dict:
|
|||||||
info["makemkv_key"] = "env"
|
info["makemkv_key"] = "env"
|
||||||
else:
|
else:
|
||||||
info["makemkv_key"] = "keiner"
|
info["makemkv_key"] = "keiner"
|
||||||
|
# Sieht DIESER Worker eine KEYDB.cfg? Die API zeigt ihren eigenen Mount —
|
||||||
|
# bei einem Remote-Worker kann das etwas ganz anderes sein, und nur das
|
||||||
|
# Verzeichnis des rippenden Workers zählt (Befund 25.07.2026).
|
||||||
|
#
|
||||||
|
# Gemeldet wird die Angabe NUR, wenn das Datenverzeichnis hier wirklich
|
||||||
|
# eingehängt ist (os.path.ismount). Ein Remote-Transcode-Worker benutzt
|
||||||
|
# dasselbe Image — makemkvcon ist dort also vorhanden und der entrypoint
|
||||||
|
# legt den Ordner an —, er bekommt den Mount aber nicht und rippt nie.
|
||||||
|
# Ohne diese Prüfung trüge er im UI dauerhaft die Warnung "keine
|
||||||
|
# KEYDB.cfg", obwohl ihn das gar nichts angeht.
|
||||||
|
try:
|
||||||
|
import makemkv_daten
|
||||||
|
daten_dir = makemkv_daten.DATEN_DIR
|
||||||
|
except Exception:
|
||||||
|
daten_dir = ""
|
||||||
|
if shutil.which("makemkvcon") and daten_dir and os.path.ismount(daten_dir):
|
||||||
|
try:
|
||||||
|
info["keydb"] = "ja" if makemkv_daten.keydb_status().get("vorhanden") else "nein"
|
||||||
|
except Exception:
|
||||||
|
info["keydb"] = "unbekannt"
|
||||||
return info
|
return info
|
||||||
|
|||||||
@@ -1,11 +1,51 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Schreibt den MakeMKV-Beta-Key aus der Umgebung in die Settings (falls gesetzt).
|
# Bereitet MakeMKVs Datenverzeichnis vor, BEVOR der Worker startet.
|
||||||
# Ohne Key: DVD-Ripping geht immer, Blu-ray läuft im 30-Tage-Testmodus.
|
#
|
||||||
|
# Wichtig (Befund 25.07.2026): Dieses Verzeichnis ist jetzt ein persistenter
|
||||||
|
# Mount vom Host (docker-compose.yml). Darin liegen KEYDB.cfg (die einzige
|
||||||
|
# heute funktionierende Schlüsselquelle für 4K-UHD), die AACS-Dumps
|
||||||
|
# fehlgeschlagener Discs und MakeMKVs settings.conf. Deshalb wird die
|
||||||
|
# settings.conf hier ERGÄNZT statt überschrieben — vorher hat dieses Skript
|
||||||
|
# sie bei jedem Start plattgemacht und dabei alles außer app_Key gelöscht.
|
||||||
|
#
|
||||||
|
# Ohne Beta-Key: DVD-Ripping geht immer, Blu-ray läuft im 30-Tage-Testmodus.
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if [ -n "${MAKEMKV_APP_KEY}" ]; then
|
DATEN_DIR="${MAKEMKV_DATA_DIR:-/root/.MakeMKV}"
|
||||||
mkdir -p /root/.MakeMKV
|
CONF="${DATEN_DIR}/settings.conf"
|
||||||
printf 'app_Key = "%s"\n' "${MAKEMKV_APP_KEY}" > /root/.MakeMKV/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
|
fi
|
||||||
|
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
"""MakeMKV-Datenverzeichnis: KEYDB.cfg, AACS-Dumps, settings.conf.
|
||||||
|
|
||||||
|
WARUM ES DIESE DATEI GIBT (Befund 25.07.2026, live im Worker nachgemessen):
|
||||||
|
Bei 4K-UHD-Discs meldet MakeMKV "The volume key is unknown for this disc".
|
||||||
|
Das Laufwerk ist dabei in Ordnung — makemkvcon meldet "Using LibreDrive mode
|
||||||
|
(v06.3)" und liest die Disc — MakeMKV kennt nur den Schluessel DIESER Pressung
|
||||||
|
nicht. Und es holt ihn NICHT mehr online nach:
|
||||||
|
|
||||||
|
* /root/.MakeMKV/_private_data.tar enthielt ausser der Index-Datei keine
|
||||||
|
einzige hkd_*.bin, also nie einen heruntergeladenen Schluessel;
|
||||||
|
* auch mit erzwungener frischer Pruefung (update.conf geloescht, Meldung
|
||||||
|
5074 belegt den Web-Kontakt) und mit app_UpdateEnable = "1" kam keiner;
|
||||||
|
* die im MakeMKV-Forum dokumentierten Schluessel-Server hkdata.fairuse.org
|
||||||
|
und hkdata.crabdance.com loesen weltweit nicht mehr auf (gegen Fritz!Box,
|
||||||
|
8.8.8.8 und 1.1.1.1 geprueft: NXDOMAIN).
|
||||||
|
|
||||||
|
Betroffen war Akira UHD (MKB v76, Pressung von Dezember 2020) — also gerade
|
||||||
|
KEINE Neuerscheinung. Die bisherige Fehlermeldung ("Disc neuer als die
|
||||||
|
Schluessel-Datenbank, mit einem der naechsten Updates rippbar") war damit
|
||||||
|
schlicht falsch.
|
||||||
|
|
||||||
|
Der einzige heute funktionierende Weg ist eine KEYDB.cfg im
|
||||||
|
MakeMKV-Datenverzeichnis. Rippy liefert KEINE Schluessel mit, laedt keine
|
||||||
|
herunter und verteilt keine — es stellt nur den Platz dafuer bereit und zeigt
|
||||||
|
ehrlich an, was dort liegt. Die Datei bringt der Nutzer selbst mit.
|
||||||
|
|
||||||
|
QUELLEN (AGENTS Regel D — externe Schnittstellen nie aus dem Kopf):
|
||||||
|
* Datenverzeichnis und Dateiname GROSS geschrieben (unter Linux
|
||||||
|
case-sensitiv), dort geloest mit "cp KEYDB.cfg ~/.MakeMKV/":
|
||||||
|
https://forum.makemkv.com/forum/viewtopic.php?t=30636
|
||||||
|
* Heruntergeladene Schluessel liegen als hkd_*.bin in _private_data.tar:
|
||||||
|
https://forum.makemkv.com/forum/viewtopic.php?t=32675
|
||||||
|
* Zeilenformat der KEYDB.cfg (libaacs): Disc-Kennung = 40 Hex-Zeichen,
|
||||||
|
optional mit 0x-Praefix, dann "= Titel", danach optionale Felder wie
|
||||||
|
"| V | <32 Hex>"; Zeilen ab ";" sind Kommentare:
|
||||||
|
https://github.com/ShiftMediaProject/libaacs/blob/master/KEYDB.cfg
|
||||||
|
* Den AACS-Dump legt MakeMKV selbst ab, Meldung 3332 "Saved AACS dump file
|
||||||
|
as file:///root/.MakeMKV/<name>.tgz" (am 25.07.2026 so beobachtet).
|
||||||
|
|
||||||
|
ZWILLINGSDATEI: liegt identisch unter docker/api/ und docker/worker/ — beide
|
||||||
|
Images brauchen sie, ein gemeinsames Paket gibt es in diesem Projekt nicht
|
||||||
|
(gleiche Lage wie bei db.py). Aenderungen IMMER in BEIDEN Dateien nachziehen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
# Container-Pfad aus der Umgebung: /root/.MakeMKV im Worker (nur dort sucht
|
||||||
|
# makemkvcon), /app/makemkv-data in der API. Beide zeigen laut
|
||||||
|
# docker-compose.yml auf DASSELBE Host-Verzeichnis.
|
||||||
|
DATEN_DIR = os.getenv("MAKEMKV_DATA_DIR", "/root/.MakeMKV")
|
||||||
|
|
||||||
|
# GROSS geschrieben — unter Linux case-sensitiv, "keydb.cfg" wird ignoriert.
|
||||||
|
KEYDB_NAME = "KEYDB.cfg"
|
||||||
|
|
||||||
|
# Obergrenze fuer den Upload. Eine vollstaendige oeffentliche KEYDB.cfg liegt
|
||||||
|
# im einstelligen MB-Bereich; 64 MB sind reichlich Luft und verhindern, dass
|
||||||
|
# eine versehentlich hochgeladene Riesendatei den Speicher vollschreibt.
|
||||||
|
MAX_KEYDB_BYTES = 64 * 1024 * 1024
|
||||||
|
|
||||||
|
# Eine Disc-Zeile beginnt mit der 40 Zeichen langen Hex-Kennung (optional mit
|
||||||
|
# 0x davor), danach folgt das Gleichheitszeichen. Alles andere (Kommentare ab
|
||||||
|
# ";", Leerzeilen, Fortsetzungsfelder) zaehlt nicht als Eintrag.
|
||||||
|
_DISC_ZEILE = re.compile(r"^\s*(?:0x)?[0-9a-fA-F]{40}\s*=")
|
||||||
|
|
||||||
|
|
||||||
|
def _iso(zeitstempel: float) -> str:
|
||||||
|
"""Unix-Zeit -> ISO-8601 in UTC (sekundengenau), wie db.utcnow() es tut."""
|
||||||
|
return datetime.fromtimestamp(zeitstempel, timezone.utc).isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
|
||||||
|
def keydb_pfad(daten_dir: str = None) -> str:
|
||||||
|
"""Voller Pfad zur KEYDB.cfg im Datenverzeichnis."""
|
||||||
|
return os.path.join(daten_dir or DATEN_DIR, KEYDB_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def zaehle_disc_eintraege(inhalt: str) -> int:
|
||||||
|
"""Zeilen mit Disc-Kennung zaehlen (pure Funktion, testbar).
|
||||||
|
|
||||||
|
Bewusst eine Heuristik und keine vollstaendige Auswertung: MakeMKV liest
|
||||||
|
die Datei mit seinem eigenen Parser, und wie viele Eintraege es daraus
|
||||||
|
macht, ist von aussen nicht sichtbar. Die Zahl dient nur dazu, im UI
|
||||||
|
"da liegt wirklich etwas drin" von "leere oder falsche Datei" zu
|
||||||
|
unterscheiden — sie wird deshalb auch genau so beschriftet.
|
||||||
|
"""
|
||||||
|
return sum(1 for zeile in inhalt.splitlines() if _DISC_ZEILE.match(zeile))
|
||||||
|
|
||||||
|
|
||||||
|
def keydb_pruefen(inhalt: str) -> str:
|
||||||
|
"""Prueft hochgeladenen Inhalt; gibt deutschen Fehlertext oder "" zurueck.
|
||||||
|
|
||||||
|
Verhindert den haeufigsten Bedienfehler: statt der KEYDB.cfg landet die
|
||||||
|
HTML-Fehlerseite eines Downloads oder eine leere Datei im Verzeichnis —
|
||||||
|
MakeMKV wuerde dann still weiter "volume key is unknown" melden.
|
||||||
|
"""
|
||||||
|
if not inhalt.strip():
|
||||||
|
return "Die Datei ist leer."
|
||||||
|
if len(inhalt.encode("utf-8")) > MAX_KEYDB_BYTES:
|
||||||
|
return (
|
||||||
|
"Die Datei ist groesser als "
|
||||||
|
f"{MAX_KEYDB_BYTES // (1024 * 1024)} MB — das ist keine KEYDB.cfg."
|
||||||
|
)
|
||||||
|
if inhalt.lstrip()[:1] == "<":
|
||||||
|
return (
|
||||||
|
"Das sieht nach HTML aus, nicht nach einer KEYDB.cfg — "
|
||||||
|
"vermutlich wurde eine Fehlerseite statt der Datei geladen."
|
||||||
|
)
|
||||||
|
if zaehle_disc_eintraege(inhalt) == 0:
|
||||||
|
return (
|
||||||
|
"Keine einzige Zeile mit Disc-Kennung gefunden (40 Hex-Zeichen, "
|
||||||
|
"dann ein Gleichheitszeichen). Das ist keine KEYDB.cfg."
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def keydb_status(daten_dir: str = None) -> dict:
|
||||||
|
"""Zustand der KEYDB.cfg. Fehlt sie, ist das der Normalfall, kein Fehler."""
|
||||||
|
pfad = keydb_pfad(daten_dir)
|
||||||
|
try:
|
||||||
|
angaben = os.stat(pfad)
|
||||||
|
except OSError:
|
||||||
|
return {
|
||||||
|
"vorhanden": False,
|
||||||
|
"pfad": pfad,
|
||||||
|
"groesse_bytes": 0,
|
||||||
|
"eintraege": 0,
|
||||||
|
"geaendert": "",
|
||||||
|
}
|
||||||
|
eintraege = 0
|
||||||
|
try:
|
||||||
|
with open(pfad, encoding="utf-8", errors="replace") as datei:
|
||||||
|
eintraege = zaehle_disc_eintraege(datei.read())
|
||||||
|
except OSError:
|
||||||
|
pass # Datei da, aber unlesbar: Groesse/Datum stimmen trotzdem
|
||||||
|
return {
|
||||||
|
"vorhanden": True,
|
||||||
|
"pfad": pfad,
|
||||||
|
"groesse_bytes": angaben.st_size,
|
||||||
|
"eintraege": eintraege,
|
||||||
|
"geaendert": _iso(angaben.st_mtime),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def keydb_schreiben(inhalt: str, daten_dir: str = None) -> dict:
|
||||||
|
"""Schreibt die KEYDB.cfg atomar und meldet den neuen Zustand.
|
||||||
|
|
||||||
|
Erst in eine Nebendatei, dann os.replace: waehrend ein Rip laeuft, darf
|
||||||
|
makemkvcon niemals eine halb geschriebene Datei zu sehen bekommen.
|
||||||
|
"""
|
||||||
|
pfad = keydb_pfad(daten_dir)
|
||||||
|
os.makedirs(os.path.dirname(pfad), exist_ok=True)
|
||||||
|
neben = pfad + ".neu"
|
||||||
|
try:
|
||||||
|
with open(neben, "w", encoding="utf-8", newline="\n") as datei:
|
||||||
|
datei.write(inhalt)
|
||||||
|
os.replace(neben, pfad)
|
||||||
|
except OSError:
|
||||||
|
# Die Nebendatei nie liegen lassen: eine halb geschriebene
|
||||||
|
# KEYDB.cfg.neu verwirrt jeden, der ins Verzeichnis schaut, und
|
||||||
|
# belegt im Extremfall 64 MB, die niemand mehr aufräumt.
|
||||||
|
try:
|
||||||
|
os.remove(neben)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
return keydb_status(daten_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def keydb_loeschen(daten_dir: str = None) -> dict:
|
||||||
|
"""Entfernt die KEYDB.cfg (z. B. nach einem Fehlgriff beim Hochladen).
|
||||||
|
|
||||||
|
Nur "Datei war schon weg" wird geschluckt — das ist das gewünschte
|
||||||
|
Ergebnis. Jeder andere Fehler (schreibgeschützter Mount, fremder
|
||||||
|
Eigentümer) MUSS nach oben durch: sonst meldete die API einen Erfolg,
|
||||||
|
den es nicht gab, und die Datei wirkte beim nächsten Rip weiter.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
os.remove(keydb_pfad(daten_dir))
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
return keydb_status(daten_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def ist_aacs_dump(name: str) -> bool:
|
||||||
|
"""Dateiname eines AACS-Dumps? (pure Funktion, testbar)
|
||||||
|
|
||||||
|
MakeMKV legt ihn als <MKB..._NAME_....>.tgz direkt im Datenverzeichnis ab
|
||||||
|
(Meldung 3332). Pfadtrenner und fuehrende Punkte werden hier schon
|
||||||
|
ausgeschlossen, damit der Download-Endpunkt keine Pfad-Tricks erlaubt.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
name.endswith(".tgz")
|
||||||
|
and "/" not in name
|
||||||
|
and "\\" not in name
|
||||||
|
and not name.startswith(".")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def dumps_auflisten(daten_dir: str = None) -> list:
|
||||||
|
"""Alle AACS-Dumps im Datenverzeichnis, neueste zuerst."""
|
||||||
|
ordner = daten_dir or DATEN_DIR
|
||||||
|
try:
|
||||||
|
namen = os.listdir(ordner)
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
liste = []
|
||||||
|
for name in namen:
|
||||||
|
if not ist_aacs_dump(name):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
angaben = os.stat(os.path.join(ordner, name))
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
liste.append(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"groesse_bytes": angaben.st_size,
|
||||||
|
"geaendert": _iso(angaben.st_mtime),
|
||||||
|
"_sort": angaben.st_mtime,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
liste.sort(key=lambda eintrag: eintrag["_sort"], reverse=True)
|
||||||
|
for eintrag in liste:
|
||||||
|
del eintrag["_sort"]
|
||||||
|
return liste
|
||||||
|
|
||||||
|
|
||||||
|
def settings_conf_zusammenfuehren(inhalt: str, key: str) -> str:
|
||||||
|
"""app_Key setzen, ohne den Rest der settings.conf zu verlieren (pure).
|
||||||
|
|
||||||
|
Bis zum 25.07.2026 haben entrypoint.sh UND tasks.py die Datei komplett
|
||||||
|
ueberschrieben. Mit dem jetzt persistenten Datenverzeichnis waere damit
|
||||||
|
bei jedem Containerstart und vor jedem Rip alles andere weg — z. B.
|
||||||
|
app_UpdateEnable. Leerer Key laesst die vorhandene Zeile ebenfalls fallen,
|
||||||
|
damit ein bewusst geleerter Key nicht heimlich weiterwirkt.
|
||||||
|
"""
|
||||||
|
zeilen = [z for z in inhalt.splitlines() if not z.lstrip().startswith("app_Key")]
|
||||||
|
if key:
|
||||||
|
zeilen.append('app_Key = "{}"'.format(key))
|
||||||
|
text = "\n".join(zeilen).strip("\n")
|
||||||
|
return text + "\n" if text else ""
|
||||||
+53
-13
@@ -114,7 +114,7 @@ def lies_titel_info(device_path: str, timeout: int = 300) -> list:
|
|||||||
|
|
||||||
|
|
||||||
def rip_titel_auswahl(device_path: str, output_dir: str, titel_liste: list,
|
def rip_titel_auswahl(device_path: str, output_dir: str, titel_liste: list,
|
||||||
progress_cb=None) -> dict:
|
progress_cb=None, log_cb=None) -> dict:
|
||||||
"""Rippt GENAU die gewählten Titel (makemkvcon kann pro Aufruf nur einen
|
"""Rippt GENAU die gewählten Titel (makemkvcon kann pro Aufruf nur einen
|
||||||
Titel oder 'all' — also ein Aufruf je Titel, Fortschritt anteilig)."""
|
Titel oder 'all' — also ein Aufruf je Titel, Fortschritt anteilig)."""
|
||||||
gesamt = len(titel_liste)
|
gesamt = len(titel_liste)
|
||||||
@@ -124,7 +124,8 @@ def rip_titel_auswahl(device_path: str, output_dir: str, titel_liste: list,
|
|||||||
if progress_cb:
|
if progress_cb:
|
||||||
progress_cb(int((_i * 100 + p) / gesamt))
|
progress_cb(int((_i * 100 + p) / gesamt))
|
||||||
|
|
||||||
ergebnis = run_makemkv(device_path, output_dir, progress_cb=anteilig, titel=str(nr))
|
ergebnis = run_makemkv(device_path, output_dir, progress_cb=anteilig, titel=str(nr),
|
||||||
|
log_cb=log_cb)
|
||||||
if ergebnis.get("status") == "cancelled":
|
if ergebnis.get("status") == "cancelled":
|
||||||
return ergebnis
|
return ergebnis
|
||||||
if ergebnis.get("status") != "success":
|
if ergebnis.get("status") != "success":
|
||||||
@@ -181,6 +182,27 @@ def lies_datei_dauer(pfad: str, timeout: int = 120) -> int:
|
|||||||
return parse_scan_dauer((ergebnis.stdout or "") + (ergebnis.stderr or ""))
|
return parse_scan_dauer((ergebnis.stdout or "") + (ergebnis.stderr or ""))
|
||||||
|
|
||||||
|
|
||||||
|
_MSG_RE = re.compile(r'^MSG:(\d+),\d+,\d+,"((?:[^"\\]|\\.)*)"')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_msg(zeile: str):
|
||||||
|
"""MSG-Zeile -> (code, klartext) oder None (pure Funktion, testbar).
|
||||||
|
|
||||||
|
Format laut https://www.makemkv.com/developers/usage.txt:
|
||||||
|
MSG:code,flags,count,"message","format","param0",... — Feld 4 ist der
|
||||||
|
fertig zusammengesetzte Klartext.
|
||||||
|
|
||||||
|
Bis zum 25.07.2026 stand hier line.split(",", 4)[3]: das schnitt JEDE
|
||||||
|
Meldung ab, die selbst ein Komma enthaelt — und MakeMKV schreibt solche
|
||||||
|
laufend ("Title #1 has length of 12 seconds, which is less than ...").
|
||||||
|
Deshalb eine Regex, die die Anfuehrungszeichen respektiert.
|
||||||
|
"""
|
||||||
|
treffer = _MSG_RE.match(zeile.strip())
|
||||||
|
if not treffer:
|
||||||
|
return None
|
||||||
|
return int(treffer.group(1)), treffer.group(2).replace('\\"', '"')
|
||||||
|
|
||||||
|
|
||||||
def get_progress_from_prgv(line: str) -> int:
|
def get_progress_from_prgv(line: str) -> int:
|
||||||
"""Extrahiert Gesamt-Fortschritt (0-100) aus einer PRGV-Zeile.
|
"""Extrahiert Gesamt-Fortschritt (0-100) aus einer PRGV-Zeile.
|
||||||
|
|
||||||
@@ -297,8 +319,17 @@ def write_abcde_config(output_dir: str) -> str:
|
|||||||
return tmp.name
|
return tmp.name
|
||||||
|
|
||||||
|
|
||||||
def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str = "all") -> dict:
|
def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str = "all",
|
||||||
"""Rippt eine DVD/Blu-ray verlustfrei mit makemkvcon; meldet Fortschritt."""
|
log_cb=None) -> dict:
|
||||||
|
"""Rippt eine DVD/Blu-ray verlustfrei mit makemkvcon; meldet Fortschritt.
|
||||||
|
|
||||||
|
log_cb(code, text) bekommt JEDE MakeMKV-Meldung. Bewusst ein eigener
|
||||||
|
Kanal statt progress_cb: der Fortschritts-Callback in tasks.py verwirft
|
||||||
|
Aufrufe, bei denen sich die Prozentzahl nicht geaendert hat — Meldungen
|
||||||
|
waeren dort also grossteils verschwunden. Ohne diesen Kanal war am
|
||||||
|
25.07.2026 nicht von aussen erkennbar, dass MakeMKV bei der UHD-Disc
|
||||||
|
nicht einmal versucht, einen Schluessel zu holen (siehe makemkv_daten).
|
||||||
|
"""
|
||||||
if not check_makemkv_installed():
|
if not check_makemkv_installed():
|
||||||
return {"status": "error", "error": "makemkvcon ist nicht installiert"}
|
return {"status": "error", "error": "makemkvcon ist nicht installiert"}
|
||||||
|
|
||||||
@@ -328,15 +359,23 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str
|
|||||||
try:
|
try:
|
||||||
for line in process.stdout:
|
for line in process.stdout:
|
||||||
progress = get_progress_from_prgv(line)
|
progress = get_progress_from_prgv(line)
|
||||||
if progress >= 0 and progress_cb:
|
if progress >= 0:
|
||||||
|
if progress_cb:
|
||||||
progress_cb(progress)
|
progress_cb(progress)
|
||||||
elif line.startswith("MSG:"):
|
continue
|
||||||
# MSG:code,flags,count,"message",... — Klartext ist Feld 4
|
meldung = parse_msg(line)
|
||||||
teile = line.split(",", 4)
|
if meldung is None:
|
||||||
if len(teile) >= 4:
|
continue
|
||||||
letzte_meldung = teile[3].strip('"')
|
code, letzte_meldung = meldung
|
||||||
if any(muster in letzte_meldung for muster in KRITISCH):
|
if any(muster in letzte_meldung for muster in KRITISCH):
|
||||||
kritische_meldungen.append(letzte_meldung)
|
kritische_meldungen.append(letzte_meldung)
|
||||||
|
if log_cb:
|
||||||
|
try:
|
||||||
|
log_cb(code, letzte_meldung)
|
||||||
|
except RipAbbruch:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
pass # Protokollieren darf einen laufenden Rip nie beenden
|
||||||
except RipAbbruch:
|
except RipAbbruch:
|
||||||
process.kill()
|
process.kill()
|
||||||
process.wait()
|
process.wait()
|
||||||
@@ -373,7 +412,7 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str
|
|||||||
|
|
||||||
def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_cb=None,
|
def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_cb=None,
|
||||||
output_dir: str = None, nur_hauptfilm: bool = False,
|
output_dir: str = None, nur_hauptfilm: bool = False,
|
||||||
titel_liste: list = None) -> dict:
|
titel_liste: list = None, log_cb=None) -> dict:
|
||||||
"""Rippt eine DVD oder Blu-ray verlustfrei mit MakeMKV.
|
"""Rippt eine DVD oder Blu-ray verlustfrei mit MakeMKV.
|
||||||
|
|
||||||
Bewusst KEIN eigener Celery-Task: der einzige Task ist worker.tasks.rip_disc,
|
Bewusst KEIN eigener Celery-Task: der einzige Task ist worker.tasks.rip_disc,
|
||||||
@@ -388,7 +427,7 @@ def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_c
|
|||||||
|
|
||||||
if titel_liste:
|
if titel_liste:
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
return rip_titel_auswahl(device_path, output_dir, titel_liste, progress_cb)
|
return rip_titel_auswahl(device_path, output_dir, titel_liste, progress_cb, log_cb)
|
||||||
|
|
||||||
titel = "all"
|
titel = "all"
|
||||||
if nur_hauptfilm:
|
if nur_hauptfilm:
|
||||||
@@ -397,7 +436,8 @@ def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_c
|
|||||||
if haupt is not None:
|
if haupt is not None:
|
||||||
titel = str(haupt)
|
titel = str(haupt)
|
||||||
# Kein Titel ermittelbar → ehrlich auf 'all' zurückfallen statt raten
|
# Kein Titel ermittelbar → ehrlich auf 'all' zurückfallen statt raten
|
||||||
return run_makemkv(device_path, output_dir, progress_cb=progress_cb, titel=titel)
|
return run_makemkv(device_path, output_dir, progress_cb=progress_cb, titel=titel,
|
||||||
|
log_cb=log_cb)
|
||||||
|
|
||||||
|
|
||||||
def rip_cd(device_path: str, disc_id: str, progress_cb=None, output_dir: str = None) -> dict:
|
def rip_cd(device_path: str, disc_id: str, progress_cb=None, output_dir: str = None) -> dict:
|
||||||
|
|||||||
+64
-18
@@ -20,6 +20,7 @@ import shutil
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
import db
|
import db
|
||||||
|
import makemkv_daten
|
||||||
import medien
|
import medien
|
||||||
import notify
|
import notify
|
||||||
from celery_app import celery_app
|
from celery_app import celery_app
|
||||||
@@ -147,15 +148,26 @@ def _makemkv_key_anwenden(einstellungen: dict) -> None:
|
|||||||
|
|
||||||
Damit ist der Monats-Key ohne Rebuild/Neustart aktualisierbar
|
Damit ist der Monats-Key ohne Rebuild/Neustart aktualisierbar
|
||||||
(Einstellungen → System). Format wie entrypoint.sh: settings.conf.
|
(Einstellungen → System). Format wie entrypoint.sh: settings.conf.
|
||||||
|
|
||||||
|
Ergänzend statt überschreibend (Befund 25.07.2026): das Datenverzeichnis
|
||||||
|
ist jetzt persistent, und hier stand vorher ein open(..., "w") — das warf
|
||||||
|
vor JEDEM Rip alles andere aus der settings.conf, z. B. app_UpdateEnable
|
||||||
|
aus dem entrypoint. Beide Schreiber müssen gleich arbeiten, sonst kommt
|
||||||
|
der Fehler beim nächsten Rip still zurück.
|
||||||
"""
|
"""
|
||||||
key = (einstellungen.get("makemkvAppKey") or "").strip()
|
key = (einstellungen.get("makemkvAppKey") or "").strip()
|
||||||
if not key:
|
if not key:
|
||||||
return
|
return
|
||||||
ordner = os.path.expanduser("~/.MakeMKV")
|
pfad = os.path.join(makemkv_daten.DATEN_DIR, "settings.conf")
|
||||||
try:
|
try:
|
||||||
os.makedirs(ordner, exist_ok=True)
|
os.makedirs(makemkv_daten.DATEN_DIR, exist_ok=True)
|
||||||
with open(os.path.join(ordner, "settings.conf"), "w") as f:
|
try:
|
||||||
f.write(f'app_Key = "{key}"\n')
|
with open(pfad, encoding="utf-8", errors="replace") as f:
|
||||||
|
alt = f.read()
|
||||||
|
except OSError:
|
||||||
|
alt = ""
|
||||||
|
with open(pfad, "w", encoding="utf-8", newline="\n") as f:
|
||||||
|
f.write(makemkv_daten.settings_conf_zusammenfuehren(alt, key))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
db.add_log("warning", "worker", f"MakeMKV-Key konnte nicht gesetzt werden: {e}")
|
db.add_log("warning", "worker", f"MakeMKV-Key konnte nicht gesetzt werden: {e}")
|
||||||
|
|
||||||
@@ -344,6 +356,22 @@ def rip_disc(self, device_path: str, job_id: str, target_dir: str = None):
|
|||||||
)
|
)
|
||||||
db.update_job(job_id, progress=progress)
|
db.update_job(job_id, progress=progress)
|
||||||
|
|
||||||
|
# MakeMKV-Meldungen ins Log (Befund 25.07.2026): Bis dahin überlebte NUR
|
||||||
|
# die letzte Zeile ("Failed to open disc"), und die sagt nichts. Dass
|
||||||
|
# MakeMKV bei der UHD-Disc nicht einmal versucht, einen Schlüssel zu
|
||||||
|
# holen, war deshalb nur per Hand-Lauf im Container zu sehen.
|
||||||
|
# Gedrosselt, weil das UI global nur die letzten 200 Zeilen zeigt: jede
|
||||||
|
# Meldung höchstens einmal, insgesamt höchstens MAX_MELDUNGEN je Rip.
|
||||||
|
# Code 1003 ist MakeMKVs eigenes DEBUG-Rauschen (am 25.07. beobachtet).
|
||||||
|
MAX_MELDUNGEN = 40
|
||||||
|
gesehen = set()
|
||||||
|
|
||||||
|
def melde_makemkv(code: int, text: str):
|
||||||
|
if code == 1003 or len(gesehen) >= MAX_MELDUNGEN or text in gesehen:
|
||||||
|
return
|
||||||
|
gesehen.add(text)
|
||||||
|
db.add_log("info", "makemkv", f"Job {job_id}: {text[:300]}")
|
||||||
|
|
||||||
einstellungen = db.get_settings()
|
einstellungen = db.get_settings()
|
||||||
ist_video = disc_type in ("dvd", "bluray", "uhd")
|
ist_video = disc_type in ("dvd", "bluray", "uhd")
|
||||||
transcode_an = ist_video and einstellungen.get("transcodeEnabled", True)
|
transcode_an = ist_video and einstellungen.get("transcodeEnabled", True)
|
||||||
@@ -411,6 +439,7 @@ def rip_disc(self, device_path: str, job_id: str, target_dir: str = None):
|
|||||||
output_dir=raw_dir,
|
output_dir=raw_dir,
|
||||||
nur_hauptfilm=nur_hauptfilm,
|
nur_hauptfilm=nur_hauptfilm,
|
||||||
titel_liste=titel_liste,
|
titel_liste=titel_liste,
|
||||||
|
log_cb=melde_makemkv,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ergebnis = rip_video(
|
ergebnis = rip_video(
|
||||||
@@ -418,26 +447,43 @@ def rip_disc(self, device_path: str, job_id: str, target_dir: str = None):
|
|||||||
progress_cb=fortschritt, output_dir=final_dir,
|
progress_cb=fortschritt, output_dir=final_dir,
|
||||||
nur_hauptfilm=nur_hauptfilm,
|
nur_hauptfilm=nur_hauptfilm,
|
||||||
titel_liste=titel_liste,
|
titel_liste=titel_liste,
|
||||||
|
log_cb=melde_makemkv,
|
||||||
)
|
)
|
||||||
|
|
||||||
# UHD-Fehler in Klartext übersetzen — "Failed to open disc" allein hilft
|
# MakeMKV-Fehler in Klartext übersetzen — "Failed to open disc" allein
|
||||||
# niemandem. Zwei bekannte Ursachen (Befunde 24.07., Summer-Wars-UHD):
|
# hilft niemandem.
|
||||||
if ergebnis.get("status") == "error" and disc_type == "uhd":
|
if ergebnis.get("status") == "error":
|
||||||
fehler_text = ergebnis.get("error") or ""
|
fehler_text = ergebnis.get("error") or ""
|
||||||
if "volume key is unknown" in fehler_text:
|
if "volume key is unknown" in fehler_text:
|
||||||
# LibreDrive lief bereits — MakeMKV kennt nur den Disc-Schlüssel
|
# Befund 25.07.2026, im Worker nachgemessen (Akira UHD, MKB v76,
|
||||||
# nicht: Version zu alt ODER Disc neuer als die Key-Datenbank.
|
# Pressung Dez. 2020): Laufwerk und MakeMKV sind in Ordnung —
|
||||||
|
# MakeMKV fragt online gar nicht erst nach einem Schlüssel, und
|
||||||
|
# der Online-Kanal liefert auch nichts mehr. Der alte Text hier
|
||||||
|
# ("Disc neuer als die Schlüssel-Datenbank, mit einem der
|
||||||
|
# nächsten Updates rippbar") war schlicht falsch und hat in die
|
||||||
|
# falsche Richtung geschickt. Details: makemkv_daten.py.
|
||||||
|
keydb = makemkv_daten.keydb_status()
|
||||||
ergebnis["error"] += (
|
ergebnis["error"] += (
|
||||||
" — Klartext: Das Laufwerk liest die Disc (LibreDrive OK), "
|
" — Klartext: Laufwerk und Rippy sind in Ordnung, MakeMKV "
|
||||||
"aber MakeMKV kennt den Schlüssel dieser Disc nicht. Erst "
|
"liest die Disc. Es kennt nur den Schlüssel dieser Pressung "
|
||||||
"prüfen: MakeMKV aktuell? (Einstellungen → System; Update = "
|
"nicht und holt ihn auch nicht mehr online nach — MakeMKVs "
|
||||||
"Image-Rebuild). Ist es aktuell, ist die Disc neuer als die "
|
"Schlüssel-Kanal liefert nichts mehr (am 25.07.2026 im "
|
||||||
"Schlüssel-Datenbank — MakeMKV hat einen AACS-Dump unter "
|
"Worker nachgemessen). Abhilfe: eine KEYDB.cfg unter "
|
||||||
"/root/.MakeMKV/ im Worker gespeichert; im MakeMKV-Forum "
|
"Einstellungen → System hochladen; sie wirkt ab dem nächsten "
|
||||||
"(Bereich 'Ultra HD Blu-ray') einreichen, mit einem der "
|
"Rip. "
|
||||||
"nächsten Updates ist die Disc dann rippbar."
|
+ (
|
||||||
|
"Aktuell liegt dort keine KEYDB.cfg."
|
||||||
|
if not keydb.get("vorhanden")
|
||||||
|
else "Es liegt bereits eine KEYDB.cfg dort — sie kennt "
|
||||||
|
"diese Pressung offenbar nicht; eine neuere Fassung "
|
||||||
|
"kann helfen."
|
||||||
)
|
)
|
||||||
elif "Failed to open disc" in fehler_text:
|
+ " Den AACS-Dump dieser Disc bewahrt Rippy jetzt dauerhaft "
|
||||||
|
"auf; er steht unter Einstellungen → System zum Download "
|
||||||
|
"bereit (für die Einreichung im MakeMKV-Forum, Bereich "
|
||||||
|
"'Ultra HD Blu-ray')."
|
||||||
|
)
|
||||||
|
elif disc_type == "uhd" and "Failed to open disc" in fehler_text:
|
||||||
ergebnis["error"] += (
|
ergebnis["error"] += (
|
||||||
" — 4K-UHD erkannt: Das Laufwerk kann UHD-Discs vermutlich "
|
" — 4K-UHD erkannt: Das Laufwerk kann UHD-Discs vermutlich "
|
||||||
"nicht entschlüsseln. Dafür ist eine LibreDrive-Firmware nötig "
|
"nicht entschlüsseln. Dafür ist eine LibreDrive-Firmware nötig "
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
"""Tests fuer makemkv_daten.py: die reinen Helfer rund um das MakeMKV-Datenverzeichnis.
|
||||||
|
|
||||||
|
WARUM ES DIESE TESTS GIBT (Befund 25.07.2026, live im Worker nachgemessen):
|
||||||
|
Eine 4K-UHD-Disc (Akira UHD, MKB v76) scheiterte mit "The volume key is unknown
|
||||||
|
for this disc", obwohl Laufwerk und MakeMKV in Ordnung waren. Der einzige heute
|
||||||
|
noch funktionierende Weg ist eine selbst mitgebrachte KEYDB.cfg im
|
||||||
|
Datenverzeichnis. Damit haengt einiges an diesen kleinen Funktionen: erkennen wir
|
||||||
|
die Datei falsch, meldet das UI "alles gut", waehrend MakeMKV weiter scheitert.
|
||||||
|
|
||||||
|
Getestet wird nur, was ohne Postgres, Redis und ohne Laufwerk laeuft — also die
|
||||||
|
puren Funktionen mit echten Beispieldaten. Zeilenformat der KEYDB.cfg laut
|
||||||
|
libaacs (AGENTS Regel D, externe Schnittstellen nie aus dem Kopf):
|
||||||
|
https://github.com/ShiftMediaProject/libaacs/blob/master/KEYDB.cfg
|
||||||
|
|
||||||
|
WARUM DER DATEINAME "_worker" HINTEN DRANHAENGT (25.07.2026): makemkv_daten.py
|
||||||
|
ist eine Zwillingsdatei, es gibt sie unter docker/api/ UND docker/worker/, und
|
||||||
|
beide Seiten haben Tests. Da im Projekt keine __init__.py liegen, importiert
|
||||||
|
pytest Testdateien unter ihrem blossen Dateinamen — zwei Dateien namens
|
||||||
|
test_makemkv_daten.py brechen deshalb die Sammelphase ab ("import file
|
||||||
|
mismatch") und faerben die ganze Ampel rot. Nicht zurueckbenennen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
|
||||||
|
# WICHTIG (Prüfbefund 25.07.2026): Ein schlichtes "from makemkv_daten import ..."
|
||||||
|
# lädt bei "pytest -q" vom Repo-Wurzelverzeichnis NICHT diese Datei, sondern die
|
||||||
|
# API-Kopie — docker/api wird zuerst gesammelt, und jeder weitere Import trifft
|
||||||
|
# nur noch den sys.modules-Cache. Die Tests hier hätten den Worker-Zwilling also
|
||||||
|
# nie angefasst und eine Abweichung wäre grün durchgelaufen. Deshalb wird er
|
||||||
|
# ausdrücklich über seinen Pfad geladen.
|
||||||
|
_HIER = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
_WORKER_MODUL = os.path.join(_HIER, "makemkv_daten.py")
|
||||||
|
_API_MODUL = os.path.abspath(os.path.join(_HIER, "..", "api", "makemkv_daten.py"))
|
||||||
|
|
||||||
|
_spec = importlib.util.spec_from_file_location("makemkv_daten_worker_kopie", _WORKER_MODUL)
|
||||||
|
_modul = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(_modul)
|
||||||
|
|
||||||
|
ist_aacs_dump = _modul.ist_aacs_dump
|
||||||
|
keydb_pruefen = _modul.keydb_pruefen
|
||||||
|
settings_conf_zusammenfuehren = _modul.settings_conf_zusammenfuehren
|
||||||
|
zaehle_disc_eintraege = _modul.zaehle_disc_eintraege
|
||||||
|
|
||||||
|
|
||||||
|
def test_zwillinge_sind_byteweise_identisch():
|
||||||
|
"""docker/api/makemkv_daten.py MUSS dieselbe Datei sein wie diese hier.
|
||||||
|
|
||||||
|
Das Modul existiert bewusst doppelt — es gibt in diesem Projekt kein
|
||||||
|
gemeinsames Paket für API und Worker (gleiche Lage wie bei db.py). Genau
|
||||||
|
deshalb braucht es einen Wächter: laufen die beiden auseinander, zeigt das
|
||||||
|
UI etwas anderes an, als der rippende Worker tatsächlich sieht, und es
|
||||||
|
fällt niemandem auf. Dieser Test ist die einzige Stelle, die das
|
||||||
|
mechanisch prüft.
|
||||||
|
"""
|
||||||
|
with open(_WORKER_MODUL, "rb") as datei:
|
||||||
|
worker = hashlib.sha256(datei.read()).hexdigest()
|
||||||
|
with open(_API_MODUL, "rb") as datei:
|
||||||
|
api = hashlib.sha256(datei.read()).hexdigest()
|
||||||
|
assert worker == api, (
|
||||||
|
"docker/worker/makemkv_daten.py und docker/api/makemkv_daten.py sind "
|
||||||
|
"auseinandergelaufen - Aenderungen immer in BEIDE Dateien uebernehmen."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Eine kleine, aber echte KEYDB.cfg im libaacs-Format: Kommentarkopf, eine
|
||||||
|
# Disc-Zeile MIT 0x-Praefix, eine OHNE, dazu ein Fortsetzungsfeld und eine
|
||||||
|
# Leerzeile. Erwartete Zahl der Eintraege: 2.
|
||||||
|
BEISPIEL_KEYDB = """; KEYDB.cfg
|
||||||
|
; Kommentarzeilen beginnen mit einem Semikolon
|
||||||
|
|
||||||
|
0x8F4E2C1A9B7D3E5F0A6C8B2D4E1F3A5C7B9D0E2F = AKIRA
|
||||||
|
| V | 0123456789ABCDEF0123456789ABCDEF
|
||||||
|
|
||||||
|
A1B2C3D4E5F60718293A4B5C6D7E8F90A1B2C3D4 = BLADE RUNNER 2049
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_zaehle_disc_eintraege_zaehlt_nur_echte_disc_zeilen():
|
||||||
|
"""Nur Zeilen mit 40 Hex-Zeichen und Gleichheitszeichen sind Eintraege.
|
||||||
|
|
||||||
|
Kommentare, Leerzeilen und Fortsetzungsfelder duerfen nicht mitzaehlen —
|
||||||
|
sonst meldet das UI bei einer reinen Kommentardatei stolz "42 Eintraege".
|
||||||
|
"""
|
||||||
|
assert zaehle_disc_eintraege(BEISPIEL_KEYDB) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_zaehle_disc_eintraege_ignoriert_kommentare_und_leerzeilen():
|
||||||
|
# Eine Datei ganz ohne Disc-Zeile hat null Eintraege, nicht drei.
|
||||||
|
nur_beiwerk = "; nur ein Kommentar\n\n| V | 0123456789ABCDEF0123456789ABCDEF\n"
|
||||||
|
assert zaehle_disc_eintraege(nur_beiwerk) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_zaehle_disc_eintraege_ignoriert_zu_kurze_kennung():
|
||||||
|
"""39 Hex-Zeichen sind keine Disc-Kennung.
|
||||||
|
|
||||||
|
Genau so sieht eine beim Kopieren verstuemmelte Datei aus — die darf nicht
|
||||||
|
als gueltig durchgehen, sonst sucht der Commander den Fehler beim Laufwerk.
|
||||||
|
"""
|
||||||
|
zu_kurz = "A1B2C3D4E5F60718293A4B5C6D7E8F90A1B2C3D = KAPUTT\n"
|
||||||
|
assert zaehle_disc_eintraege(zu_kurz) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_keydb_pruefen_meldet_leere_datei():
|
||||||
|
# Haeufigster Fehlgriff: das Textfeld war leer, es wird trotzdem gespeichert.
|
||||||
|
assert keydb_pruefen("") != ""
|
||||||
|
assert keydb_pruefen(" \n\n ") != ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_keydb_pruefen_erkennt_html_fehlerseite():
|
||||||
|
"""Der zweithaeufigste Fehlgriff: der Download lieferte eine HTML-Seite.
|
||||||
|
|
||||||
|
MakeMKV wuerde die Datei still ignorieren und weiter "volume key is unknown"
|
||||||
|
melden — deshalb muss der Fehler schon beim Hochladen sichtbar werden.
|
||||||
|
"""
|
||||||
|
html = "<!DOCTYPE html>\n<html><body><h1>404 Not Found</h1></body></html>\n"
|
||||||
|
meldung = keydb_pruefen(html)
|
||||||
|
assert meldung != ""
|
||||||
|
assert "HTML" in meldung
|
||||||
|
|
||||||
|
|
||||||
|
def test_keydb_pruefen_meldet_text_ohne_disc_zeile():
|
||||||
|
# Irgendein Text (hier: eine README) ist keine KEYDB.cfg.
|
||||||
|
meldung = keydb_pruefen("Diese Datei enthaelt keine Schluessel, nur Prosa.\n")
|
||||||
|
assert meldung != ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_keydb_pruefen_akzeptiert_gueltigen_inhalt():
|
||||||
|
# Leerer Rueckgabewert heisst laut Vertrag: alles in Ordnung.
|
||||||
|
assert keydb_pruefen(BEISPIEL_KEYDB) == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_ist_aacs_dump_akzeptiert_echten_namen():
|
||||||
|
"""Name aus der Praxis: so legt MakeMKV den Dump laut Meldung 3332 ab
|
||||||
|
(am 25.07.2026 im Worker so beobachtet)."""
|
||||||
|
assert ist_aacs_dump("MKB20_v76_UHD_AKIRA_C02B.tgz") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_ist_aacs_dump_lehnt_pfad_tricks_und_fremde_dateien_ab():
|
||||||
|
"""Der Download-Endpunkt haengt den Namen an das Datenverzeichnis an —
|
||||||
|
ohne diese Pruefung koennte man sich damit aus dem Verzeichnis heraus
|
||||||
|
lesen. Versteckte Dateien und Nicht-Dumps sind ebenfalls nichts fuer die
|
||||||
|
Liste."""
|
||||||
|
assert ist_aacs_dump("../ausbruch.tgz") is False
|
||||||
|
assert ist_aacs_dump(".versteckt.tgz") is False
|
||||||
|
assert ist_aacs_dump("irgendwas.txt") is False
|
||||||
|
assert ist_aacs_dump("..\\windows\\ausbruch.tgz") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_conf_ersetzt_key_und_behaelt_den_rest():
|
||||||
|
"""DIE Regression, um die es geht: bis zum 25.07.2026 haben entrypoint.sh
|
||||||
|
und tasks.py die settings.conf komplett ueberschrieben. Mit dem jetzt
|
||||||
|
persistenten Datenverzeichnis waere damit bei jedem Containerstart und vor
|
||||||
|
jedem Rip alles andere weg — allen voran app_UpdateEnable."""
|
||||||
|
alt = 'app_Key = "T-alterSchluessel"\napp_UpdateEnable = "1"\napp_DefaultSelectionString = "+sel:all"\n'
|
||||||
|
neu = settings_conf_zusammenfuehren(alt, "T-neuerSchluessel")
|
||||||
|
assert 'app_Key = "T-neuerSchluessel"' in neu
|
||||||
|
assert 'app_Key = "T-alterSchluessel"' not in neu
|
||||||
|
assert 'app_UpdateEnable = "1"' in neu
|
||||||
|
assert 'app_DefaultSelectionString = "+sel:all"' in neu
|
||||||
|
# Genau EINE app_Key-Zeile, sonst gewinnt am Ende die falsche.
|
||||||
|
assert neu.count("app_Key") == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_conf_leerer_key_entfernt_die_zeile():
|
||||||
|
# Ein bewusst geleerter Key darf nicht heimlich weiterwirken.
|
||||||
|
alt = 'app_Key = "T-alterSchluessel"\napp_UpdateEnable = "1"\n'
|
||||||
|
neu = settings_conf_zusammenfuehren(alt, "")
|
||||||
|
assert "app_Key" not in neu
|
||||||
|
assert 'app_UpdateEnable = "1"' in neu
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_conf_aus_dem_nichts_ergibt_saubere_datei():
|
||||||
|
"""Erststart: die Datei gibt es noch gar nicht. Der abschliessende
|
||||||
|
Zeilenumbruch ist Absicht — MakeMKV liest die Datei zeilenweise."""
|
||||||
|
assert settings_conf_zusammenfuehren("", "T-neuerSchluessel") == 'app_Key = "T-neuerSchluessel"\n'
|
||||||
|
|
||||||
|
|
||||||
|
def test_settings_conf_ohne_key_und_ohne_inhalt_bleibt_leer():
|
||||||
|
# Kein Inhalt, kein Key: keine Datei mit einer einsamen Leerzeile erzeugen.
|
||||||
|
assert settings_conf_zusammenfuehren("", "") == ""
|
||||||
@@ -13,6 +13,7 @@ from ripping import (
|
|||||||
build_makemkv_cmd,
|
build_makemkv_cmd,
|
||||||
get_progress_from_line,
|
get_progress_from_line,
|
||||||
get_progress_from_prgv,
|
get_progress_from_prgv,
|
||||||
|
parse_msg,
|
||||||
write_abcde_config,
|
write_abcde_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,6 +64,60 @@ def test_prgv_parsing_ignoriert_fremde_zeilen():
|
|||||||
assert get_progress_from_prgv("PRGV:kaputt") == -1
|
assert get_progress_from_prgv("PRGV:kaputt") == -1
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_msg_trennt_code_und_klartext():
|
||||||
|
"""Echte Zeilen aus einem makemkvcon-Lauf vom 25.07.2026 (Akira UHD).
|
||||||
|
|
||||||
|
Feld 4 ist laut https://www.makemkv.com/developers/usage.txt der fertig
|
||||||
|
zusammengesetzte Klartext — genau der landet im Rippy-Log.
|
||||||
|
"""
|
||||||
|
assert parse_msg(
|
||||||
|
'MSG:1005,0,1,"MakeMKV v1.18.4 linux(x64-release) started","%1 started","MakeMKV v1.18.4 linux(x64-release)"'
|
||||||
|
) == (1005, "MakeMKV v1.18.4 linux(x64-release) started")
|
||||||
|
assert parse_msg(
|
||||||
|
'MSG:1011,0,1,"Using LibreDrive mode (v06.3 id=866A98CB9C4E)","%1","Using LibreDrive mode (v06.3 id=866A98CB9C4E)"'
|
||||||
|
) == (1011, "Using LibreDrive mode (v06.3 id=866A98CB9C4E)")
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_msg_liest_die_uhd_fehlermeldung():
|
||||||
|
"""3303 ist der Befund, um den es beim ganzen KEYDB-Thema geht: das
|
||||||
|
Laufwerk laeuft im LibreDrive-Modus, MakeMKV kennt nur den Schluessel
|
||||||
|
DIESER Pressung nicht. Ohne diese Zeile im Log raet der Commander."""
|
||||||
|
assert parse_msg(
|
||||||
|
'MSG:3303,16777216,0,"The volume key is unknown for this disc - video can\'t be decrypted","The volume key is unknown for this disc - video can\'t be decrypted"'
|
||||||
|
) == (3303, "The volume key is unknown for this disc - video can't be decrypted")
|
||||||
|
assert parse_msg('MSG:5010,0,0,"Failed to open disc","Failed to open disc"') == (
|
||||||
|
5010,
|
||||||
|
"Failed to open disc",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_msg_schneidet_meldungen_mit_komma_nicht_ab():
|
||||||
|
"""DER Grund fuer die Regex (Stand 25.07.2026): vorher stand hier
|
||||||
|
line.split(",", 4)[3]. Das schnitt jede Meldung ab, die selbst ein Komma
|
||||||
|
enthaelt — und MakeMKV schreibt solche laufend. Im Log stand dann nur noch
|
||||||
|
ein Satzfragment, das mehr verwirrt als hilft."""
|
||||||
|
zeile = (
|
||||||
|
'MSG:3025,0,3,"Title #1 has length of 12 seconds, which is less than '
|
||||||
|
'minimum title length of 120 seconds and was therefore skipped",'
|
||||||
|
'"Title #%1 has length of %2 seconds which is less than minimum title '
|
||||||
|
'length of %3 seconds and was therefore skipped","1","12","120"'
|
||||||
|
)
|
||||||
|
code, text = parse_msg(zeile)
|
||||||
|
assert code == 3025
|
||||||
|
assert text.endswith("and was therefore skipped")
|
||||||
|
assert "which is less than" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_msg_ignoriert_fremde_zeilen():
|
||||||
|
# Alles ausser MSG muss None liefern, sonst landet Fortschritts-Rauschen
|
||||||
|
# (PRGV kommt mehrmals pro Sekunde) als Log-Eintrag in der Datenbank.
|
||||||
|
assert parse_msg("PRGV:100,32768,65536") is None
|
||||||
|
assert parse_msg('DRV:0,2,999,12,"BD-RE ASUS BW-16D1HT","AKIRA","/dev/sr0"') is None
|
||||||
|
assert parse_msg("TCOUNT:5") is None
|
||||||
|
assert parse_msg("") is None
|
||||||
|
assert parse_msg("irgendwelcher Muell ohne Struktur") is None
|
||||||
|
|
||||||
|
|
||||||
def test_abcde_cmd_hat_genau_ein_ausgabeformat():
|
def test_abcde_cmd_hat_genau_ein_ausgabeformat():
|
||||||
"""Review-Fund 22.07.: '-o' stand doppelt (Format UND Verzeichnis) — abcde
|
"""Review-Fund 22.07.: '-o' stand doppelt (Format UND Verzeichnis) — abcde
|
||||||
parste das Verzeichnis als Format, CD-Ripping war nie funktionsfähig."""
|
parste das Verzeichnis als Format, CD-Ripping war nie funktionsfähig."""
|
||||||
|
|||||||
Reference in New Issue
Block a user