Files
rippy/SAVEPOINT.md
T
Hitonabi 4c2331563c
Ampel / ampel (push) Successful in 31s
docs(savepoint): v3.18 - Auswurf, externer Worker, und die Mount-Ursache
Stellt eine Aussage aus v3.17 richtig: Dort stand die Mount-Sache als "Ursache
liegt beim NAS, nicht gefunden". Gemessen liegt sie bei uns - die CIFS-Verbindung
lebt in der Netz-Namespace des api-Containers und stirbt mit ihm. Das NAS ist
unschuldig.

AGENTS.md bekommt die Lehre, die diesen Abend zweimal gekostet hat: Ein
Rueckgabewert ist kein Beweis, wo die Wirkung pruefbar ist. CDROMEJECT quittiert
Erfolg auf einem verriegelten Laufwerk, `mount` quittiert Erfolg auf einer
Verbindung, die Sekunden spaeter stirbt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:40:31 +02:00

1726 lines
96 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SAVEPOINT — Rippy
## Aktueller Stand: v3.18 — Auswurf, externer Worker, und DIE Mount-Ursache (26.07.2026, Abend)
> **Deployt und live gegengeprüft.** Zwei Commander-Meldungen: Das Laufwerk geht
> nach dem Rip nicht auf, und der externe Worker „ist ein bisschen dünn". Beide
> abgearbeitet — und unterwegs ist die Ursache gefallen, die v3.17 noch als
> „liegt beim NAS, nicht gefunden" führte. Sie lag bei uns.
### ZUSTAND, gemessen
| | |
|---|---|
| Repo + VM | `4cf7acb`, Ampel **grün**, deployt |
| Tests | **228** grün (Sitzungsbeginn: 130) |
| Freigabe nach dem Deploy | **heilt sich selbst in ~150 s**, ohne Handgriff |
| Job `95afdc89` | `can_retry = true`, 74,1 GB erreichbar |
| ⚠️ Laufwerks-Schublade | **steht OFFEN** — vom Auswurf-Test, per Software nicht schließbar (siehe unten) |
### 1. DER AUSWURF: das ioctl meldete Erfolg und tat nichts
Commander: *„Den Button gibt es in den Settings, aber es passiert nicht, das
Laufwerk geht nicht auf."* An der Disc, die gerade drin lag, nachgestellt:
```
wirf_disc_aus("/dev/sr0") → True
CDROM_DRIVE_STATUS danach → 4 (Disc drin)
```
Das ioctl wird **angenommen und tut nichts**. Ursache: MakeMKV verriegelt während
des Rips die Laufwerkstür (`CDROM_LOCKDOOR 1`) und entriegelt sie nicht wieder.
Ein verriegeltes Laufwerk quittiert den Auswurf trotzdem mit Erfolg. Gegenprobe
an derselben Disc:
```
CDROM_LOCKDOOR 0 + CDROMEJECT → Status 2 (SCHUBLADE OFFEN)
```
Genau das macht das Werkzeug `eject` immer: erst entriegeln, dann auswerfen.
**Die wichtigere Hälfte des Fixes:** Das Ergebnis wird jetzt GEPRÜFT statt
geglaubt (bis zu 5 s, die Schublade braucht ein bis zwei; „kein Datenträger"
zählt mit, weil ein Slot-Laufwerk keine Schublade hat). Deshalb ist der Fehler
in v3.14 durchgerutscht: Dort wurde richtig festgestellt, dass die Einstellung
von niemandem gelesen wurde — danach WURDE sie gelesen, ausgeworfen wurde
weiterhin nicht, und im Log stand „Disc ausgeworfen". Dieselbe Lücke steckte im
Auswurf-Knopf der API.
**Live bewiesen** (API-Weg, also der Knopf im UI): aus „Disc drin" wurde in
**0,67 s** „Schublade offen".
**Zum eigentlichen Ziel — zweite Disc parallel:** Das war nur am Laufwerk
gescheitert. `has_active_job` blockiert ausschließlich bei `pending`/`running`,
ein Job in `transcoding` gibt das Gerät also längst frei, und der Worker läuft
ohne `--concurrency`, also mit 4 Slots. Der Auswurf sitzt auch an der richtigen
Stelle: nach dem Rip, VOR dem Einreihen der Kompression.
⚠️ **Die Schublade der VM steht jetzt offen.** Der Test hat sie geöffnet, und
`CDROMCLOSETRAY` bleibt wirkungslos (viermal versucht, mit bis zu 15 s Wartezeit
je Versuch) — das BU40N will von Hand zugeschoben werden. Deshalb ist der
AUTOMATISCHE Weg (`wirf_disc_aus` nach einem Rip) nicht noch einmal live
gemessen: identischer Code, durch Tests gedeckt, aber der Live-Beweis steht nur
für den API-Weg.
### 2. DER EXTERNE WORKER: Log nach Rippy, Anzeige, Slots
Commander: *„der externe Encoder Worker ist ein bisschen dünn — der könnte noch
viel mehr"*, dazu ausdrücklich *„bessere Log-Ansichten (kein txt file → direkt
von Rippy Logs)"*.
**Log geht nach Rippy** (`worker/logbruecke.py`). Vorher schrieb das Tray nach
`%LOCALAPPDATA%` und öffnete die Datei im Editor — wer wissen wollte, warum der
Worker nichts tut, musste sich an den PC setzen. Jetzt landen die wichtigen
Zeilen in Rippys Log-Tabelle (Quelle `w:<name>`), und die Logs-Seite hat Knöpfe
je Quelle: „was macht mein PC" ist ein Klick.
Durchgelassen wird **wenig**, und das mit Grund: Die Job-Meldungen stehen längst
in Rippy (`tasks.py` schreibt sie selbst). Es fehlte, was DANEBEN passiert und
den Worker unbrauchbar macht, ohne dass ein Job existiert — hochgefahren oder
nicht, Verbindung zu Redis/Postgres, Abstürze. Alles andere fliegt weg: Celery
ist bei `--loglevel=info` gesprächig, die `logs`-Tabelle hat **keine**
automatische Aufräumung. Dazu eine Drossel (30 Zeilen/Minute), die MELDET,
wieviel sie verschluckt hat. Das Zeilenformat ist wörtlich aus dem laufenden
Container abgenommen (Celery 5.4.0). Die lokale Datei bleibt — sie ist genau dann
die einzige Auskunft, wenn Rippy nicht erreichbar ist.
**Das Tray zeigt, was läuft.** Vorher stand dort „läuft" oder „gestoppt" — auf
einer Maschine, die stundenlang an einem Film rechnet, ist das keine Auskunft.
Jetzt Titel, Prozent und Restzeit, geholt von Rippys `/jobs`. Bewusst dieselbe
Quelle wie das Dashboard, damit im Tray nicht eine zweite, abweichende Schätzung
steht. Dazu: **Windows schläft nicht mehr mitten im Encode ein**
(`SetThreadExecutionState`, ohne `ES_DISPLAY_REQUIRED` — der Bildschirm darf
ausgehen). Die Sperre wird zurückgenommen, sobald nichts läuft, und auch bei
einem harten Ende des Trays — sonst schläft der PC nie wieder ein und niemand
weiß warum.
**Mehrere Encodes gleichzeitig.** Der Worker lief fest mit `--pool=solo` und nahm
genau EINEN Auftrag an. Der Installer fragt die Zahl jetzt (Feld neben dem Namen,
erkannte Kernzahl daneben), Vorbelegung ab 12 Kernen zwei, sonst einer: HandBrake
nutzt schon alle Kerne, aber x265 skaliert nicht linear. Auf Windows gibt es
keinen prefork-Pool (kein `fork`) — deshalb `--pool=threads`, was hier passt,
weil die Arbeit ein Kind-Prozess ist und der Thread nur wartet.
**Zur Frage des Commanders** *„würden wir mit auch rippen können' nicht den Sinn
von Rippy aushebeln?"* — teilweise ja, und die Antwort steht als Empfehlung:
Ein Windows-Ripper bräuchte eine **zweite komplette Laufwerks-Erkennung**
(`detection.py` steckt voller Linux-ioctls), also einen zweiten Unterbau mit
eigenen Fehlern. Der einzige harte Vorteil ist, dass MakeMKV unter Windows die
4K-Disc-Schlüssel selbst holt. Das ist viel billiger zu haben: ein kleiner
Helfer, der bei eingelegter Disc MakeMKV öffnen lässt und `_private_data.tar`
automatisch zu Rippy hochlädt. **Entscheidung steht beim Commander, nichts
gebaut.**
### 3. DIE MOUNT-URSACHE — sie lag nicht am NAS, sondern bei uns
v3.17 führte das als „Ursache liegt beim NAS, nicht gefunden". Gemessen in
`/proc/fs/cifs/DebugData`:
```
Mount-Verbindung → Net namespace: 4026532653
api-Container → net:[4026532653] ← DIESELBE
worker-Container → net:[4026532540] ← andere
```
**Die CIFS-Verbindung lebt in der Netz-Namespace des api-Containers** — dort wird
sie eingehängt, weil nur dieser Container `CAP_SYS_ADMIN` hat. Wird der Container
neu gebaut, stirbt sein Netz-Namespace und mit ihm der Socket. Der Mount steht
danach weiter in `/proc/mounts` (per rshared auf den Host propagiert) und sieht
vollkommen gesund aus — aber jeder Zugriff läuft in den CIFS-Timeout.
Damit erklärt sich alles, was vorher widersprüchlich aussah: warum es nach JEDEM
Deploy passiert, warum `mount` Erfolg meldet, warum genau eine korrekte Schicht
in `/proc/mounts` steht, und warum nur ein echtes Neu-Verbinden hilft. **Das NAS
ist unschuldig** — eine Sitzung, alles Status 1, 630 Credits, Ping 0,47 ms.
**Zweiter Fund, der den Rest erklärt:** Direkt nach einem frischen Mount
antwortete die Freigabe — und Sekunden später nicht mehr. Das ist ein Wettlauf
mit `umount -l`: lazy heißt, der Abbau passiert später, und fällt er samt
Propagation hinter den neuen Mount, zeigt der Pfad wieder auf die Leiche. Eine
einzige Probe kann das nicht sehen — deshalb prüft `wirklich_erreichbar()`
**zweimal mit drei Sekunden Abstand**.
**Gebaut:** eine Mount-Wache (erste Prüfung nach 10 s, dann minütlich), die
stumme Freigaben neu verbindet. Zwei Dinge daran sind wichtiger als die Heilung:
Sie rührt **nie** etwas an, solange irgendein Job nicht durch ist (neu verbinden
heißt `umount -l`; mitten in einem Rip wäre das Datenverlust — `db.hat_arbeit()`,
`pending` zählt mit), und sie meldet nur den **Übergang**, nicht jede Minute
(ein ausgeschaltetes NAS wäre sonst ein Log-Wasserfall).
**Live bewiesen, ohne einen Handgriff:** Nach `docker compose up -d --build` war
die Freigabe stumm; nach rund 150 s stand im Log „antwortet nicht — wird neu
verbunden" → „neu verbunden", und `can_retry` war wieder `true`. Vorher heilte
das **nie** von selbst.
### NOCH OFFEN
**1. Der api-Container HÄLT die NAS-Verbindung.** Startet er mitten in einem Rip
neu, verliert auch der Worker sein Ziel — das ist die strukturelle Folge des
Namespace-Befunds. Das saubere Gegenmittel wäre ein Mount auf dem HOST
(systemd/fstab) statt im Container; das ist ein eigener Umbau und widerspräche
„Speicherziele über das UI einhängen" (Commander-Anforderung 23.07.). **Zu
entscheiden.** Bis dahin gilt: nicht deployen, während ein Rip läuft.
**2. Die Schublade der VM steht offen** und braucht einen Handgriff (siehe oben).
**3. Der Auswurf nach einem echten Rip ist nicht live gemessen** — identischer
Code wie der bewiesene API-Weg, durch Tests gedeckt, aber der Beweis fehlt, weil
die Schublade sich nicht per Software schließen lässt.
**4. Der externe Worker läuft weiter mit altem Code.** Er braucht einen Lauf des
neuen Installers, um Pfad-Mapping, Preset-Meldung, Log-Brücke, Tray-Anzeige und
Slots zu bekommen. Danach ist auch die Live-Gegenprobe von „mehrere Encodes
gleichzeitig" möglich — bisher ist das nur die richtige Celery-Option, kein
gemessener Durchsatz.
**5. „Auch rippen können"** — Entscheidung steht beim Commander (siehe oben).
**6. Unverändert offen aus v3.17:** Live-Gegenprobe der Metadaten-Kette
(MyAnimeList war die ganze Sitzung weg, HTTP 504); ISO-Sicherung und
Mehr-Laufwerk-Betrieb; Klartext-Geheimnisse in der settings-Tabelle; VM-CPU-Typ
auf `qemu64`; `install.sh` nie als root gelaufen; kein TypeScript-Typcheck.
### FALLEN DIESER RUNDE
- **Ein ioctl-Rückgabewert beweist nichts.** Zweimal in einer Sitzung dasselbe
Muster: `CDROMEJECT` quittiert Erfolg auf einem verriegelten Laufwerk, `mount`
quittiert Erfolg auf einer Verbindung, die gleich stirbt. Wo eine Wirkung
prüfbar ist, prüfe die Wirkung.
- **`/proc/fs/cifs/DebugData` ist die Antwort auf „warum hängt der Mount".**
Sitzungen, Shares, Credits, TCP-Status — und die Netz-Namespace, die hier den
Fall gelöst hat.
- **Tests, die `main` importieren, gehören in `test_api_smoke.py`** — nur dieses
Modul überspringt sich unter Windows selbst. In `test_mounts_helpers.py`
brachen sie den lokalen Lauf.
- **Ein Stub muss zur neuen Aufrufzahl passen.** `iter([False, True])` lief in
StopIteration, als eine Prüfung zu zwei wurde. Besser die HÖHERE Funktion
stubben als die Anzahl der Aufrufe nachzählen.
---
## Vorheriger Stand: v3.17 — der Blocker ist zu, neun Punkte abgearbeitet (26.07.2026)
> **Deployt und live gegengeprüft.** Auftrag war „lies den Savepoint und lass uns
> das Projekt endlich beenden". Die neun Commander-Punkte aus v3.16 sind
> abgearbeitet; unterwegs kamen vier Bestandsfehler dazu, die vorher niemand
> gesehen hat. Was offen bleibt, steht ganz unten — und es ist wenig.
### ZUSTAND, gemessen
| | |
|---|---|
| Repo + VM | `87537bf`, Ampel **grün**, deployt |
| Container | alle 5 `Up`, api/postgres/redis `healthy` |
| Tests | **211** grün (vorher 130) |
| Job `95afdc89` | `failed`, aber **`can_retry = true`** — die 74,1 GB sind erstmals über das UI erreichbar |
| Rohschnitt | `/app/media/rippy/95afdc89-…/title_t00.mkv`, 79.604.951.639 Bytes, intakt |
| Platte VM | 148 GB, 107 GB frei |
| NAS-Freigabe | eingehängt und antwortend — **aber siehe „NOCH OFFEN" Punkt 1** |
### DER BLOCKER IST ZU — und er brauchte keine Eingabe vom Nutzer
`RIPPY_PATH_MAP` wurde von niemandem gesetzt; deshalb konnte externes Encoden
**nie** funktionieren. Die Lösung musste nichts erfinden: **Rippy hat die
Freigabe selbst eingehängt und kennt ihre Quelle.** Mountpunkt plus Quelle IST
das Mapping.
```
GET /worker-setup/pfad-map
→ /app/media/rippy=\\192.168.178.62\rippy
```
Live auf der VM abgefragt, Antwortzeit 0,010 s. Beide Windows-Installer holen
diesen Wert selbst, prüfen mit `Test-Path`, ob **dieser PC** die Freigabe
erreicht, und schreiben ihn in `start-tray.bat` und `start-worker.bat`. Der
Commander muss nichts über Container-Pfade wissen — das Feld bleibt leer.
Ist keine Freigabe eingehängt, sagt der Endpunkt das im Klartext samt Abhilfe,
statt ein leeres Mapping zu liefern.
**Was der Commander jetzt tun kann:** `RippyWorkerSetup.exe` neu holen
(Einstellungen → Worker) und laufen lassen. Danach steht bei „Neu komprimieren"
sein Ryzen mit avx512f und der RX 9070 XT zur Wahl, und die 74 GB gehen durch,
ohne die Disc noch einmal einzulegen.
### DIE NEUN PUNKTE — Stand jetzt
| Sein Punkt | Stand |
|---|---|
| 1. „Bei den Presets soll IMMER das Beste ausgewählt werden" | **erledigt** — Knopf „Bestes wählen", Begründung unter jeder Liste |
| 2. „Hier steht noch meine Settings drin" | erledigt (v3.16) |
| 3. „Vektorbefehle müssen ausgelesen werden" | erledigt (v3.16) |
| 3b. „…wenn der Worker AV1 oder besseres kann, immer diesem empfehlen" | **erledigt** — Hardware-Presets nach Familie, AV1 vor H.265 |
| 3c. „…Warnung verschwindet / orange zu grün" | erledigt (v3.16) |
| 4. „JIKAN ist drin — wird das genutzt?" | **beantwortet: ja** — und ab jetzt an der richtigen Stelle in der Kette |
| 5. „Was wenn der Unterbau nicht Debian ist?" | **erledigt** — Paketwerkzeug wird erkannt |
| 6. „Mein PC hat ne dicke Grafikkarte" | **erledigt** — Erkennung (v3.16) + Presets + Pfad-Mapping, die Kette ist vollständig |
| 7. „Warum wird der Datei-Browser noch angezeigt" | **erledigt** — eingeklappt |
| 8. „Server Status muss dringend überarbeitet werden" | **erledigt** — zwei Placebos entfernt |
| 9. „ETA im externen Worker und im Dashboard" | **erledigt** — Restzeit aus gemessenem Fortschritt |
### GEBAUT — was das Raten beendet
**Presets kommen vom Worker, nicht aus dem Quelltext.** `caps.py` meldet
`HandBrakeCLI --preset-list` (auf der VM: **90 Namen**), `api/presets.py`
staffelt daraus die Empfehlung. Kein Punktesystem: für jede Lage eine feste
Reihenfolge echter Namen, genommen wird der erste, den der Worker kennt.
> ⚠️ **Richtigstellung zu v3.16:** Dort stand, die Namen der Hardware-Presets
> seien auf der Rippy-VM „nicht ermittelbar", weil deren HandBrake keinen
> Hardware-Encoder hat. **Gemessen ist das falsch** — die Kategorie `Hardware/`
> steht vollständig in der Liste (`H.265 VCN 2160p 4K`, `AV1 QSV 2160p 4K`,
> NVENC, MF). HandBrake trennt zwei Fragen: `--preset-list` nennt alle Presets,
> `--help` nur die nutzbaren Encoder. Zwei Fragen, zwei Quellen. Der vermeintliche
> Blocker existierte nicht.
Feinheiten, die zählen: Der Encoder heißt `vce`, das Preset heißt **VCN** — ohne
diese Zuordnung liefe die AMD-Karte unter einem Intel-Namen. `vaapi` bekommt
bewusst **kein** Hardware-Preset (HandBrake 1.6.1 liefert keines mit), `MF` wird
nie empfohlen (Nutzbarkeit nicht ablesbar), und für DVD-Auflösungen gibt es
keine Hardware-Presets. Kennt ein Worker keinen der bekannten Namen, sagt das UI
„selbst wählen" statt etwas Falsches einzustellen. Und HandBrakes
`Invalid preset <Name>` wird jetzt übersetzt — vorher stand da „Code 3".
**Dashboard: zwei Placebos raus.** Die Karte hieß „Echte Live-Daten" und log an
zwei von drei Stellen. „Auslastung: 0 % (Aktiv)" war nicht die CPU-Last, sondern
der Job-Fortschritt (bzw. eine feste 15 bzw. 5). Die Verlaufskurve daneben war
`Math.random()`. Und „N Worker Online" zählte registrierte Einträge, auch Leichen
alter Rebuilds. Jetzt: laufende Phase mit **Restzeit**, jeder erreichbare Worker
mit Kernen/Vektorbefehlen/extern, freier Platz mit Warnung, wenn er nicht mehr
für eine Disc reicht.
**Restzeit ehrlich.** Messreihe je Phase (Rip und Kompression haben nichts
miteinander zu tun), Stillstand verlängert die Schätzung, unter zwei Messwerten
oder 60 Sekunden Spanne gibt es **keine** Aussage — dann steht „Restzeit wird
gemessen". Mit den echten 25.07.-Werten gegengerechnet: 1,44 % in 29 min ergibt
„noch ca. 1 Tag 23 h".
**Warnung VOR dem Start.** Wählt man einen externen Encoder, der Quelle oder Ziel
nicht erreicht, steht das im Rip-Dialog — nicht erst nach einer Stunde. Bei
„Automatisch" wird genannt, welcher Worker die Aufgabe kaputtmachen könnte.
**Metadaten: exakt schlägt unscharf.** Das Gate 0,55 war nachrechenbar, weil
`titel_aehnlichkeit` rein ist:
```
"Alien" vs. "Alien 9" → 0,83 TRIFFT
"Inception" vs. "Deception" → 0,78 TRIFFT
"Hero" vs. "Heroman" → 0,73 TRIFFT
"The Dark Knight" vs. "Dark Knight Rises" → 0,69 TRIFFT
```
Ein Kinofilm, den TMDB verpasst, bekam damit Anime-Metadaten, und OMDb wurde nie
gefragt. **Und die Vermutung „das Gate ist zu großzügig" ist trotzdem falsch:**
Für den Fall, für den Jikan eingebaut wurde, ist es zu **streng**
„Evangelion 2.22" gegen den MAL-Titel ergibt 0,51 und fällt durch. Eine einzelne
Zahl kann beides nicht leisten. Jetzt zwei Schwellen: nur ein praktisch exakter
Titel (≥ 0,9) beendet die Kette, ein unscharfer Treffer wird gemerkt, dann kommt
OMDb, und erst wenn OMDb nichts hat, gilt er als **Vorschlag** (0,6).
### VIER BESTANDSFEHLER, die beim Gegenprüfen auffielen
Alle vier durch **Messen an der laufenden Instanz** gefunden, nicht durch Lesen.
**1. „Neu komprimieren" ging überhaupt nicht.** Der Savepoint v3.16 schrieb:
„Rohschnitt 79,6 GB intakt → Neu komprimieren' genügt". Gemessen war
`can_retry = false`**den Knopf gab es nicht.** Gesucht wurde nur in
`/app/temp/raw` und unter dem *aktuellen* `workDir`; der Rip lag aber auf der NAS,
weil beim Start eine Wahl **nur für diesen Rip** getroffen worden war, und die
Einstellung stand auf leer. Dasselbe in `retry_transcode`: es hätte am falschen
Ort gesucht. Neues Modul `rohdaten.py` sieht jetzt an allen Orten nach, die
überhaupt in Frage kommen. **Wieder derselbe Fehler:** aus einem Zustandswert
(der heutigen Einstellung) auf einen Mechanismus (wohin damals gerippt wurde)
geschlossen.
**2. `os.path.isdir` hing im KERNEL und tötete eine Hintergrund-Schleife.**
Zwei Threads des API-Prozesses standen im Zustand **D** (uninterruptible sleep).
Die Rohdaten-Schleife startete ihren ersten Durchlauf, während Rippy die
CIFS-Freigabe neu einhängte — `asyncio.to_thread` kam nie zurück, die Schleife
erreichte ihr `sleep` nie und war **für immer tot**. Ein Timeout um den Aufruf
hätte nichts geholfen: ein im Kernel hängender Thread lässt sich aus Python nicht
abbrechen. Ein Kind-**Prozess** lässt sich abbrechen — geprüft wird jetzt mit
`timeout 4 ls -d`, dasselbe Werkzeug, das `mounts.ist_erreichbar` seit dem
24.07. benutzt.
**3. „Konnte nicht nachsehen" wurde als „ist weg" gewertet.** Nach einem
Container-Neustart stallt der erste Zugriff auf die Freigabe; in diesem Fenster
verschwand der Knopf „Neu komprimieren", obwohl 74 GB dalagen. Jetzt drei
Antworten statt zwei (`da` / `weg` / `unklar` — 124 ist der Rückgabewert von
`timeout`), und bei „unklar" wird die letzte bekannte Antwort gehalten.
**4. Der NAS-Mount kam nach einem Rebuild nicht zurück.** `mounten()` glaubte
`os.path.ismount` — und der Mountpunkt existiert im neuen Container weiter
(Bind-Mount vom Host), also brach das Wiederherstellen genau dort ab. Dazu öffnet
`schreibtest()` eine Datei **ohne Zeitgrenze**: auf einem toten CIFS blockiert das
im Kernel. Jetzt entscheidet `ist_erreichbar()` zuerst, und nach dem Mount wird
geprüft statt geglaubt (zwei Versuche, dann ehrlicher Fehler).
### DIE FALLE, DIE DIE MEISTE ZEIT KOSTETE
**Ein `except Exception: pass` in einer Hintergrund-Schleife.** Der Vorrat blieb
leer, die Funktion lief direkt aufgerufen einwandfrei, und **warum** sie im
Server nichts tat, war nirgends zu sehen. Erst ein Blick auf die Thread-Zustände
in `/proc/<pid>/task/*/stat` brachte das D. Konsequenzen, beide gebaut: Die
Schleife **meldet** ihren Fehler jetzt, und es gibt `GET /health/vorraete` — sagt
für Ping- und Rohdaten-Vorrat, wie alt der letzte Durchlauf ist. Bleibt so ein
Vorrat leer, ist am Endpunkt selbst nämlich **nichts** zu sehen; er antwortet nur
dauerhaft „nichts gefunden".
Merksatz für die nächste Sitzung: Ein Hintergrund-Prozess, der still scheitert,
ist schlimmer als einer, der laut scheitert.
### NOCH OFFEN
**1. Die NAS-Freigabe antwortet nach einem Container-Neustart oft erst beim
zweiten Anlauf — Ursache liegt beim NAS, nicht bei Rippy.** Gemessen: `mount`
meldet Rückgabewert 0, `/proc/mounts` zeigt genau eine korrekt aussehende Schicht
mit den richtigen Optionen, `ist_erreichbar` bekommt direkt danach eine Antwort —
und Sekunden später läuft jeder Zugriff in die Zeitgrenze.
`POST /storage-mounts/rippy/repair` stellt sie dann in ~30 s her,
reproduzierbar. Das NAS ist per Ping bei 0,47 ms erreichbar. Rippy geht damit
jetzt sauber um (harte Zeitgrenzen, zwei Versuche, ehrliche Meldung,
Selbstheilung binnen 30 s), aber **die eigentliche Ursache ist nicht gefunden**
Verdacht: das NAS mag sieben Mount-Zyklen in zwanzig Minuten nicht. Zu prüfen,
wenn es im Alltag wieder auftritt: SMB-Protokollversion und Sitzungsgrenzen am
NAS, und ob ein `repair`-Aufruf beim API-Start als Automatik sinnvoll ist.
**2. Der externe Worker des Commanders läuft noch mit altem Code.** Er meldet
`simd=unbekannt`, `presets: 0`, `pfad_map: None` und stand bei der Prüfung auf
`online=false`. Alles, was diese Sitzung gebaut hat, wirkt für ihn erst nach
einem Lauf des neuen Installers.
**3. Live-Gegenprobe der Metadaten-Kette steht aus.** MyAnimeList war während
der ganzen Sitzung weg (Jikan antwortete durchweg HTTP 504). Die Arithmetik des
Gates ist davon unberührt und in `test_jikan_helpers.py` festgehalten; ein echter
Anime-Rip muss es noch bestätigen.
**4. Aus dem ARM-Vergleich, unverändert nicht gebaut:** ISO-Sicherung für
Datenträger, die weder Film noch Audio-CD sind, und **mehrere Laufwerke
gleichzeitig** (Rippy sieht sie, ob parallel gerippt wird, ist unbewiesen).
**5. Ältere Punkte, unverändert gültig:**
- Discord-Webhook und MakeMKV-Key liegen im **Klartext** in der settings-Tabelle.
Für Heimnetz erwartbar, aber der Webhook ist eine echte Zugangsberechtigung.
- **VM-CPU-Typ steht auf `qemu64`** → `host` würde AVX2 freischalten, 24×
schneller. Anleitung in der README („Rippy schneller machen"). Ein
VM-Neustart, nicht gemacht — das ist eine Entscheidung des Commanders.
- **`install.sh` ist nie als root durchgelaufen.** Verzeichnisse anlegen,
`mount --make-rshared` und die systemd-Unit bleiben ungetestet; sie laufen erst
bei einer echten Neuinstallation.
- Es gibt **kein TypeScript-Typcheck** im Projekt (`typescript` ist nicht
installiert, `npm run build` transpiliert nur mit esbuild). Typfehler im UI
fallen nirgends auf. Bewusst nicht geändert: `tsc` in die Ampel zu nehmen färbt
sie womöglich wegen Bestandscode rot, und das ist eine Entscheidung.
### FALLEN DIESER SITZUNG (für die nächste)
- **Ein Prozess-Zustand `D` in `/proc/<pid>/task/*/stat` heißt: im Kernel
blockiert.** Das ist der schnellste Weg, einen hängenden Netz-Zugriff zu
finden — schneller als jedes Log.
- **`os.path.join` für Container-Pfade ist unter Windows falsch**
(`/app/media\rippy`). Dritter Fund derselben Falle; diesmal hat ein Test sie
gefunden, nicht die Produktion. Immer `posixpath` für Pfade, die in Container
gehen.
- **Ein Umlaut-Werkzeug hat `install.ps1` von CRLF auf LF gestellt** — 470
Zeilen Diff-Rauschen für eine Änderung von 56. Nach automatisierten
Textänderungen an `.ps1` immer `file` gegenprüfen (BOM **und** CRLF).
- **Eigene Testerwartungen sind auch Behauptungen.** Zweimal lag der Test falsch
und der Code richtig (Confidence 0,3 statt 0,0; „1 Tag 23 h" statt „mehr als 2
Tage"). Beide Male hat die Ampel bzw. der lokale Lauf es gefangen.
- **Ein Test, der Quelltext-Positionen vergleicht, findet seine eigene Erwähnung
im Kommentar.** Verhalten prüfen, nicht Zeichenketten.
---
## Vorheriger Stand: v3.16 — ÜBERGABE: externes Encoden konnte nie funktionieren (26.07.2026)
> **Zuerst lesen.** Diese Sitzung endete wegen vollem Kontext. Der Auftrag war
> Commander-Feedback aus der ersten Selbst-Einrichtung plus die Diagnose eines
> fehlgeschlagenen Test-Rips. Die Diagnose ist abgeschlossen, zwei Commits sind
> gebaut — **aber NICHT deployt**, und der Kern-Blocker steht noch offen.
### ZUSTAND, gemessen
| | |
|---|---|
| Repo | `fd1feaa`, Ampel **grün** |
| **VM** | **`61c38a0` — die letzten 2 Commits sind NICHT aktiv** |
| Windows-Worker des Commanders | läuft mit **altem** Code (vor der Windows-Erkennung) |
| Job `95afdc89` | `failed` beim Encoden |
| **Rohschnitt** | **79,6 GB intakt** auf `/app/media/rippy/95afdc89-…/title_t00.mkv` (NAS) → „Neu komprimieren" genügt, **kein Neu-Rip** |
| Commander-PC | AMD Ryzen 7 9700X · **avx512f** · 16 Kerne · RX 9070 XT |
| Rippy-VM-CPU | QEMU-Generikum · nur `sse4_2` · 4 Kerne |
### DER KERNBEFUND: drei Ursachen, die ineinandergriffen
Der Commander vermutete „das System sieht den externen Encoder nicht". **Falsch —
das Routing funktionierte einwandfrei.** Gemessen:
```
meta.transcode_node = tobisnicerpc@TobisNicerPC
VM-Worker-Log = nur rip_disc, KEIN transcode_files
22:19:41.271 Kompression eingereiht
22:19:41.453 "Keine Roh-MKVs gefunden" ← 182 ms später
```
Sein PC nahm die Aufgabe an und lehnte sie sofort ab. Warum:
1. **`RIPPY_PATH_MAP` wird von NIEMANDEM gesetzt** — nicht vom Installer, nicht
vom UI, nicht von Compose. `pfad_lokal()` in `tasks.py` hat sogar Tests, aber
keinen Anschluss. `/app/media/...` sind Container-Pfade; ein externer Worker
sieht sie nur übersetzt. **Ein Windows-Worker konnte also nie transcodieren.**
⚠️ **DAS IST DER OFFENE BLOCKER.**
2. **Die Ablage war im UI überhaupt nicht einstellbar.** `outputDir` wurde von
Vollautomatik und Schnellwahl gelesen, hatte aber **kein Eingabefeld**. Das
Arbeitsverzeichnis hatte eine Auswahl eingehängter Ziele, das Ziel nicht.
Folge: Rohdaten auf der NAS (erreichbar), Ziel auf der VM-Platte (nicht
erreichbar — dort läuft **kein Samba**). Die richtige Einstellung war mit den
vorhandenen Bedienelementen **nicht erreichbar.** → behoben, siehe unten.
3. **Die Fehlermeldung log.** „Keine Roh-MKVs gefunden" klang nach kaputtem Rip,
obwohl der Rip vollständig war. → behoben.
### GEBAUT in dieser Sitzung (2 Commits, gepusht, Ampel grün)
**`fe12401`** — Hardware-Encoder + CPU-Merkmale auf Windows, ehrliche Fehler:
- **Hardware-Erkennung war MEIN Bug:** `leite_backends_ab()` verlangte
`/dev/dri` bzw. `nvidia-smi` — beides gibt es unter Windows nicht. Der PC des
Commanders meldete deshalb nur CPU, obwohl HandBrakes Windows-Build `vce_*`
kennt. Die Prüfung war zudem überflüssig: **HandBrake probiert Hardware selbst
an und listet nur Nutzbares** (auf der VM belegt: „qsv: not available", und
`qsv_*` fehlt dann in `--help`). Jetzt ist HandBrakes Liste die einzige
Auskunft, unterschieden nach Familie (`nvenc`/`qsv`/`vce`/`vaapi`) plus eigener
Kennung für Hardware-AV1.
- **Vektorbefehle unter Windows** über `IsProcessorFeaturePresent` (kernel32,
winnt.h) + CPU-Name aus der Registry. Auf dem Commander-PC gegengeprüft:
vorher „AMD64 Family 26 Model 68 / unbekannt", jetzt „AMD Ryzen 7 9700X /
avx512f". Damit verschwindet die AVX2-Warnung für diesen Worker von selbst.
- **`_erreichbarkeit_pruefen()`** unterscheidet jetzt „Pfad nicht erreichbar +
kein Mapping" (nennt `RIPPY_PATH_MAP` mit Beispiel), „Mapping greift nicht"
und „Ordner leer" — und prüft **beide** Pfade, nicht nur die Quelle. Jede
Meldung sagt ausdrücklich, dass die Rohdaten nicht verloren sind.
- Vier Platzhalter mit der Umgebung des Commanders (`192.168.178.20`,
`TOBIS-PC`, seine Jellyfin- und Rippy-IP) verallgemeinert.
**`fd1feaa`** — Ablage einstellbar + Kohärenz-Prüfung:
- Auswahlliste **„Ablage"** in Einstellungen → Ripping, gleiche Liste wie beim
Arbeitsverzeichnis. Wirkt automatisch in Vollautomatik und Schnellwahl, weil
beide `outputDir` lesen — genau wie vom Commander gefordert.
- **Warnung, sobald ein EXTERNER Worker gemeldet ist** und Ablage +
Arbeitsverzeichnis nicht beide auf einer erreichbaren Freigabe liegen. Drei
Fälle mit Konsequenz im Klartext (kann nicht lesen / kann nicht schreiben /
läuft, kostet aber eine Vollkopie).
- „Extern" ist keine Heuristik: `caps.py` meldet `extern` selbst (`/app` gibt es
im Rippy-Image immer, außerhalb nie) und zusätzlich `pfad_map`.
### COMMANDER-FEEDBACK aus der ersten Selbst-Einrichtung — wo steht was
Seine neun Punkte, damit nichts durchfällt. „→ Punkt N" verweist auf die
Prioritätenliste darunter.
| Sein Punkt | Stand |
|---|---|
| 1. „Bei den Presets soll IMMER das Beste ausgewählt werden" | **offen → Punkt 2** (hängt an der Preset-Liste vom Worker) |
| 2. „Hier steht noch meine Settings drin, muss verallgemeinert werden" | **erledigt** (`fe12401`, vier Platzhalter) |
| 3. „Vektorbefehle müssen ausgelesen werden" | **erledigt** (`fe12401`, avx512f auf seinem PC gemessen) |
| 3b. „…wenn der Worker AV1 oder besseres kann, immer diesem empfehlen" | **offen → Punkt 2** |
| 3c. „…Warnung verschwindet / orange zu grün" | **erledigt**`schwacheEncoderCpu()` schaltet ab, sobald SIMD schnell ODER Hardware da ist. Wirkt für seinen PC ab dem Deploy |
| 4. „JIKAN ist drin — wird das genutzt?" | **beantwortet: ja.** Kette TMDB → Jikan → OMDb, Gate 0,55. Nebenbefund → Punkt 7 |
| 5. „Was wenn der Unterbau nicht Debian ist?" | **offen → Punkt 5** |
| 6. „Mein PC hat ne dicke Grafikkarte (RX 9070 XT), NVIDIA auch möglich" | **Erkennung erledigt** (`fe12401``vce`/`nvenc`/`qsv` getrennt, Hardware-AV1 eigene Kennung). **Benutzen** hängt an **Punkt 1** (Pfad-Mapping) **und Punkt 2** (Preset-Namen) — beides offen |
| 7. „Warum wird der Datei-Browser noch angezeigt" | **offen → Punkt 4** |
| 8. „Server Status muss dringend überarbeitet werden" | **offen → Punkt 3** |
| 9. „ETA im externen Worker und im Dashboard" | **offen → Punkt 3** |
Drei erledigt, eine Frage beantwortet, fünf offen — und die fünf sind unten
priorisiert.
### OFFENE PUNKTE — nach Priorität, mit Kontext
**1. `RIPPY_PATH_MAP` setzen (DER BLOCKER).** Der Installer muss fragen „wie
erreicht dieser PC die Freigabe?" und den Wert in `start-tray.bat` /
`start-worker.bat` schreiben. Format laut `pfad_lokal()`:
`/app/media=\\NAS\rippy;/app/temp=…` (Paare per `;`, Präfix-Ersetzung, wandelt
`/` zu `\`, wenn das Ziel Backslashes enthält). Commander-Vorgabe dazu: *„Wenn
ein externes Ziel für den Speicher eingehängt ist, soll Rippy das ausgewählte als
Arbeitsziel verwenden … das muss auch für den Automatik-Modus so sein."* Punkt 2
oben erfüllt die Einstellungs-Seite davon; es fehlt die Worker-Seite.
Danach: Commander kann seinen 80-GB-Rohschnitt per „Neu komprimieren" auf dem
Ryzen laufen lassen.
**2. Preset-Liste vom Worker holen** — Voraussetzung für „immer das Beste".
Commander: *„Bei den Presets soll IMMER das Beste ausgewählt werden"* und *„wenn
der Worker AV1 oder noch besseres kann, immer diesem empfehlen, die Warnung
verschwindet oder wird von orange zu grün"*. Blocker: Die Namen der
HandBrake-**Hardware**-Presets sind auf der Rippy-VM **nicht ermittelbar** (deren
HandBrake hat keinen Hardware-Encoder), und Erfinden verstößt gegen AGENTS
Regel D — das Projekt hat das zweimal teuer bezahlt. Richtige Lösung: `caps.py`
meldet `HandBrakeCLI --preset-list` des jeweiligen Workers, das UI bietet genau
diese an. Beendet das Raten dauerhaft.
**3. Dashboard + ETA.** Commander: *„Der Server Status muss dringend überarbeitet
werden, das Dashboard soll ja quasi alles auf einen Blick zeigen"* — „Auslastung:
0 % (Aktiv)" ist nichtssagend. Dazu eine **ETA** im Dashboard und beim externen
Worker. Restzeit ist aus dem Fortschrittsverlauf schätzbar; ehrlich bleiben,
solange die Datenlage dünn ist. Verlässlichster Messpunkt (in dieser Sitzung
bewährt): Leseposition im Quellstrom über `/proc/<pid>/fdinfo/`.
**4. Datei-Browser aus dem Rip-Dialog.** Commander: *„Warum wird hier der Datei
Browser noch angezeigt — das ist doch quatsch"*. Prüfen, ob wirklich redundant
(darüber gibt es Schnellwahl + Arbeitsverzeichnis-Auswahl), dann raus oder
einklappen. Datei: `docker/ui/src/components/RipTargetModal.tsx`.
**5. Nicht-Debian-Hosts.** `install.sh` nennt bei fehlendem Compose nur
`sudo apt install docker-compose-plugin`; auf Arch/Fedora/openSUSE falsch.
Paketmanager erkennen. Klarstellen: der Worker-**Container** ist unabhängig vom
Host-System.
**6. Warnung im Rip-Dialog** (nicht nur in den Einstellungen), wenn der gewählte
externe Encoder die Pfade nicht erreicht — vor dem Start, nicht nach einer Stunde.
Bausteine liegen: `caps.py` meldet `extern` und `pfad_map`.
**7. Jikan-Reihenfolge prüfen** (Nebenbefund). Kette ist TMDB → **Jikan** → OMDb
mit Gate `MINDEST_AEHNLICHKEIT = 0.55`. Jikan wird also wirklich benutzt (Antwort
auf die Commander-Frage), läuft aber **vor** OMDb: bei einem Nicht-Anime, den
TMDB verpasst, antwortet zuerst eine Anime-Datenbank. 0,55 ist großzügig.
**8. Aus dem ARM-Vergleich, nicht gebaut:** ISO-Sicherung für Datenträger, die
weder Film noch Audio-CD sind (daran scheitert Rippy heute), und **mehrere
Laufwerke gleichzeitig** — Rippy sieht sie (`/dev/sr[0-9]*`), ob parallel gerippt
wird, ist **unbewiesen**.
**9. Ältere offene Punkte, unverändert gültig:**
- „Job aus der Liste entfernen" sagt nicht, wie viel daneben liegen bleibt — der
Rohschnitt verwaist unsichtbar (v3.14).
- Discord-Webhook und MakeMKV-Key liegen im Klartext in der settings-Tabelle.
- **VM-CPU-Typ steht auf `qemu64`** → `host` würde AVX2 freischalten, 24×
schneller. Anleitung steht in der README („Rippy schneller machen").
- **`install.sh` ist nie als root durchgelaufen** — Verzeichnisse anlegen,
`mount --make-rshared` und die systemd-Unit sind ungetestet.
- Zwei API-Tests laufen nur in der Ampel (`test_api_smoke.py` überspringt sich
unter Windows selbst).
### FALLEN, die diese Sitzung gekostet haben
- **Deutsche Anführungszeichen in DOPPELT gequoteten Python-Strings** beenden den
String vorzeitig. Dreimal hineingetappt. Der Bestand nutzt dafür **einfach**
gequotete f-Strings (`f'…„{x}"…'`). In dreifach gequoteten Docstrings ist es
unproblematisch.
- **In f-Strings steht in `{...}` CODE, kein Text.** Ein Umlaut-Konverter machte
aus `f"{groesse}"` ein `f"{größe}"`, während die Variable `groesse` hieß —
Ruff fand es als F821.
- **`docker exec` ohne `sh -c` expandiert kein `*`** — ein `du -sh /pfad/*`
liefert dann still nichts und sieht wie „leer" aus.
- **Eine `/proc`-Suche nach „HandBrake" trifft die eigene Shell mit**, weil deren
Kommandozeile das Wort enthält. Zwei Fehlalarme.
- **`pydantic-core` springt lokal wiederholt auf 2.47.0** und bricht damit das
Einsammeln ALLER API-Tests (`SystemError`). Fix:
`python -m pip install "pydantic-core==2.46.4"`. Zweimal nötig gewesen.
---
## Vorheriger Stand: v3.15 — Aufräum-Runde: Tempo, tote Routen, 4K-Entscheidung (25.07.2026, 20:00)
> **Deployt und live gegengeprüft.** Auftrag war „bau alles so um, dass es
> sauber ist" mit fünf Vorgaben: saubere Codebase, selbsterklärendes UI,
> externe Worker, kein Laggen, idiotensicher. Was noch offen ist, steht unten.
### Das „Laggen" hatte genau eine Ursache — gemessen und behoben
Über alle 15 Endpunkte gemessen, die das UI beim Laden braucht:
| Endpunkt | vorher | nachher |
|---|---|---|
| `/capabilities` | **1,010 s** | **0,003 s** |
| `/system/updates` | 0,491 s | unverändert (hängt am Knopf) |
| `/metadata/status` | 0,412 s | unverändert (hängt am Knopf) |
| die anderen 12 | < 0,025 s | < 0,010 s |
Nur `/capabilities` schlug beim **Seitenaufbau** zu — und fünf Stellen holen
ihn (Dashboard, Einstellungen, Worker-Tab, Wizard, Rip-Dialog). Jede Seite
zahlte eine Sekunde. Ursache ist kein Fehler, sondern das Wesen des
Celery-Pings: er sammelt Antworten bis zum Timeout und kann nicht früher
aufhören. Den Timeout zu kürzen würde Antworten langsamer Remote-Worker
verschlucken — also genau die Maschinen, um die es beim externen Encoding geht.
Jetzt pingt eine Hintergrund-Schleife im 5-s-Takt, der Endpunkt liest ab. Ist
der Vorrat älter als 30 s, wird einmal synchron gepingt: lieber langsam als
falsch („alles offline", obwohl alles läuft). Kompletter Dashboard-Aufbau:
**1,03 s → 0,028 s.**
### 4K ist entschieden — Kompression je Disc-Typ abwählbar
Die offene Frage aus v3.14 ist gebaut. Bisher gab es nur `transcodeEnabled`:
alles oder nichts. Jetzt kann das Preset eines Disc-Typs auf den Reservewert
`keine` stehen → die verlustfreie Datei bleibt stehen. Damit ist die sinnvolle
Einstellung für diese Maschine erstmals möglich: **4K verlustfrei behalten,
DVD und Blu-ray weiter schrumpfen.**
`komprimieren_fuer()` ist die neue reine Funktion; `preset_fuer()` überspringt
den Reservewert bewusst und gibt ihn NIE als Preset-Namen zurück — sonst bekäme
HandBrake `--preset keine`. Gegengeprüft: keiner der 90 echten Presets aus
`--preset-list` heißt so, und die fünf im UI angebotenen Namen existieren alle.
### Der Wizard läuft nicht mehr in die 4K-Falle
Er hatte die Zahlen längst vorliegen (`/capabilities` meldet `cpu_simd` und
`cpu_kerne`) — benutzt hat er sie nicht und H.265 als Standard vorgeschlagen.
Auf einer CPU ohne AVX2 sind das ein bis zwei Tage pro 4K-Film.
Jetzt entscheidet die **gemessene** Leistung: schwache CPU → 4K nicht
komprimieren, Blu-ray/DVD auf H.264. Stark oder Hardware-Encoder → H.265
durchgehend. Und er schreibt **alle vier** Preset-Felder statt nur des
allgemeinen; vorher fiel 4K auf ein 1080p-Preset zurück.
Dazu gehärtet: Kasten „Was Rippy gerade sieht" (Laufwerk, Worker mit
Kernen/SIMD, freier Platz) — jede Zeile mit **Handlungsanweisung** statt nur
einem Kreuz. Lädt parallel und wiederholt im 5-s-Takt, weil der Worker beim
ersten Start noch hochläuft. API-Keys sind sichtbar statt Punkte (kopierte
Keys, keine Passwörter — Tippfehler sieht man in Punkten nicht) und werden
direkt nach dem Speichern geprüft, mit der Wahl „Key korrigieren" oder
„Trotzdem fertigstellen".
**Gewarnt wird nur, wenn es belegt ist:** `schwacheEncoderCpu()` verlangt
mindestens einen Worker mit BEKANNTER SIMD-Stufe und keinen mit
Hardware-Encoder. Ein Windows-Worker meldet `unbekannt` (kein `/proc/cpuinfo`)
→ dann wird geschwiegen statt falsch gewarnt.
### Vier tote Routen raus
`POST /prescan`, `POST /jellyfin/format` (+ `nfo_generator.py` und
`image_downloader.py`, die sonst nichts nutzte), `GET /stream/jobs`,
`GET /worker-setup/windows-gui`. Jede ein Überrest eines ersetzten Entwurfs,
keine mit Aufrufer. `main.py`: 1726 → 1682 Zeilen, dazu 279 Zeilen in zwei
gelöschten Modulen. Tests halten beide Seiten fest: die vier müssen WEG
bleiben, die drei für die Worker-Installation (`/worker-setup/paket`,
`/windows`, `/windows-exe`) müssen DA sein.
### Externe Worker — vorbereitet, vom Commander zu testen
Auf **Windows gegengeprüft** (dieser PC): `cpu_kerne` 16 und CPU-Modell kommen
korrekt durch, `encoders` ist ohne installiertes HandBrake korrekt **leer**,
`cpu_simd` ehrlich `unbekannt` → keine falsche Warnung. Ein GPU-Worker braucht
ein HandBrake-Build mit `nvenc_*`/`qsv_*`; die neue Anzeige nennt die
ungefilterte HandBrake-Auskunft, damit das nachprüfbar ist.
### ARM-Vergleich (Vorbild-Projekt)
Fast alles, was ARM automatisch macht, macht Rippy schon — und meist
gründlicher: Metadaten aus drei Quellen statt nur OMDb, Episoden-Erkennung per
Laufzeitabgleich, Kompression auf eine eigene Queue routbar. Bezeichnend:
ARMs Auto-Auswurf war bei Rippy nur **behauptet** und ist erst in v3.14 echt
geworden. Zwei Lücken bleiben, beide **nicht gebaut, als Vorschlag**:
- **ISO-Sicherung** für Datenträger, die weder Film noch Audio-CD sind — daran
scheitert Rippy heute.
- **Mehrere Laufwerke gleichzeitig.** Rippy sieht sie (`/dev/sr[0-9]*`), ob
parallel gerippt wird, ist **unbewiesen** — mit einem Laufwerk nicht testbar.
### Installation: ein Befehl statt Checkliste (`install.sh`)
Commander-Rückmeldung: „Das Docker Deployment ist mir zu kompliziert." Zu
Recht — es war eine Sechs-Schritte-Checkliste, von der zwei Punkte Fachwissen
verlangten. Jetzt:
```bash
git clone <repo-url> rippy && cd rippy
sudo ./install.sh
```
Der schlimmste Punkt war das **Laufwerk**: MakeMKV braucht ZWEI Geräteknoten,
und die sg-Nummer ist je Host anders. Das Skript gleicht sie über die
SCSI-Adresse in `/sys` ab statt zu raten — auf der VM gegengeprüft
(`sr0 → 3:0:0:0`, `sg1 → 3:0:0:0` = dasselbe Gerät, korrekt erkannt). Dazu:
Verzeichnisse, Mount-Propagation **inklusive neustart-fester systemd-Unit**
(vorher stand in der README nur „reboot-fest persistieren", ohne zu sagen wie —
nach einem Reboot scheiterte das NAS-Einhängen aus dem UI stillschweigend),
`.env` schreiben ohne Bestehendes zu überschreiben, bauen, starten.
`./install.sh --nur-pruefen` sieht nur nach. Wiederholbar, damit auch der
Update-Weg: `git pull && sudo ./install.sh`.
**Zwei Fallgruben fielen beim Testen auf, beide meine eigenen:**
- `--nur-pruefen` verlangte root und brach ab — ein Prüf-Modus, der nichts
ändert, darf daran nicht scheitern.
- `.env.example` hatte `OPTICAL_SG=/dev/sg1` **unkommentiert** vorbelegt. Der
Installer hätte den erkannten Wert deshalb nicht eingetragen („steht schon
drin") und auf jedem fremden Host still eine kaputte Konfiguration
hinterlassen — genau das, was er verhindern soll. Beide Gerätezeilen sind
jetzt auskommentiert (Compose hat ohnehin Vorgaben), plus eine Gegenprobe:
zeigt ein wirksamer Wert auf ein Gerät, das es hier nicht gibt („`.env` von
einem anderen Rechner"), wird das benannt statt kryptisch von Docker gemeldet.
Die `.env`-Logik ist in drei Fällen auf der VM geprüft: frische Datei bekommt
den erkannten Wert, ein selbst gesetzter Wert bleibt unangetastet, zweimal
ausführen erzeugt genau eine Zeile.
### README neu aufgebaut
Vorher 293 Zeilen, in denen der Schnellstart zwischen `lsscsi`, sg-Knoten,
USB-Passthrough und `mount --make-rshared` begraben war. Jetzt: Installation in
zwei Zeilen oben, dann die Tabelle „Wenn etwas nicht geht" mit den vier Fällen,
die praktisch alles abdecken. Alles Technische darunter in aufklappbaren
Abschnitten, inklusive der Handarbeits-Variante.
**Neu und ausdrücklich gewünscht: „Rippy schneller machen"** — der VM-CPU-Typ.
Warum Virtualisierer eine generische CPU ohne AVX2 geben, was das kostet
(gemessene 2855 h je 4K-Film), die genauen Proxmox-Schritte (herunterfahren →
Hardware → Processors → Type auf `host` → starten, bzw. `qm set <vmid> --cpu
host`), **warum ein Neustart von innen nicht genügt**, und wie man nachprüft:
Rippy zeigt die Vektorbefehle selbst an. Dazu der Nachteil (keine
Live-Migration auf andere CPUs) und die Alternative `x86-64-v3` für Cluster.
### Der MakeMKV-„Fallback" auf der VM war seit Monaten tot
Beim Aufräumen der VM-`.env` aufgefallen und nachgemessen (25.07.2026):
| Quelle | Antwort |
|---|---|
| `https://www.makemkv.com/download` (Repo-Standard) | **HTTP 200** |
| `https://www.makemkv.com/download/old` | HTTP 525 (Cloudflare) |
| web.archive.org-Schnappschuss **aus der VM-`.env`** | **HTTP 404** |
Die VM zeigte also auf eine **kaputte** Adresse. Aufgefallen ist es nie, weil
dort die `vendor/`-Tarballs liegen und der Download-Zweig gar nicht erreicht
wird. Nimm die Tarballs weg, und jeder Bau scheitert mit 404 — während die
Konfiguration gesund aussieht. Genau diese Sorte Fehler ist bei einer Übergabe
teuer.
**Bereinigt** (Sicherung liegt als `.env.sicherung-vor-aufraeumen-20260725`):
`MAKEMKV_URL_BASE` raus → es gilt der Standard, der liefert. `JWT_SECRET_KEY`
raus → Überrest der in v3.4 ausgebauten Anmeldung, wirkungslos.
**Gebaut — Rückfall dreistufig und ehrlich:** `vendor/`-Tarballs →
`MAKEMKV_URL_BASE``MAKEMKV_URL_FALLBACK` (neu, wird automatisch versucht).
Der Fallback ist **absichtlich leer vorbelegt**: Es gibt derzeit keine belegbare
zweite Quelle, und eine einzutragen, die nicht liefert, wäre schlimmer als
keine — siehe oben. Scheitert alles, nennt die Fehlermeldung jetzt die beiden
Wege, die funktionieren (Tarballs nach `vendor/`, oder eigene Quelle als
`MAKEMKV_URL_FALLBACK`), statt nur einen curl-Rückgabewert zu hinterlassen.
### NOCH OFFEN
1. **Externe Worker im Praxistest** (Commander).
2. **ISO-Sicherung und Mehr-Laufwerk-Betrieb** — siehe ARM-Vergleich.
3. **„Job aus der Liste entfernen" sagt nicht, wie viel daneben liegen bleibt**
(v3.14). Der Rohschnitt verwaist dabei unsichtbar.
4. **Discord-Webhook und MakeMKV-Key liegen im Klartext** in der
settings-Tabelle. Für Heimnetz erwartbar, aber der Webhook ist eine echte
Zugangsberechtigung.
5. **Der VM-CPU-Typ steht auf `qemu64`.** `host` würde AVX2 freischalten und
jeden Software-Encode 24× beschleunigen. Ein VM-Neustart, nicht gemacht —
**die Anleitung steht jetzt in der README** („Rippy schneller machen").
6. **`install.sh` ist nie als root durchgelaufen.** Auf dieser VM sind alle
root-Schritte No-Ops (Verzeichnisse da, Propagation schon `shared`), und
`sudo` verlangt hier ein Passwort. Bewiesen sind Laufwerkserkennung,
`.env`-Logik (drei Fälle) und Syntax; **ungetestet bleiben das Anlegen der
Verzeichnisse, `mount --make-rshared` und die systemd-Unit** — die laufen
erst bei einer echten Neuinstallation.
---
## Vorheriger Stand: v3.14 — Durchsicht: vier Placebos und ein unwirksamer Schutz (25.07.2026, 18:40)
> **Stand 19:30: alles committet, Ampel grün (`a1aabd5`), DEPLOYT und live
> gegengeprüft.** Der Akira-Encode wurde um 18:31 abgebrochen (Nachtrag im
> Block darunter), danach der Job aus der Liste gelöscht und die Rohdaten
> entsorgt. Es läuft nichts, die Platte hat **111 GB frei**.
> Was noch zu entscheiden ist, steht unten unter „NOCH OFFEN".
### DER WICHTIGSTE FUND: der Platten-Schutz aus `c065967` greift nicht
`_original_aufheben()` entschied per `os.stat().st_dev`, ob umgehängt oder
kopiert werden muss. Auf der VM gemessen — **beides gleichzeitig wahr**:
```
st_dev /app/temp = 2050
st_dev /app/media = 2050 → identisch
os.rename(...) → EXDEV, "Invalid cross-device link"
```
Der Kernel vergleicht bei `rename()` den **Mount**, nicht das Gerät.
`/app/temp` (Docker-Volume) und `/app/media` (Bind-Mount) sind zwei Mounts
DERSELBEN ext4-Partition. Die Prüfung sah deshalb „gleiches Dateisystem",
**übersprang die Platzprüfung**, und `shutil.move` kopierte doch — 75 GB bei
37 GB frei. Der Schutz hätte genau den Schaden zugelassen, gegen den er
gebaut wurde.
**Behoben:** `os.rename` wird jetzt VERSUCHT statt vorhergesagt. Klappt es,
ist es umgehängt. Kommt EXDEV, steht die Kopie fest — und erst dann wird der
Platz geprüft. Vier Tests dazu (`test_original_aufheben.py`), inklusive des
Falls, der die Platte füllte.
### GEMESSEN, Stand 18:30 (alles per Befehl auf der VM geprüft)
- **Container:** alle 5 `Up`, api/postgres/redis `healthy`. api/ui/worker seit
18:00:39 (Deploy von `8bb075c`), postgres/redis älter.
- **Platte:** 148 G, 105 G belegt, 37 G frei = 75 G Rohschnitt + 4,9 G Medien
(Evangelion) + ~25 G System. **Keine Kopier-Reste**, kein `original`-Ordner.
- **Akira-Job:** `failed`, „Abgebrochen durch Nutzer" (Abbruch 18:30:17
angefordert, Worker bestätigt 18:33:38 → **3,4 Minuten Verzug**, siehe unten).
Es läuft **kein** HandBrake mehr (per `/proc` geprüft, Stand 19:06).
- **Der Encode war bei 1,44 %**, gemessen an der Leseposition im Quellstrom
(`/proc/<pid>/fdinfo/3`: 1.145.940.149 von 79.604.951.639 Bytes) — exakter
als jede Fortschrittsanzeige. Zwei Tempo-Fenster ergaben 395 und 784 KB/s
**2855 h** für den Film, bei 3,84 von 4 gesättigten Kernen.
- **Achtung bei Prozess-Suchen per `/proc`:** Ein `case "$c" in *HandBrake*)`
trifft die eigene Shell mit, weil deren Kommandozeile das Wort enthält. Zwei
Fehlalarme in dieser Sitzung. Immer die PID gegenprüfen.
- **Die Ursache dafür ist neu und behebbar:** Die VM läuft auf dem generischen
QEMU-CPU-Modell (`QEMU Virtual CPU version 2.5+`), `grep -c avx2
/proc/cpuinfo` = **0**, nur bis `sse4_2`. x265 lebt von AVX2. Abhilfe:
CPU-Typ von VM 106 in Proxmox auf `host` stellen (braucht VM-Neustart).
- **HandBrake im Worker-Image kann:** `svt_av1`, `x264`, `x265` (je 10/12-bit),
`mpeg4/2`, `VP8/9`, `theora` — und **keinen einzigen Hardware-Encoder**.
- **Ampel grün** für `b526a0a` und `8bb075c` (Gitea-API abgefragt).
### GEBAUT — vier Placebos entfernt bzw. echt gemacht
1. **Fortschritt log statt Wahrheit.** `get_progress_from_line` matchte jede
Zahl vor einem `%` — also auch HandBrakes **Scan-Durchlauf**, der VOR dem
Encodieren bis 100 % hochläuft. Dazu warf `if progress > 0` alle echten
Werte unter 1,00 % weg. Ergebnis: Anzeige klebte stundenlang auf 99 %.
Jetzt wird nur die `Encoding:`-Zeile gelesen, `task N of M` mitgerechnet,
und **-1 heißt „keine Angabe"** (Muster von `get_progress_from_prgv`).
Formatstrings aus dem Binary gelesen, nicht geraten.
**⚠️ Richtigstellung zu v3.13:** Dort steht, `progress=99` sei ein „Altwert
aus dem Absturz". Falsch — beide Startpfade setzen auf 0, der Wert war
frisch vom Scan-Durchlauf geschrieben. Es war ein Bug, kein Überrest.
2. **„Automatischer Auswurf" tat nichts.** Die Einstellung (Standard: ein)
wurde von niemandem gelesen: DVD/Blu-ray warfen **nie** aus, Audio-CDs
**immer**, weil abcde `-x` fest verdrahtet bekam. Jetzt entscheidet die
Einstellung beides (`wirf_disc_aus()` per CDROMEJECT-ioctl, Linux-guarded).
3. **„Alle Tracks rippen" konnte nichts bewirken** — abcde bekommt keine
Track-Auswahl und es gibt keine Oberfläche dafür. Schalter entfernt, an
seiner Stelle steht jetzt die Wahrheit („wird immer vollständig gerippt").
4. **Encoder-Auslese behauptete statt zu messen.** `cpu-x264`/`cpu-x265`
standen fest verdrahtet drin („immer dabei") — ein Rip-Worker **ohne**
HandBrake behauptete damit, komprimieren zu können. Und `vaapi` wurde
allein wegen `/dev/dri` gemeldet, ohne zu prüfen, ob HandBrake das kann
(dieses Image kann es nicht). Jetzt aus `HandBrakeCLI --help` geparst, plus
**CPU-Modell, Kernzahl und Vektorbefehlsstufe** je Worker — mit sichtbarer
Warnung im UI, wenn AVX2 fehlt. Genau die Angabe, deren Fehlen den
55-Stunden-Encode unsichtbar machte.
### GEBAUT — Zombie-Erkennung (v3.13 Punkt 2 abgearbeitet)
`zombies.py`: Beim Worker-Start werden Jobs, die auf `ripping`/`transcoding`/
`canceling` stehen, gegen Celerys `active`/`reserved`/`scheduled` gehalten und
ehrlich auf `failed` gesetzt, wenn niemand daran arbeitet. Drei Sicherungen,
weil ein falsch getöteter Job teurer ist als eine stehende Leiche:
- **nur beim Start** (da ist „es lief nichts" eindeutig),
- **120 s Gnadenfrist** (Celery stellt unbestätigte Aufgaben erneut zu),
- **Vollzähligkeit**: antworten weniger Knoten als laut Herzschlag online
sind, wird NICHTS gewertet — sonst wäre der laufende Job eines
beschäftigten Remote-Workers eine falsche Leiche.
13 Tests, unter anderem: „laufender Job wird nicht angetastet" und
„schweigender Worker verhindert jedes Urteil".
### GEBAUT — Pfad-Prüfung gehärtet
Elf Stellen prüften mit nacktem `startswith(MEDIA_ROOT)`. `/app/media-boese/x`
beginnt mit `/app/media`, liegt aber außerhalb — betroffen waren auch `/browse`
und `/browse/mkdir`, wo der Pfad vom Nutzer kommt. Neuer Zwillings-Helfer
`unter_wurzel()` in `api/main.py` und `worker/tasks.py`, alle elf Stellen
umgestellt, Tests in beiden.
Nebenbefund dabei: `_zielbasis()` benutzte `os.path.normpath` — unter Windows
werden daraus Backslashes und die Prüfung greift nicht mehr; das gewählte Ziel
wäre still auf den Standard zurückgefallen. Genau die Falle, die
`_arbeitsverzeichnis()` drei Zeilen weiter dokumentiert und mit `posixpath`
vermeidet. Live war es nie (nur aus `rip_disc`, das auf Windows verriegelt
ist), jetzt konsistent.
### GEFUNDEN, NICHT ANGETASTET — vier tote Endpunkte
Nirgends im UI referenziert (mechanisch gegengeprüft: alle `api.*`-Aufrufe
gegen alle Routen):
| Endpunkt | Lage |
|---|---|
| `POST /jellyfin/format` | ersetzt durch `medien.py` im Worker; zieht `nfo_generator.py` + `image_downloader.py` in der API mit, die sonst niemand nutzt |
| `POST /prescan` | ohne Aufrufer (die `PreScan`-Klasse selbst wird woanders sehr wohl gebraucht) |
| `GET /stream/jobs` | niemand konsumiert ihn; der Kommentar behauptet einen „Fix 23.07.", der ihn funktionsfähig machte — gebraucht wird er trotzdem nicht |
| `GET /worker-setup/windows-gui` | seit der `.exe` (v3.9) unreferenziert |
Bewusst nicht entfernt: `test_api_smoke.py` prüft `/prescan` als verdrahtete
Route, und Entfernen ist eine Entscheidung, keine Reparatur. **Der Commander
entscheidet.**
### NOCH OFFEN / EHRLICH UNGEKLÄRT
- **Was die Platte am 25.07. mittags füllte, ist nicht belegt.** Es gibt keine
Kopier-Reste, kein `original`-Verzeichnis und **keinen einzigen Log-Eintrag
von `_original_aufheben`** — das loggt in beiden Zweigen. Zwischen 10:21:50
und 17:49 steht überhaupt nichts im Log. Der EXDEV-Mechanismus ist jetzt
belegt (siehe oben), dass er DAMALS zuschlug, ist es nicht.
- **`tracks:/dev/sr0` meldet `{"status":"done","tracks":[]}`** — null Titel für
eine Disc, die MakeMKV mit `TCOUNT:5` öffnet. Nicht weiter verfolgt.
- **Der Discord-Webhook und der MakeMKV-Key liegen im Klartext** in der
settings-Tabelle. Für Heimnetz erwartbar, aber der Webhook ist eine echte
Zugangsberechtigung.
- **Zwei neue API-Tests laufen nur in der Ampel** — `test_api_smoke.py`
überspringt sich unter Windows selbst (`main.py` braucht `fcntl`).
### GEBAUT — „Abbrechen" wirkt jetzt sofort
Der Fund aus dem Nachtrag (Commander, am laufenden Job beobachtet) ist behoben.
Ursache war genau wie dort beschrieben: Der Abbruch wurde nur in
`datei_fortschritt` geprüft, und diese Closure stieg oben sofort wieder aus,
wenn sich die Prozentzahl nicht geändert hatte. Bei einem Prozent je halber
Stunde sah „Abbrechen" entsprechend lange wirkungslos aus.
Jetzt gibt es in `run_handbrake` einen **eigenen Abbruch-Kanal** neben dem
Fortschritts-Callback — dasselbe Muster, das `run_makemkv` schon für `log_cb`
benutzt, und aus demselben Grund. Er wird bei JEDER Ausgabezeile aufgerufen
und ist im Worker auf 5 Sekunden gedrosselt (`ABBRUCH_INTERVALL_SEKUNDEN`).
Nebeneffekt: Er greift auch während des Scan-Durchlaufs, der überhaupt keine
Encode-Prozente liefert — dort war ein Abbruch vorher grundsätzlich unmöglich.
Die Leseschleife ist als `_handbrake_schleife()` herausgezogen, damit die
Reihenfolge (Abbruch VOR Fortschritt) ohne echtes HandBrake testbar ist.
### ERLEDIGT UM 19:30 — deployt und live gegengeprüft
`a1aabd5` läuft auf der VM. Belege, nicht Behauptungen:
- **Container:** alle 5 up, api/postgres/redis `healthy`.
- **Die neue Auslese antwortet ehrlich** (`GET /capabilities`):
`cpu-x264, cpu-x265, cpu-av1`**kein Phantom-`vaapi`** mehr, obwohl die
alte Fassung es bei vorhandenem `/dev/dri` gemeldet hätte. Dazu
`cpu_modell: QEMU Virtual CPU version 2.5+`, `cpu_kerne: 4`,
`cpu_simd: sse4_2` → die AVX2-Warnung im UI greift.
- **Zombie-Erkennung:** 19:26:09, exakt 120 s nach Worker-Start,
`{'geprueft': 0, 'aufgeraeumt': []}` — nachgesehen, nichts in Arbeit
gefunden, korrekt nichts angetastet.
- **604 Disc-Schlüssel** haben den Rebuild überlebt.
- **Aufgeräumt:** das unbrauchbare 4K-Fragment (67.633.152 B) gelöscht; der
Akira-Job wurde um 19:07 aus der Liste entfernt, wodurch der 75-GB-Rohschnitt
verwaiste (kein Job-Datensatz zeigte mehr darauf, im UI unsichtbar,
„Neu komprimieren" unmöglich) — auf Entscheid des Commanders gelöscht, samt
vier leerer Alt-Ordner. **Platte: 37 GB → 111 GB frei.**
⚠️ **Merken für die Job-Verwaltung:** „Job aus der Liste entfernen" löscht
bewusst keine Dateien — das ist richtig, macht die Rohdaten aber unerreichbar,
weil beides nur über die Job-ID verbunden ist. Ein Rohschnitt ohne Job-Zeile
taucht nirgends mehr auf. Das UI sollte beim Löschen sagen, wie viel daneben
liegen bleibt. Nicht gebaut.
### NOCH OFFEN
**1. Die UHD-Strategie** (aus dem Nachtrag, unverändert gültig).
Preset-je-Disc-Typ war richtig, reicht aber nicht: Software-HEVC in 4K ist auf
dieser CPU keine Option. Drei Wege, keiner davon gebaut:
- **UHD gar nicht komprimieren** — Roh-MKV behalten. Ehrlichste Variante,
kostet Platz (75100 GB je Film, gehört dann auf die NAS).
- **Hardware-Encoder** — Remote-Worker mit GPU (`nvenc`/`vaapi`). Rippy kann
das schon routen (bewiesen v3.7); es fehlt die Maschine. Achtung: Das
Worker-Image kann selbst keinen Hardware-Encoder, ein GPU-Worker braucht ein
HandBrake-Build mit `nvenc_*`/`qsv_*` — die neue Anzeige sagt das jetzt.
- **CPU-Typ der VM auf `host`** — schaltet AVX2 frei, bringt bei x265 typisch
Faktor 24. Aus 2855 h werden damit aber immer noch Stunden bis Tage; das
allein löst 4K nicht, hilft aber jedem 1080p-Encode.
**2. Vier tote Endpunkte** — entfernen oder behalten (Tabelle oben). Alle vier
sind Überreste eines ersetzten Entwurfs: `/jellyfin/format` (die Arbeit macht
seit v3.2 `medien.py` im Worker), `/prescan` (Metadaten-Seite ist seit v3.4
weg; die `PreScan`-Klasse selbst wird sehr wohl gebraucht), `/stream/jobs`
(kein `EventSource` im UI — das Dashboard pollt `setInterval(…, 4000)`) und
`/worker-setup/windows-gui` (seit der `.exe` in v3.9 ohne Aufrufer; die Datei
steckt weiter in der `.exe`). **`/stream/jobs` ist der unangenehmste:** keine
harmlose Leiche, sondern eine Endlosschleife je Verbindung, die jeder erreichen
kann. Empfehlung: alle vier raus, plus `nfo_generator.py` und
`image_downloader.py` in der API (nutzt sonst nichts) — und `test_api_smoke.py`
prüft `/prescan` als verdrahtete Route, der Test muss also mit.
**3. Akira liegt jetzt gar nicht mehr vor.** Fragment und Rohschnitt sind
gelöscht (siehe oben). Wer den Film will, legt die Disc neu ein — der
Schlüsselspeicher steht (604 Schlüssel), der Rip dauert rund eine Stunde. Vor
dem nächsten UHD-Versuch aber Punkt 1 entscheiden, sonst läuft dasselbe
50-Stunden-Rennen wieder an.
---
## Vorheriger Stand: v3.13 — ÜBERGABE (25.07.2026, 18:05)
> Dieser Block ist eine Übergabe an die nächste Sitzung. Er trennt bewusst
> **gemessen** von **vermutet** — in dieser Sitzung wurden zwei Behauptungen
> aufgestellt, die sich als falsch erwiesen (siehe „Vertrauensnotiz" unten).
> Im Zweifel: selbst nachmessen, nicht diesem Dokument glauben.
### GEMESSEN, Stand 18:05 (alles per Befehl auf der VM geprüft)
- **Container:** api/postgres/redis/ui/worker alle `Up`, api + postgres +
redis `healthy`.
- **Platte:** `/dev/sda2` 148 G, 105 G belegt, **37 G frei** (75 %).
- **Läuft gerade:** `HandBrakeCLI --input
/app/temp/raw/73b89777-…/title_t00.mkv --output
"/app/media/movies/Akira (1988)/title_t00.mkv" --preset
"H.265 MKV 2160p60 4K" --all-audio --all-subtitles`
- **Job `73b89777-a808-426f-8159-d406d27d0ec8`:** `status='transcoding'`,
`progress=99` (⚠️ **Altwert aus dem Absturz, NICHT der echte Fortschritt** —
der Lauf hat um 18:02 begonnen), `disc_type='uhd'`,
`output_path='/app/media/movies/Akira (1988)'`.
- **Dateien:** `…/Akira (1988)/title_t00.mkv` ist **0 Bytes** (HandBrake hat
die vorherige 1080p-Fassung beim Start gekürzt und schreibt gerade neu).
Roh-Rip `/app/temp/raw/73b89777-…/` = **75 GB, unversehrt.**
- **Git:** `8bb075c` ist HEAD und deployt. Davor `c065967`, `e84afc1`,
`f449c4e`, `0935766`. Alle Ampeln waren grün.
- **Schlüsselspeicher:** 604 Disc-Schlüssel, `/system/keystore` antwortet.
- **NAS:** `//192.168.178.62/rippy` auf `/srv/rippy/media/rippy` gemountet,
erreichbar, **2311 GB frei**. Wird als Ablageziel gelistet.
### NACHTRAG 18:35 — der 4K-Lauf wurde abgebrochen, zwei neue Befunde
- **4K-HEVC ist auf dieser CPU nicht machbar.** Gemessen: von 18:02 bis 18:31
kam der Lauf von 0 auf **1 %** → hochgerechnet **~50 Stunden** für den Film.
Der Commander hat um 18:31 abgebrochen. **Konsequenz, die noch zu
entscheiden ist:** entweder UHD gar nicht komprimieren (Roh-MKV behalten,
`transcodeEnabled` für UHD aus), oder ein Hardware-Encoder (Remote-Worker
mit GPU, `nvenc`/`vaapi`), oder bewusst bei 1080p bleiben. Das eben
gebaute Preset-je-Disc-Typ ist damit richtig, aber allein nicht genug.
- **Abbruch wirkt verzögert (FEHLER, nicht gebaut).** Job steht seit 18:31 auf
`canceling`, **HandBrake lief um 18:35 immer noch**. Ursache: Der Abbruch
wird nur in `datei_fortschritt` geprüft, und diese Closure läuft nur, wenn
sich die PROZENTZAHL ändert (`tasks.py`, `if gesamt == letzter[0]: return`).
Bei 1 % alle 30 Minuten heißt das: bis zu eine halbe Stunde, in der
„Abbrechen" für den Nutzer wirkungslos aussieht. Der Abbruch muss unabhängig
vom Fortschritt geprüft werden (z. B. zeitgesteuert beim Lesen der
HandBrake-Ausgabe).
- **Die 1080p-Fassung ist weg.** HandBrake hat sie beim Start auf 0 Bytes
gekürzt; aktuell liegen dort ~64 MB 4K-Fragment. Der **75-GB-Rohschnitt ist
unversehrt** — es ist also nichts unwiederbringlich verloren, aber im
Akira-Ordner liegt gerade eine unbrauchbare Datei.
### WAS ALS NÄCHSTES ANSTEHT
1. **Ausgang des 4K-Laufs prüfen.** Erwartung, die noch NICHT bewiesen ist:
Der Job endet **erfolgreich**, und „Original behalten" (Setting steht auf
`true`) meldet nur eine **Warnung**, weil 75 GB nicht neben 37 GB freien
Platz passen. Genau dieser Pfad ist neu (`_original_aufheben` in
`tasks.py`) und in der Praxis noch nie gelaufen. Wenn er hält, ist der
Ausfall von heute Mittag strukturell behoben.
2. **Zombie-Erkennung fehlt (gefunden, NICHT gebaut).** Nach dem Absturz stand
der Job auf `transcoding 96 %`, obwohl weder ein Prozess lief noch etwas in
den Celery-Queues stand (beide `llen` = 0). Folge: keine Anzeige, kein
Download, und der „Neu komprimieren"-Knopf fehlt, weil
`_kann_neu_komprimieren` (main.py) `status == "failed"` verlangt. Der
Worker sollte beim Start solche Leichen erkennen und ehrlich auf `failed`
setzen.
3. **`keepOriginal` steht auf `true`** — von dieser Sitzung gesetzt, um den
4K-Rohschnitt zu retten. Bewusst entscheiden, ob das so bleibt: mit
Arbeitsverzeichnis auf der NAS-Freigabe wäre es ein reines Umhängen statt
einer Vollkopie.
4. **75 GB Rohschnitt** liegen weiter in `/app/temp/raw/73b89777-…`. Nach
erfolgreichem 4K-Lauf entscheiden, ob er weg kann.
### VERTRAUENSNOTIZ — zwei Fehlschlüsse dieser Sitzung
- **„MakeMKVs Schlüssel-Kanal ist abgeschaltet"** — falsch. Stützte sich auf
zwei tote Hostnamen aus alten Forumsbeiträgen, die MakeMKV längst nicht mehr
benutzt. Richtig: `makemkvcon` unter **Linux** fragt nie, die
**Windows**-Version schon. Korrigiert in v3.11.
- **„Celery hat die Aufgabe nach dem Deploy erneut zugestellt"** — falsch.
Aus dem Status `transcoding` geraten, ohne Prozesse oder Queues zu prüfen.
Es war ein Zombie-Eintrag (Punkt 2 oben).
Beide Male war das Muster dasselbe: **aus einem Zustandswert auf einen
Mechanismus geschlossen, statt den Mechanismus zu messen.**
---
## Vorheriger Stand: v3.12 — Preset je Disc-Typ (25.07.2026)
- **Der Fund:** Beim ersten echten UHD-Rip aufgefallen — die Kompression
fragte den Disc-Typ **gar nicht**: `preset = einstellungen.get(
"transcodePreset")`, ein globales Preset für alles. Live eingestellt war
`HQ 1080p30 Surround`. Der laufende Akira-Rip wäre also verlustfrei in 4K
gerippt und danach **auf 1080p heruntergerechnet** worden — und mit
`keepOriginal: False` wäre der 4K-Rohschnitt anschließend gelöscht worden.
Aufgefallen ist es nur, weil der Commander gefragt hat, ob Rippy das
UHD-Preset automatisch nimmt.
- **Sofortmaßnahme am laufenden Job:** `keepOriginal` auf `True` gesetzt
(nur dieses eine Feld, gegengeprüft: kein anderer Schlüssel verändert).
Damit überlebt der 4K-Rohschnitt die Kompression auf jeden Fall.
- **Gebaut:** `preset_fuer(disc_type, einstellungen)` in `ripping.py` (pure,
getestet) plus drei Einstellungen `transcodePresetDvd` / `…Bluray` /
`…Uhd`. Reihenfolge: Preset des Disc-Typs → allgemeines
`transcodePreset` → `DEFAULT_HB_PRESET`. **Bestandsinstallationen ändern
ihr Verhalten nicht**, solange die neuen Felder nicht gespeichert sind.
`transcode_files` holt den Disc-Typ aus dem Job-Datensatz und schreibt ihn
mit ins Log.
- **UI (Einstellungen → Verarbeitung):** drei Auswahlfelder statt einem, mit
Klartext dazu, warum 4K auf ein 2160p-Preset gehört. Alle Preset-Namen
stammen aus `HandBrakeCLI --preset-list` im Worker-Image (1.6.1) — nicht
geraten (AGENTS Regel D).
- **⚠️ Deploy bewusst zurückgehalten:** `docker compose up -d --build`
würde den Worker-Container neu erstellen und den **laufenden Akira-Rip
abbrechen**. Erst deployen, wenn der Job durch ist. Der 4K-Rohschnitt ist
durch `keepOriginal` geschützt; danach reicht „Neu komprimieren" im UI,
um mit dem richtigen Preset in 4K zu komprimieren.
- **Nebenbefund:** `ps` gibt es im Worker-Image nicht (python-slim). Frühere
Prüfungen auf laufende Rips per `ps | grep` lieferten deshalb still
„nichts aktiv" — richtig geht es über `/proc`.
---
## Vorheriger Stand: v3.11 — 4K-UHD GELÖST: Akira geht auf (25.07.2026)
- **🎉 Der Durchbruch:** Nach Übernahme des Schlüsselspeichers öffnet
`makemkvcon` auf der VM die Akira-UHD: „Operation successfully
completed", **`TCOUNT:5`**, fünf Titel — identisch zum Windows-Ergebnis.
**Erster belegter UHD-Disc-Zugriff auf der Rippy-Maschine.**
- **Die echte Ursache — und sie ist eine andere als in v3.10:**
`makemkvcon` unter **Linux** ruft Disc-Schlüssel **nie** ab. Die
**Windows**-Version tut es. Gegenprobe mit demselben Laufwerk und
derselben Disc:
| | Linux (Worker) | Windows |
|---|---|---|
| Verbindungen beim Disc-Öffnen | **keine einzige** | 185.84.108.20:443 |
| Meldung 3338 „Downloading latest HK" | nie | ja |
| `_private_data.tar` | 2048 B, **0** Schlüssel | 6,4 MB, **604** Schlüssel |
| Disc | „volume key is unknown" | **geht auf** |
Gegengeprüft mit leerem UND gefülltem Speicher, mit und ohne `--noscan`,
mit `dev:/dev/sr0` und `disc:0`, mit gelöschter `update.conf`. Immer:
keine Verbindung. Die Meldungsvorlage „Downloading latest %1 to %2 ..."
steckt sehr wohl im Linux-Binary — sie löst nur nie aus. Gleiches
Symptom im MakeMKV-Forum, seit Jahren offen (t=25782, t=34022).
- **⚠️ Richtigstellung zu v3.10 (direkt darunter):** Dort steht, MakeMKVs
Schlüssel-Kanal sei abgeschaltet. **Das war falsch.** Die Herleitung
stützte sich auf zwei Hostnamen aus alten Forumsbeiträgen
(`hkdata.fairuse.org`, `hkdata.crabdance.com`), die tatsächlich nicht
mehr auflösen — MakeMKV benutzt sie aber längst nicht mehr. **Der Dienst
lebt, der Worker erreicht ihn sogar** (Verbindungstest auf
185.84.108.20:443 erfolgreich); er wird unter Linux nur nie gefragt.
Aufgedeckt hat das der Commander mit dem Einwand, unter Windows ginge es
sofort. Alles in v3.10 Gebaute bleibt richtig und nötig — nur die
`KEYDB.cfg` ist nicht der Haupt-, sondern der Ersatzweg.
- **Gebaut — Schlüsselspeicher übernehmbar:** `GET`/`POST
/system/keystore` plus die Helfer in `makemkv_daten.py` (beide
Zwillinge). Der Rohkörper der Anfrage IST die Datei — binär, deshalb
kein JSON und kein Base64. Die Prüfung lehnt einen Speicher **ohne**
`hkd_*.bin` ab, sonst lädt jemand den leeren Vorrat einer frischen
Installation hoch und wundert sich, dass nichts passiert.
- **Gebaut — UI:** neuer Block „Disc-Schlüssel für 4K-UHD" **über** dem
KEYDB-Block, mit Schlüssel-Anzahl, Upload und der Schritt-für-Schritt-
Anleitung für den Windows-Umweg. KEYDB.cfg ist jetzt als **Notnagel**
beschriftet. Die Worker-Plakette zeigt die Anzahl bekannter Schlüssel;
`0` heißt sichtbar „4K-UHD scheitert".
- **Gebaut — ehrliche Texte:** Der UHD-Fehlertext nennt jetzt den
Windows-Weg und die Anzahl der Schlüssel dieses Workers. Alle
Falschaussagen korrigiert: UI (3 Stellen), Anleitung (2), README (3),
KONZEPT §8 + §10, Modulkopf von `makemkv_daten.py`, Worker-Dockerfile
und `makemkv_key.py` (dort stand: „Den AACS-Schlüssel zieht MakeMKV via
LibreDrive ohnehin selbst aus dem Laufwerk" — gilt für Blu-ray, NICHT
für UHD).
- **So hältst du den Vorrat aktuell:** Laufwerk an den Windows-PC, Disc in
MakeMKV öffnen, dann `_private_data.tar` aus dem MakeMKV-Datenverzeichnis
(*Preferences → General*) unter Einstellungen → System hochladen. Der
Speicher der Windows-Installation liegt bereits auf der VM unter
`/srv/rippy/makemkv/`.
- **Offen:** Der volle UHD-Rip inklusive Transcode-E2E ist noch nicht
durch — belegt ist der Disc-Zugriff, nicht die komplette Kette bis zur
fertigen Datei. Und ob sich der Abruf unter Linux doch anstoßen lässt,
ist ungeklärt; der Code dafür ist im Binary vorhanden.
---
## Vorheriger 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
ps2exe kompiliert: eingebettetes Rippy-Disc-Icon, KEIN Konsolenfenster
(-noConsole), WinForms (-STA). Vorgebaut auf Windows
(deploy/worker-windows/build-exe.ps1) + committet — Windows-.exe geht
nicht von Linux (Rippy-Host). API: GET /worker-setup/windows-exe.
UI (Worker → Windows): „Installer herunterladen (.exe)" als Haupt-Weg.
**E2E bewiesen**: von der VM geladen → Fenster öffnet, conhost-Zähler
unverändert (kein Konsolenfenster), Icon eingebettet.
- **Wichtig — friert nichts ein**: Die .exe holt HandBrake zur Laufzeit
(GitHub latest) und den Worker-Code live von Rippy. Nur bei Änderung der
GUI (install-gui.ps1) neu bauen (build-exe.ps1), NICHT bei Tool-Updates.
- **Update-Story dokumentiert** (Commander-Frage): MakeMKV → .env-Bump +
Rebuild (Update-Check zeigt es); HandBrake Docker = Debian-stabil,
native Worker = latest beim Installieren.
---
## Vorheriger Stand: v3.8 — Grafischer Windows-Installer + Versionslage klar (24.07.2026)
- **Grafischer Windows-Installer** (statt CLI): install-gui.ps1 (WinForms,
ASCII-only) — Fenster mit Feldern (Rippy-IP, Worker-Name, Autostart) +
Install-Knopf mit Live-Log + „Worker starten". Gleiche Schritte wie der
CLI-Installer (Worker-Code, HandBrake latest, venv, Tray/Start/Uninstall).
UI (Worker → Windows): Knopf „Grafischen Installer herunterladen (.bat)" —
die .bat wird CLIENT-seitig mit der eingetragenen LAN-IP erzeugt,
Doppelklick lädt+startet die GUI von Rippy (GET /worker-setup/windows-gui).
CLI-Befehl bleibt als Profi-Alternative. Bewiesen: GUI-Fenster öffnet
sauber auf dem Commander-PC (Titel „Rippy Encoding-Worker - Installation").
- **Werkzeug-Versionslage exakt dargestellt** (Commander-Wunsch): System-Tab
erklärt jetzt klar — MakeMKV aktualisierbar (wichtig wegen Schlüssel-DB),
HandBrake im Docker-Worker bewusst Debian-stabil (1.6.1, KEIN Alarm mehr),
native Worker holen die neueste. Update-Check zeigt MakeMKV mit „✓ aktuell"
bzw. Update-Befehl, HandBrake neutral als Info.
---
## Vorheriger Stand: v3.7 — Drei Praxis-Bugs (Mounts, HandBrake, Encoder-Wahl) (24.07.2026)
Alle drei live auf der VM verifiziert:
- **Speicher-Mounts robust**: Ein toter CIFS-Mount (NAS weg/Rebuild) war
mounted:false, verschwand aus „Verfügbare Ziele" und ließ sich nicht neu
anlegen („Name existiert"). Jetzt: `reachable`-Status (bounded `timeout 3
ls` — Endpoint lädt in 0,014 s statt 10 s), `umount -l`-Fallback,
`reparieren()` (lazy abhängen + frisch mounten), Re-Add repariert statt
409, POST /storage-mounts/{name}/repair, eigene „Netzwerk-Mounts"-Liste im
UI mit Status + Reparieren/Entfernen. Bewiesen: Reparatur des `rippy`-
Mounts → mounted:true, writable:true.
- **HandBrake-Versionen konsistent**: Windows-Installer zieht dynamisch die
neueste (GitHub latest) — bewiesen: HandBrake 1.11.2 installiert (statt
fest 1.9.2). Update-UI erklärt: Docker = Debian-stabil (bewusst älter),
Windows = neueste. Nebenbei ein latenter Installer-Bug gefixt: PowerShell
5.1 liest .ps1 als ANSI — ein Gedankenstrich zerschoss das Skript →
install.ps1 ist jetzt ASCII-only.
- **Encoder-/Worker-Wahl beim Rip**: Celery `worker_direct=True`, gezieltes
Routing (transcode_queue) mit sicherem Fallback auf die geteilte Queue.
Bewiesen mit ping_worker: Task landet exakt beim gewählten Node. Dropdown
im Rip-Dialog ab 2 Online-Workern. Node-Zuordnung disambiguiert bei
mehreren Workern pro Host. Nebenfund gefixt: DeviceDiscovery leitete die
Titel-Auswahl (titles) gar nicht weiter.
⚠️ Gezieltes Routing braucht AKTUELLEN Worker-Code (worker_direct) — ein
vor dieser Version installierter Remote-Worker pingt zwar, konsumiert aber
seine Direkt-Queue nicht → neu installieren.
- **Voller Transcode-E2E weiterhin durch die UHD-Key-Lage blockiert** (keine
entschlüsselbare Disc) — die Routing-Mechanik ist per ping_worker bewiesen.
---
## Vorheriger Stand: v3.6 — Design 2.0 gelandet + Ein-Branch-Umstellung (24.07.2026)
- **Design 2.0 ist in `main`** (von Gemini umgesetzt): UI von 428
`theme === 'dark'`-Ternaries auf Tailwind-`dark:` + UI-Primitives
(`components/ui/`, `lib/design.ts`) umgebaut, „Cinematic Cinema OS"-Look.
Ampel grün (49 Tests, Ruff, Vite-Build), Live auf der VM.
- **Nur noch EIN Branch: `main`** (Commander-Entscheid). Der
`stable`-Zwischenbranch samt Grün-Gate ist abgeschafft — er war
vestigial, weil die VM ohnehin aus `main` deployt. Konsequenzen:
- ci.yml: Beförderungs-Schritt raus, die Ampel PRÜFT nur noch.
- deploy.sh: nutzt `main` statt `stable`.
- AGENTS §C / README / DESIGN-Briefing entsprechend umgeschrieben.
- Gelöschte Branches: `stable`, `design-2.0` (gemergt), `kernumbau-2026-07-23` (alt).
- **Deploy-Weg ab jetzt:** push → Ampel grün prüfen → auf der VM
`git pull` (in ~/projects/rippy) bzw. `./deploy.sh`.
---
## Vorheriger Stand: v3.5 — Track-Tabelle, nativer Windows-Worker, Anleitungs-Tab (24.07.2026, Claude)
- **Volle Titel-Auswahl vor dem Rip**: „Disc scannen" im Rip-Dialog →
Worker-Task scan_tracks (makemkvcon info; Ergebnis via settings-Tabelle
'tracks:<device>', UI pollt) → Tabelle mit Checkbox/Dauer/Größe/Kapiteln,
Vorauswahl ab 5 min. Gewählte Titel gehen als meta.titles in den Job;
der Worker rippt sie einzeln (makemkvcon kann pro Aufruf nur einen Titel
oder all), Fortschritt anteilig. Parser mit Tests (apdefs-Attr 8/9/11).
- **Nativer Windows-Worker (ohne Docker!)**: Einstellungen → Worker bietet
jetzt ZWEI Varianten — Linux (Docker, wie gehabt) und Windows (nativ,
braucht nur Python). Die Rippy-Instanz versorgt sich selbst:
GET /worker-setup/windows liefert install.ps1, /worker-setup/paket den
Worker-Code als Zip (beides liegt via api-Dockerfile im Image).
install.ps1: venv + Abhängigkeiten, HandBrakeCLI 1.9.2 vom offiziellen
GitHub-Release (URL verifiziert), start-worker.bat (celery -Q transcode
--pool=solo), optional -Autostart (schtasks onlogon). tasks.py lädt auf
Windows ohne fcntl (Import-Guard); rip_disc ist dort hart verriegelt.
Für echte Transcodes übersetzt RIPPY_PATH_MAP die Container-Pfade aufs
Netzlaufwerk (pfad_lokal, mit Tests) — Voraussetzung bleibt eine
Freigabe der Rippy-Ablage (derselbe offene Infra-Punkt wie beim
Linux-Remote-Worker/AI-Box).
**E2E-BEWEIS (24.07., Commander-PC):** Installer in einem Rutsch
durchgelaufen (venv, Abhängigkeiten, HandBrakeCLI 1.9.2), Worker
gestartet und in Rippy erschienen:
`test-windows-nativ | online: True | HandBrake: 1.9.2 | IP: 192.168.178.98`
— echte LAN-IP, Celery-Ping grün. Testeintrag danach wieder entfernt.
- **Tray-Symbol + rückstandsfreie Deinstallation (v3.5.1, bewiesen):**
tray.py (pystray/Pillow, nur Windows-Worker) — Disc-Symbol neben der
Uhr mit Status, Start/Stopp, „Rippy öffnen", Log, Beenden; Celery läuft
als Kind ohne Konsolenfenster. install.ps1 erzeugt start-tray.bat
(empfohlen), start-worker.bat (Debug) und uninstall.ps1. **E2E bewiesen
auf dem Commander-PC:** Frisch installiert → Tray + celery-Kind liefen →
`tray-test` online in Rippy → uninstall.ps1 -Force stoppte alle
Prozesse, meldete den Worker per DELETE /workers/{name} ab und löschte
den Ordner restlos (danach: 0 Prozesse, Ordner weg, in Rippy nur noch
rippy-hauptworker).
- **Anleitungs-Tab** im UI: kompletter Selbsterklärer (Disc-Weg, Serien,
NAS, Media-Server, Benachrichtigungen, Key, Worker, Downloads, FAQ).
- Design 2.0 kommt bewusst in eine FRISCHE Session (Commander-Entscheid) —
Infrastruktur steht (darkMode 'class'), reine Konvertierungsarbeit.
---
## Vorheriger Stand: v3.4 — Restefeger: der Ideen-Katalog ist abgearbeitet (24.07.2026, Claude)
**Commander-Entscheid: AUTH IST KOMPLETT RAUS** (Heimnetz-only; das UI hatte
nie einen Login, die Endpoints waren Placebo, passlib/bcrypt brach die Ampel).
/token + /api-keys + auth.py + Abhängigkeiten entfernt, JWT_SECRET_KEY ist
keine Pflicht mehr — der Schnellstart läuft ohne .env-Zwang. KONZEPT §10.
**Neu in v3.4 (Details in ROADMAP Etappe 15):**
- **Serien-Flow**: Rip-Dialog fragt Serienname + Staffel → Ablage
`<Serie>/Season NN`, tvshow.nfo/poster im Serien-Ordner, und die
**Episoden werden per Laufzeitabgleich (TMDB) erkannt und benannt**
(„Serie S01E02.mkv") — nur bei eindeutiger Zuordnung, sonst ehrliches Log.
Komplette Staffel auf einer Disc klappt auch bei uniformen Anime-Laufzeiten.
- **Jellyfin/Emby-Refresh** nach jedem fertigen Rip (URL/API-Key +
Test-Knopf in Einstellungen → Ripping) — Disc rein, Film erscheint im
Server, null Klicks dazwischen.
- **Duplikat-Warnung** per Disc-Fingerabdruck (Karte zeigt „bereits
gerippt", Vollautomatik überspringt).
- **„Nur Hauptfilm" funktioniert jetzt wirklich** (Info-Lauf → längster
Titel; pro Rip im Dialog wählbar). Vorher wirkungsloses Setting.
- **OMDb-Treffer werden eingedeutscht** (TMDB /find über die IMDb-ID).
- Dashboard: **Speicherplatz-Anzeige** (amber < 60 GB) + **CSV-Export**.
- **Metadaten-Seite entfernt** (Korrektur-Popup ist der einzige Weg) samt
Placebo-Endpoints; Doppel-Jahr „(2009) (2009)" gefixt.
- **Remote-Worker-Blocker behoben**: redis/postgres waren NIE veröffentlicht
— Ports 6379/5432 jetzt offen, API_URL für Worker gesetzt. Damit sind
AI-Box/Windows-Worker überhaupt erst anschließbar.
**Bewusst offen** (ROADMAP „Ideen-Katalog (Rest)"): volle Track-Tabelle,
nativer Windows-Worker (braucht Testlauf auf Ziel-Hardware), Design-2.0-
Konvertierung (Infrastruktur steht), AI-Box-NFS-Export, Kodi-Refresh.
---
## Vorheriger Stand: v3.3 — Praxis-Feedback-Runde (24.07.2026, Claude)
**Zwei echte Bugs mit Beweis gefixt:**
1. **SMB-Mount „Unable to apply new capability set"**: mount.cifs hebt
CAP_DAC_READ_SEARCH an — die fehlt in Dockers Default-Caps. Auf der VM
reproduziert (Bounding-Set a82425fb, Bit 2 fehlt) und mit
`cap_add: DAC_READ_SEARCH` bewiesen behoben (docker-compose.yml).
2. **TMDB fiel still aus**: Der Client konnte nur v4-Bearer-Tokens — der
eingetragene übliche v3-Key (32 Hex) bekam still 401, Suche lieferte nur
OMDb. Jetzt beide Key-Arten (ist_v4_token, mit Tests); Einstellungen →
APIs hat „Verbindung prüfen" mit Live-Status je Quelle (am Cache vorbei).
**Neu in v3.3:**
- **4K UHD als eigener Disc-Typ** (classify ≥ 55 GiB, beide detection.py,
Tests): eigene Badge-Farbe überall, Prescan-Label „4K UHD".
- **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" — 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-Schlüssel fehlt" inkl. Forum-Dump-Hinweis
(MakeMKV speichert den AACS-Dump automatisch unter /root/.MakeMKV/).
**⚠️ 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
daliegen (can_retry); Jobs einzeln löschbar (Papierkorb) + „Erledigte
aufräumen" mit Bestätigungs-Dialog — Dateien bleiben immer liegen.
- **„Alle herunterladen"** im Job-Detail (gestaffelte Einzel-Downloads —
bewusst kein Server-seitiges 40-GB-Zip).
- **Worker zuordenbar**: WORKER_NAME-Env als stabiler Anzeigename (compose:
rippy-hauptworker; Remote-Worker: frei wählbar) — fixt zugleich die
Offline-Leichen nach Rebuilds; IP + Container-ID werden mit angezeigt,
verwaiste Einträge sind löschbar (DELETE /workers/{name}). Online-Abgleich
läuft jetzt über info.hostname.
- **Disc-Karte**: „Quelle: TMDB/OMDb/MyAnimeList · xx % sicher" statt des
nackten „Übereinstimmung xx %"; TMDB-Metadaten sind Deutsch (language=
de-DE war schon überall dran — sie kamen nur nie an, siehe Key-Bug).
- **Ripping-Tab nach Medium** gegliedert (Video / Audio-CD / Allgemein) +
Klartext: alle Tonspuren & Untertitel bleiben erhalten (--all-audio/
--all-subtitles) — wichtig für Anime.
- **Ordner-Verwaltung** (Ex-„Dateibrowser") ist jetzt beschriftet, erklärt
und standardmäßig eingeklappt.
- **Docker-Pflicht beim Worker**: ehrlich im UI beantwortet; nativer
Windows-Dienst steht als Ausbaustufe in der ROADMAP (Ideen-Katalog).
---
## Vorheriger Stand: v3.2 — Universal-Komfort-Runde + Ampel entrostet (24.07.2026, Claude)
**Wichtigster Befund zuerst: die Ampel war seit dem 23.07. ROT und `stable`
hing 10 Commits hinter `main`** — deshalb kam nichts Neues mehr auf die VM.
Zwei Ursachen, beide behoben:
1. bcrypt ≥ 4.1 bricht passlib 1.7.4 (`__about__` entfernt → Selbsttest wirft
„password cannot be longer than 72 bytes") → **bcrypt==4.0.1 gepinnt**.
2. Der cache_keys-Test kannte den Disc-Fingerprint im Prescan-Key (23.07.)
nicht → Test an das echte Format angepasst + Fingerprint-Testfall dazu.
**Neu in v3.2 (Commander-Sammelauftrag, alles mit Tests / Vite-Build grün):**
- **Media-Server-Integration**: Setting `mediaServer` (Wizard + Einstellungen →
Ripping): Zielordner „Titel (Jahr)" statt Job-UUID; für Jellyfin/Emby/Kodi
zusätzlich movie.nfo/tvshow.nfo + poster.jpg (Worker: medien.py). Plex = nur
Benennung. Job speichert Disc-Metadaten jetzt mit (jobs.meta, Migration).
- **Benachrichtigungen ECHT**: notify.py in API+Worker (Discord/Slack/ntfy/
generisch, Erkennung an der URL) — das Feld war vorher ein Placebo. Meldung
bei fertig/fehlgeschlagen/abgebrochen; Anleitung + „Test senden" im UI.
- **SMB-Scan-Fix**: „NT_STATUS_ACCESS_DENIED" heißt jetzt im Klartext „Gast-
Abfrage verweigert → Benutzer/Passwort eintragen"; Credential-Felder stehen
im UI VOR dem Auflisten-Knopf. NAS-Ziel-Anlage quittiert per Toast.
- **4K-UHD-Vorsorge**: Arbeitsverzeichnis `workDir` konfigurierbar (auf
NAS-Freigabe legbar), Platz-Check per Disc-Größe (ioctl) VOR dem Rip mit
Klartext-Abbruch statt voller Platte bei 40 GB.
- **Einstellungen → System**: Werkzeug-Versionen je Worker (MakeMKV/HandBrake,
ENV MAKEMKV_VERSION im Image), Key-Quelle (ui/env/keiner), freier Platz;
**MakeMKV-Beta-Key im UI pflegbar** — gilt ab dem nächsten Rip, ohne Rebuild.
- **Job-Detail-Popup** (Klick auf Titel in „Neueste Jobs"): Poster, Jahr,
Beschreibung, Genres, Ablagepfad, Fehler (GET /jobs/{id}/detail).
- **Download fertiger Rips im Browser**: „Download"-Knopf bei fertigen Jobs
(Aktion-Spalte) → Dateiliste mit Größen im Detail-Popup, Stream via
GET /jobs/{id}/files/{name} (Pfad-Validierung strikt unter /app/media,
realpath-Check gegen Symlink-Ausbrüche) — vorher nur per scp erreichbar.
- **UI-Feedback**: Toast-System (ToastContext, ploppt oben rechts), drehende
Refresh-Knöpfe, Logs-Pills um Warnung/Fehler ergänzt, Datei-Browser zeigt
jetzt auch DATEIEN (grau, mit Größe) — der „leere" Bluray-Ordner war voll.
- **Favicon** (Disc, Indigo-Verlauf) + **Footer** „Created with ❤️ by LucyAI,
Claude and KrBrZ".
- **Doku**: README (Media-Server, Benachrichtigungen, UHD-Arbeitsverzeichnis,
„Rippy woanders bereitstellen"), ROADMAP Etappe 13 + Ideen-Katalog,
KONZEPT-Fortschreibungen (Abschnitt 10), AGENTS-Stand.
**Nächste Schritte:** unverändert die Fäden aus v3.1 (unten) + Ideen-Katalog
in der ROADMAP (Serien-Flow, Duplikat-Warnung, Jellyfin-Refresh, Auth).
---
## Vorheriger Stand: v3.1 — E2E BEWIESEN + Universal-Runde (24.07.2026, Claude)
**Der Beweis steht: Evangelion 2.22 (BD-50) komplett durch die Kette** —
erkannt → verlustfrei gerippt (43 GB, LibreDrive) → H.264-komprimiert →
**4,8 GB Endergebnis** (Hauptfilm 3,3 GB + 4 Extras), Rohdaten automatisch
gelöscht. Retry-transcode dabei live bewiesen (x265 war auf 4 vCPUs zu lahm
→ Abbruch + Neustart mit H.264 OHNE Neu-Rip).
Seit v3.0 dazugekommen (alles deployt, Ampel grün):
- **Korrektur-Flow**: „Nicht korrekt?!"-Popup an der Disc-Karte; Suche über
alle Quellen (GET /metadata/search), Wahl wird 30 Tage per
Disc-Fingerabdruck gemerkt (POST /metadata/override)
- **Metadaten-Kette**: BD-Klartext-Titel via ro-Mount (bdmt_*.xml),
Jikan/MAL (keyless, Ähnlichkeits-Score), OMDb-Qualitäts-Gate,
Wort-Strip-Degradation, ehrliches Cache-TTL (unknown nie cachen)
- **Worker-Verwaltung** (Einstellungen → Worker): Live-Erreichbarkeit
(Celery-Ping + Minuten-Herzschlag), Copy-Paste-Anbindung neuer Maschinen
- **Speicherziele**: SMB-Freigaben-Auflistung, Schreibtest beim Mount,
lokaler Ordner-Browser + mkdir; Mounts via UI (CAP_SYS_ADMIN + rshared)
- **First-Run-Wizard**, Job-Abbruch (kooperativ), Transcode als eigener
Task auf Queue `transcode` (Basis Remote-GPU-Worker), ruff.toml gepinnt
**Nächste Schritte:** AI-Box als VAAPI-Transcode-Worker (VCN4; NFS-Export
nötig, deploy/remote-transcode-worker.yml) · TMDB-Key eintragen ·
Arcane-GitSync-Binding auf `stable` (aktuell Notfall-Deploy-Weg) ·
Metadaten-Seite entfernen, wenn Popup abgenommen · Jellyfin-Postprocessing
an Worker · Auth vor schreibende Endpoints · MAKEMKV_APP_KEY erneuern
(~Ende Juli, Forum t=1053).
---
## Vorheriger Stand: v3.0 — Kernumbau (23.07.2026, Claude)
**Der Zweck existiert jetzt wirklich.** Komplett-Audit ergab: „Disc rein →
gerippt raus" hatte nie einen Code-Pfad (Details in ROADMAP Etappe 11).
Kernpunkte des Umbaus:
- **Rip-Kette komplett neu**: POST /jobs → Celery → Worker (MakeMKV 1.18.4
verlustfrei, Etappe 10) → Postgres-Job-Status → UI. CD weiter via abcde.
- **Disc-Erkennung** über Kernel-ioctls (CDROM_DISC_STATUS + Größe), Watcher
pollt statt udev (im Container gibt es kein udev).
- **UI**: echter Vite-Build hinter nginx mit /api-Proxy — vorher Dev-Server
und hartkodiertes localhost:8000 (deshalb waren nie Laufwerke zu sehen).
- **Compose**: Laufwerk als devices: (Bind-Mounts gaben EPERM), Ausgabe auf
media-Volume (vorher /output → nicht gemountet + read-only-Falle).
- **Placebos entfernt**: Mock-Logs, toter SSE-Stream, udevadm-Discovery,
14 Troubleshooting-Altdateien (ARCAN-*.md, Setup-Skripte, udev-Rules).
**Hardware-Kette (23.07.):** Laufwerk (Verbatim 4K BD RW, BU40N 1.05) hängt am
Proxmox-Host; LPM-Quirk `usbcore.quirks=18a5:0428:k` gefixt die Serien-Disconnects
(reboot-fest in GRUB). VM 106 bekommt es per USB-Passthrough `host=18a5:0428`.
⚠️ Firmware 1.05 kann KEIN UHD-Ripping (verschlüsselt) — Crossflash auf 1.03-MK
ist der dokumentierte Weg, separater Faden. DVD/BD normal geht.
**Offen:** E2E mit echter Disc · Jellyfin-Post-Processing an Worker anbinden ·
Auth vor schreibende Endpoints · MAKEMKV_APP_KEY in Arcane-Env pflegen (monatlich).
---
## Vorheriger Stand
**v1.10 — Complete Dark Mode Implementation (22.07.2026)**
Rippy läuft auf Arcane VM (192.168.178.162):
- ✅ Arcane WebUI: http://192.168.178.162:3552
- ✅ Rippy UI: http://192.168.178.162:80
- ✅ Rippy API: http://192.168.178.162:8000
- ✅ Theme Context ausgelagert (App.tsx → ThemeContext.tsx + useDarkMode.ts)
- ✅ Config Validation mit TMDB-API-Key Pflicht
- ✅ Cache Key Centralization (cache/keys.py)
- ✅ Dark Mode mit Theme-Toggle und localStorage persistence (UI-Only)
- ✅ **Alle UI-Komponenten komplett auf Dark Mode aktualisiert:**
- `App.tsx`, `Dashboard.tsx`, `Settings.tsx`, `MetadataPreview.tsx`
- ✅ Sidebar Navigation mit Dark Mode Support
- ✅ Einstellungen-Page mit Tab-Struktur
- ✅ Doppelte Arcane-Einträge behoben (`rippy` statt `Rippy`)
- ✅ Import-Fixes (relative → absolute Imports)
- ✅ Git Sync in Arcane konfiguriert
- ✅ Commit: `d2fb281` — Complete Dark Mode Implementation
### Docker-Container
| Container | Port | Status |
|-----------|------|--------|
| rippy-api | 8000 | Healthy |
| rippy-worker | - | Running |
| rippy-ui | 80 | Running |
| rippy-postgres | 5432 | Healthy |
| rippy-redis | 6379 | Healthy |
### Features
- ✅ **Etappe 3**: SQLite-Cache, TMDB/MusicBrainz/TheTVDB Clients, Pre-Scan, Metadaten-Preview
- ✅ **Etappe 4**: Jellyfin-Formatierung (NFO-Generator, Image-Downloader)
- ✅ **Etappe 5**: JWT-Auth (15min/7T), Rate-Limiting (100/min), API-Key-Management
- ✅ **Etappe 6**: Docker read_only, tmpfs, healthchecks, minimale Images
- ✅ **Etappe 7**: API UI Modernisiert, Import-Fixes, Doppelte Einträge behoben
- ✅ **Etappe 8**: Dark Mode mit Theme-Toggle, Separation of Concerns
- ✅ **Etappe 9**: Complete Dark Mode Implementation (alle UI-Komponenten)
- ✅ **Dokumentation**: README, KONZEPT.md, ROADMAP.md, SAVEPOINT.md
### Nächste Schritte
- Job-Verlauf UI (aktuell nur leere Listen)
- TMDB API Schlüssel in Arcane Environment konfigurieren
- Proxmox LXC Template
- Ansible Playbooks
### SoC Refactoring — Abgeschlossen (Etappe 9)
- ✅ **Theme-Context**: Theme-Logik aus App.tsx ausgelagert
- ✅ **Dark Mode Utility**: useDarkMode Hook implementiert
- ✅ **Component-Struktur**: Dark Mode props von inneren Komponenten versteckt
- ✅ **Config Validation**: TMDB-API-Key ist Pflicht
- ✅ **Cache Key Centralization**: cache/keys.py erstellt
- ✅ **CD-Ripping**: abcde-Integration implementiert
### Git-Log
```
d2fb281 - fix: complete dark mode implementation with all UI components
43dfce5 - feat(ui): implement dark mode with theme toggle
rippy-ui-modern - UI Modernisiert & Import-Fixes
3d87c8c - Dokumentation aktualisiert
bdb9f8d - SAVEPOINT v1.5: Sicherheit abgeschlossen
efa642e - Etappe 6: Sicherheit
```
---
## v2.0 — Fix-Runde nach externem Code-Review (22.07.2026, Claude)
**Alle Befunde des Reviews behoben, echte Tests eingeführt, Ampel-CI aktiv.**
Gefixt:
- **Metadaten-Preview WIEDERHERGESTELLT**: Beim SoC-Refactoring hatte ein Stub-Paket
die echte prescan-Implementierung überschattet — die Preview lieferte immer
„Unknown Disc". Echte Logik liegt jetzt im Paket, tote Altmodule (cache.py,
prescan.py flach) gelöscht.
- Fortschrittsmeldung: Celery `update_state` statt Aufrufe eines nie existierenden
Tasks; `self.send_task` (erfundene API) entfernt.
- abcde-Kommando korrigiert (`-o` war doppelt → CD-Ripping war nie funktionsfähig);
Zielverzeichnis jetzt via OUTPUTDIR-Config; Kommando-Bau als testbare Funktion.
- JWT: fester Schlüssel PFLICHT (kein Zufalls-Fallback pro Prozess mehr);
Logout blacklistet wirklich; Cleanup löscht nur Abgelaufene (vorher: alles).
- main.py: crashende Endpoints repariert (fehlende Imports Path/secrets,
nicht existentes api_keys-Dict → ratelimit-Store), Audio-`year`-Bug,
Admin-Zugang aus .env statt hartkodiert.
- Ruff komplett grün (29 Funde), package-lock.json committet.
- Tests: test_auth, test_cache_keys, test_ripping_helpers (der giftige
test_health-Placebo ist raus).
## ✅ ENTSCHIEDEN (22.07. abends): Das KONZEPT gilt — MakeMKV kommt
**MakeMKV lossless (Muss-Feature) wird umgesetzt — als eigene Etappe 10 in der
ROADMAP** (Worker-Container + makemkvcon + Beta-Key, in Zed zu bauen, E2E mit
echter Disc). Bis dahin bleibt HandBrake als SICHTBARER Übergang aktiv — jede*r
weiß: aktuell wird transkodiert, nicht verlustfrei gesichert.
## Deploy-Weg seit 22.07. abends: GRÜN-GATE (vollautomatisch)
Push auf `main` → Ampel prüft → grün → CI befördert auf Branch `stable` →
**Arcane-GitSync zieht `stable` und deployt**. Rot deployt NIE. `./deploy.sh`
ist nur noch der Notfall-Hebel; SSH zur VM bleibt als Fallback erlaubt.