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