Commit Graph
72 Commits
Author SHA1 Message Date
HitonabiandClaude Opus 5 d3e86d2641 fix(windows): Der Schalter, den es nicht gibt, und vierzehn weitere Funde
Commander: „Kompression fehlgeschlagen bei Spartacus … _t01.mkv: HandBrake
endete mit Code 0" — 16,5 GB fertiger Rohschnitt, und die Kompression war in
derselben Sekunde vorbei, in der sie begann.

Nachgestellt mit genau der Befehlszeile, die Rippy baute:

    unknown option (--audio-codec)
    HandBrake has exited.        $? = 0

Den Schalter `--audio-codec` gibt es bei HandBrakeCLI nicht; er heisst
`-E` / `--aencoder`. Ein unbekannter Schalter ist fuer HandBrake kein Fehler,
der Rueckgabewert ist 0. Der Test dazu forderte den falschen Namen sogar ein.

Daraus wurde ein Rundgang durch den Windows-Pfad. Alles unten ist gemessen,
nichts vermutet (Regel D).

## Die Kompression

1. `--aencoder` statt `--audio-codec`. Am mitgelieferten HandBrakeCLI 1.11.2
   gemessen, mit einem 5-Sekunden-Encode auf der echten Roh-Datei bestaetigt.

2. HandBrakes letzte Zeilen werden aufgehoben (12 gepuffert, 4 in der
   Meldung) und `unknown option (...)` wird als eigener Fall erkannt, VOR
   allen anderen. Vorher wurde jede Zeile weggeworfen, die kein Fortschritt
   war — bei Rueckgabewert 0 blieb damit keine Auskunft uebrig. Die geratene
   Zeile „Meist ist der Zielordner nicht beschreibbar" ist raus; sie war
   falsch und hat die Suche in die falsche Richtung geschickt.

3. Ein `ue`-Umlaut im Pfad toetete die Kompression. HandBrake schreibt zwei
   Kodierungen in denselben Strom (derselbe Pfad einmal UTF-8, einmal CP850).
   In CP850 ist das Byte 0x81, und das ist in cp1252 — was `text=True` auf
   deutschem Windows waehlt — undefiniert:

       UnicodeDecodeError: charmap codec can't decode byte 0x81

   Neu: `rip/handbrake_aufruf.py` mit `HB_LESEN`, benutzt von ripping.py und
   caps.py. Bewusst nicht binaer wie bei makemkvcon: HandBrake trennt
   Fortschrittszeilen mit CR, im Binaermodus waere der Balken weg.

## Die Rohdaten

4. `rohdaten.kandidaten` machte aus dem Arbeitsordner `F:\` ein `F:` und
   verband damit weiter. Das ist unter Windows der aktuelle Ordner auf
   Laufwerk F, nicht dessen Wurzel — 16,5 GB waren unsichtbar, und der
   Wiederholen-Dialog bot nur „Neu rippen" an. Die Falle steht woertlich im
   Kopf von `pfade.verbinden`.

5. Gesucht wurde unter der heutigen Einstellung statt unter der Wahl DIESES
   Rips (`meta["work_dir"]`). Genau dafuer wurde rohdaten.py am 26.07.
   gebaut; repariert wurde damals die Kandidatenliste, nicht der Aufrufer.
   Neu: `_arbeitsverzeichnis_des_jobs`, benutzt an vier Stellen.

6. Zwei Speicher fuer dieselben Ordner: Die Oberflaeche schreibt
   `outputDir`/`workDir` in die Datenbank, `betrieb` liest `storage.*` aus
   der Konfigurationsdatei, und die schreibt niemand. Gemessen: eingestellt
   `E:\Rippy`, angezeigt `C:\Users\...\Videos\Rippy`. Neu:
   `betrieb.mit_einstellungen`.

## Das Laufwerk

7. `device_info` fing den OSError ab und lieferte „unknown" ohne den Grund.
   Nach einem Rip mit Lesefehlern beantwortete das Laufwerk keine
   Medien-Abfragen mehr (Win32-Fehler 1), die Geraete-Auskunft aber schon —
   im UI stand eine volle Laufwerkskarte, kein Rip startbar, und im
   Protokoll das laengst veraltete „Disc erkannt". Neu: `ZUGRIFFS_GRUENDE`,
   ein Feld `grund` im Laufwerks-Eintrag und eine Protokollzeile je Wechsel.
   Eine fehlgeschlagene Disc-Erkennung wird ebenfalls protokolliert.

8. Der Linux-Treiber nannte ein unzugaengliches Laufwerk „empty", waehrend
   Windows richtig „unknown" sagt. Angeglichen, samt Feld-Paritaet.

9. `CreateFileW`, `DeviceIoControl` und `CloseHandle` hatten weder `restype`
   noch `argtypes` — 32-Bit-`c_int` fuer einen 64-Bit-HANDLE, in beide
   Richtungen. Mit `restype` aendert sich der Fehlerwert von -1 auf
   0xFFFFFFFFFFFFFFFF; die Pruefung deckt jetzt beides ab. Am echten
   Laufwerk gegengeprueft, Fehlerpfad eingeschlossen.

10. Der Vor-Scan lief bei JEDER eingelegten Disc ein `makemkvcon info` mit
    120 s Zeitgrenze — 20 bis 120 Sekunden „Disc wird gelesen". Frueher war
    das schnell, weil der Zweig unter Windows nie lief (`shutil.which`,
    repariert am 28.08.). Das Ergebnis landete allein in `toc["tracks"]`,
    das niemand liest: Der Rip-Dialog holt seine Liste ueber
    `/devices/{id}/scan-tracks`, wenn sie gebraucht wird. Entfernt.

## Notbremsen

11. `_frei_bytes` suchte den naechsten vorhandenen Ordner selbst.
    `os.path.dirname("Q:\\")` gibt sich selbst zurueck — ein
    Arbeitsverzeichnis auf einer abgezogenen Platte haette den Job vor dem
    Rip stumm haengen lassen. Benutzt jetzt
    `pfade.naechster_vorhandener`, das den Abbruch seit V2-1 hat.

12. `naechster_vorhandener` haelt Laufwerks- und UNC-Wurzeln jetzt absolut.

13. `aufraeum_skript` baut sein `rmdir /s /q` aus `InstallLocation` in der
    Registry. Waere das eine Laufwerks-Wurzel, loeschte die Deinstallation
    das Laufwerk. Nicht beobachtet, aber nicht wiedergutzumachen — der
    Loeschbefehl bleibt in dem Fall weg.

## Lesefehler

MakeMKV sicherte 1 von 2 Titeln, endete mit 0, und Rippy schrieb „Rip
fertig". Jetzt gibt es eine Warnung, auch wenn der Rip als Erfolg endet, und
die MSG-Nummer steht im Protokoll: MakeMKVs Texte sind uebersetzt, die
Nummern nicht.

## Aus der Gegenprobe am laufenden Rippy

Die erste Fassung dieses Standes war installiert, als der Commander meldete:
„nun oeffnen sich diverse fenster im hintergrund, gehen ganz kurz auf und dann
wieder zu. Das laufwerk hoert auch einfach auf zu lesen." Beides Altlasten,
die erst durch die neue Protokollzeile sichtbar wurden.

14. Prozesserzeugung mitgeschnitten:

        14:54:40  timeout.exe          timeout 4 ls -d C:\Users\...\d7ee6c06-...
        14:54:40  WindowsTerminal.exe

    `rohdaten.pruefen` fragt mit `timeout N ls -d`, ob es ein Verzeichnis
    gibt. Unter Linux ist das richtig (os.path.isdir kann an einem toten
    CIFS-Mount im Kernel haengen, ein Kindprozess laesst sich abbrechen).
    Unter Windows ist es dreifach falsch: timeout.exe gibt es dort, kennt
    aber weder `ls` noch `-d`; sie braucht eine Konsole, und die reisst
    Windows auf; und ihr Rueckgabewert ist nie 0, die Antwort lautete also
    „weg" fuer JEDES Verzeichnis. Rohdaten waren unter Windows
    grundsaetzlich unsichtbar. Neu: `nativ_nachsehen()`. Die zwei
    gleichartigen Aufrufe in mounts.py bekommen dieselbe Absicherung.

15. Der Waechter fragte das Laufwerk alle drei Sekunden ab — auch mitten im
    Rip, also drei CreateFileW plus IOCTLs auf ein Geraet, das makemkvcon
    gerade liest:

        12:49:52  bluray-Rip gestartet
        12:50:09  [watcher] Laufwerk G: beantwortet keine Medien-Abfragen
        12:50:12  MSG 2003 SCSI-Fehler ILLEGAL REQUEST:INVALID FIELD IN CDB
        12:50:12  makemkvcon endete mit Code 11

    `_auto_prescan` haelt sich seit dem 29.08.2026 an die Regel „waehrend
    eines Rips wird nicht gescannt"; die Laufwerksabfrage tat es nicht.
    Jetzt gilt in der Zeit der letzte bekannte Stand.

## Zwei Tests, die gelogen haben

* `assert "--audio-codec" in cmd` schrieb den Fehler fest.
* `lambda: {}` als Doppelgaenger fuer `get_settings(key, bei_fehler_leer)`
  brach, sobald ein Aufrufer einen Parameter benutzte — und zeigte dann auf
  den Code statt auf sich selbst.

977 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 15:11:00 +02:00
HitonabiandClaude Opus 5 f0f719ca12 fix(windows): Zweiter Rip startete waehrend der Kompression ins leere Laufwerk
Der Commander meldete einen Rip-Fehlschlag „bei der Komprimierung", obwohl
der Rip laengst durch war:

    makemkvcon endete mit Code 11 — letzte Meldung:
    Das Öffnen der Disk schlug fehl  — keine MKV-Datei entstanden

Auffaellig war, was FEHLTE: kein „Ursache:". Waere der Geraetepfad schuld
gewesen, stuende dort MSG 2024. Bleibt: kein Datentraeger im Laufwerk.

Der Ablauf dahinter:
  1. Rip fertig → Rippy wirft die Disc aus (Standardeinstellung)
  2. Status wird auf `transcoding` gesetzt — ab hier sagte `has_active_job`
     NEIN, das Laufwerk sei frei; es kennt nur pending/running
  3. Die Disc-Wache sieht beim Auswurf einen Statuswechsel → „eingelegt"
  4. Die Vollautomatik startet einen ZWEITEN Rip — auf ein Laufwerk, dessen
     Schublade gerade herausfaehrt

Der zweite Rip lief in ein leeres Laufwerk. Auf dem Bildschirm sah das aus,
als sei die Kompression gescheitert — sie lief ungestoert weiter.

Drei Aenderungen:

* `store.job_offen` — der weitere Riegel (pending/running/transcoding/
  canceling) fuer die Vollautomatik. `has_active_job` bleibt unveraendert:
  Das fragt „haelt gerade jemand das Laufwerk?", und waehrend der Kompression
  tut das niemand — ein Auswurf von Hand bleibt erlaubt.
* `ripping.disc_fehlt` — vor dem makemkvcon-Start nachsehen, ob ueberhaupt
  eine Disc drin liegt. Statt zwei Minuten Warten und Code 11 gibt es einen
  Satz, den man versteht. Ein FEHLGESCHLAGENER Blick verweigert nichts:
  „ich weiss es nicht" darf nie zu „es geht nicht" werden.
* MSG 5010 in KRITISCHE_CODES — MakeMKVs Sammelmeldung sagt fuer sich nichts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 20:36:59 +02:00
HitonabiandClaude Opus 5 1b84ec2a45 fix(windows): Vollstaendiger Rundgang durch die Docker-Reste
Ampel / ampel (push) Successful in 1m20s
Commander: „Bro, du musst alles was rippy jetzt im code hat für Windows
Bauen! Jeden pfad, alles wo die tools drauf zugreifen. Diese Rippy version
MUSS 100% Windows Kompatibel sein. Prüfe bitte den kompletten Quellcode nach
Docker Resten."

Systematisch gesucht statt Fundstelle fuer Fundstelle: feste POSIX-Pfade,
Linux-Programme, POSIX-eigene Aufrufe, `shutil.which`, `posixpath` auf echten
Pfaden, Container-Texte. Sechs echte Fehler dabei.

## 1. `/dev/{name}` in drei Endpunkten — der schwerste

Das UI ruft `/devices/{id}/eject`, `/scan-tracks` und `/tracks` mit der
Kennung aus der Geraeteliste auf, unter Windows also `G`. Gebaut wurde daraus
`/dev/G` — steht in keiner Laufwerksliste. **Auswerfen und „Disc scannen"
antworteten unter Windows IMMER mit 404**, ohne dass irgendwo stand, warum.

Hin- und Rueckweg gehoeren zusammen: Beide Treiber haben jetzt `kennung()`
und `pfad_zu_kennung()`. Wer die Kennung vergibt, loest sie auch auf.

## 2. `os.path.isdir("/app")` — zum zweiten Mal

Nach `caps.py` (heute frueh) auch in `ablauf.py`: Der eigenstaendige
Windows-Rippy hielt sich fuer einen FREMDEN Worker und haette sich selbst
vorgeworfen, Container-Pfade nicht zu erreichen — auf einer Maschine ohne
Container. Die Entscheidung ist jetzt einspritzbar; vorher hing der Test
daran, ob es einen Ordner `/app` gibt.

## 3. `shutil.which` in `schluessel.py`

Ausgerechnet im Modul, das es NUR unter Windows gibt: Es suchte makemkvcon im
PATH, wo unter Windows nie ein Programm aus „Programme" steht. Die
Schluessel-Automatik fuer 4K-UHD lief damit nie an.

## 4. `posixpath.join` auf echten Pfaden

`rohdaten.py` baute `C:\Roh/datei.mkv` — gemischte Trenner, die im UI falsch
aussehen und jeden Vergleich brechen.

## 5. Container-Pfad in einer Nutzermeldung

„Roh-Datei bleibt in /app/temp erhalten" nennt jetzt den echten Ordner. Wer
die Datei retten will, sucht sonst am falschen Ort.

## 6. Container-Pfade als UI-Vorbelegung

Rip-Dialog und `useBetrieb` starteten mit `/app/media`, bis die Antwort da
war. Leer ist ehrlicher: Es behauptet nichts.

## Und HandBrakes „Code 0"

Code 0 heisst ERFOLG. Rippy meldete trotzdem „fehlgeschlagen", weil die Datei
nicht am erwarteten Ort lag: **HandBrake bestimmt den Container aus dem
PRESET, nicht aus der Endung** — ein MP4-Preset schreibt `.mp4` neben das
verlangte `.mkv`. Jetzt erzwingt `--format` den Container passend zur Endung
(an HandBrake 1.11.2 gegengeprueft), und falls doch etwas daneben liegt, wird
es gefunden statt weggeworfen.

## Der Waechter

`test_keine_container_reste.py` prueft mechanisch, dass im Windows-Weg kein
Container-Pfad ohne Begruendung steht. Die Ausnahmen stehen namentlich mit
Grund da (Linux-Zweige, benannte Rueckfaelle) — und ein zweiter Test wirft
jede Ausnahme raus, die niemand mehr braucht.

Ueber den Tokenizer, nicht ueber „faengt mit Anfuehrungszeichen an": Der
erste Anlauf blieb prompt an seinem eigenen `r\"\"\"`-Docstring haengen.

887 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 16:14:10 +02:00
HitonabiandClaude Opus 5 1a529f4e75 fix(windows): „Disc wird gelesen" blieb waehrend des ganzen Rips stehen
Ampel / ampel (push) Successful in 1m13s
Commander: „der ‚Disk wird gelesen' panel braucht eeeeewig. Der Rip ist
bereits bei 30% und er liest immernoch."

Genau so war es — und die Erkennung kam auch nie zum Ende.

## Warum

`_auto_prescan` hatte GENAU EINE Absicherung: nicht zweimal gleichzeitig
(`_laeuft`). Ob auf dem Laufwerk gerade ein Rip laeuft, hat es nie gefragt.

Waehrend eines Rips haelt `makemkvcon` das Laufwerk. Ein zweites
`makemkvcon info` daneben wartet, bis es seine Zeitgrenze erreicht (gemessen:
eine Laufwerks-Abfrage braucht dann 14 s statt 5, der `info`-Aufruf laeuft in
seine 120 s). Solange steht die Marke `_laeuft` — und damit das Panel, das
ich heute frueh genau dafuer gebaut habe.

Es gibt keinen Grund, waehrend eines Rips zu scannen: Das Laufwerk ist
belegt, und **welche Disc drin ist, wissen wir bereits** — der Job laeuft ja
auf ihr.

## Zwei Stellen, weil es zwei Wege hinein gibt

1. `_auto_prescan` bricht ab, wenn auf dem Geraet ein Job laeuft. Das
   verhindert jeden Scan, der NACH dem Rip-Start angestossen wird.
2. Der Job-Start raeumt eine haengende Marke weg. Ein Scan, der KURZ VORHER
   begann, haelt sie sonst bis zu seinem Ende fest. Verloren geht dabei
   nichts: Unter `_laeuft` steht nur der Platzhalter, und der laufende Scan
   traegt sein Ergebnis spaeter selbst nach.

880 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 15:54:48 +02:00
HitonabiandClaude Opus 5 548742e771 fix(windows): Restzeit blieb ewig „wird gemessen" — zwei Docker-Annahmen
Ampel / ampel (push) Successful in 1m32s
Commander: „Über den kompletten vorgang steht dort ‚Restzeit wird gemessen'
aber messung wird nicht abgeschlossen. Das heißt man hat kein ETA"

Zwei Ursachen, beide unabhaengig, beide toedlich fuer sich allein.

## 1. Die Messreihe lag nur in Redis

`eta.py` schrieb sie ausschliesslich in den Cache — mit der Begruendung im
Modul-Kopf: „Der Cache (Redis) ist schon da". Im Container stimmt das. Auf
einem Windows-PC gibt es kein Redis: `cache_get` gab bei JEDEM Aufruf None
zurueck, `beobachtung_hinzufuegen` legte also jedes Mal eine frische Reihe mit
EINEM Punkt an — und `restzeit_sekunden` braucht `MINDEST_PUNKTE = 2`.

Jetzt wird in beide Ablagen geschrieben: in den Cache, wo es einen gibt (er
ueberlebt einen API-Neustart), und in ein Woerterbuch im Prozess. Das ist ein
paar Zahlen gross, gilt nur fuer die Dauer eines Jobs, und im eigenstaendigen
Betrieb gibt es ohnehin nur diesen einen Prozess. Alte Reihen werden nach
einem Tag weggeraeumt.

## 2. Der Ereignisstrom rechnete die Restzeit gar nicht

Die Rechnung stand nur in `/jobs`. Der SSE-Schnappschuss baute seine Jobs mit
dem nackten `_job_row_to_model` — also ohne Restzeit. **Seit der Umstellung
auf den Ereignisstrom (V2-3) liest die Oberflaeche aber genau diesen
Schnappschuss und nicht mehr `/jobs`.** Die Restzeit wurde also brav berechnet
und niemandem gezeigt.

Beides jetzt in `jobs_fuer_ui()` — dieselbe Lehre wie bei
`laufwerke_mit_disc` heute frueh: Eine Auskunft in zwei Fassungen ist eine
Fassung zu viel.

Nebenbei: Unlesbare Einstellungen duerfen die Jobliste nicht umwerfen. Seit
sie auch den Schnappschuss baut, haengt daran die ganze Oberflaeche — zwei
Snapshot-Tests wurden davon prompt rot.

## Beweis

Job in der Datenbank, Fortschritt 10 % -> 25 % ueber 130 s:

    nach 1. Messpunkt : eta_text=''            (richtig, eine Messung reicht nicht)
    nach 2. Messpunkt : 674 s, „noch ca. 11 min"

877 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 15:49:59 +02:00
HitonabiandClaude Opus 5 aeb3128a85 fix(windows): Dauerscan der Disc, aufblitzende Fenster, Docker-Einstellung uebernommen
Ampel / ampel (push) Successful in 1m30s
Commander: „liest er die disc NOCHMAL ein das macht aber keinen sinn, wenn er
sie bereits erkannt hat. Außerdem öffnen sich nun immer irgendwelche fenster
ganz kurz im hintergrund."

## Beides war dieselbe Schleife

In der Disc-Wache stand:

    except OSError:
        continue

Damit blieb `bekannt[pfad]` ungesetzt, und im naechsten Durchlauf war
`vorher is None` — also wieder „Erststart, liegt schon eine Disc drin". **Ein
einziger fehlgeschlagener Lesevorgang loeste einen neuen Vor-Scan aus.**

Und der scheitert regelmaessig: Waehrend der Vor-Scan laeuft, haelt
makemkvcon das Laufwerk (gemessen: /devices braucht dann 14 s statt 5). Also
Scan haelt das Laufwerk -> Statusabfrage scheitert -> naechster Durchlauf
haelt es fuer den ersten -> neuer Scan. Eine Schleife, die sich selbst am
Leben haelt.

Und weil derselbe makemkvcon-Aufruf in `prescan.py` als EINZIGER kein
`creationflags=OHNE_FENSTER` hatte, blitzte bei jeder Runde eine Konsole auf.
Das war meine Zeile von heute Vormittag.

Die Entscheidung steckt jetzt in `disc_entscheidung()` — pure Funktion, also
ohne Laufwerk pruefbar. Zwei getrennte Fragen statt einer: Haben wir dieses
Laufwerk je gelesen, und was war zuletzt drin? Ein Fehlschlag beantwortet die
zweite nicht und darf die erste nicht zuruecksetzen.

Nachgemessen: Ein Tabwechsel loest KEINEN neuen Scan aus (8 Erkennungen
vorher, 8 nachher). Meine erste Zaehlung von „7 vs 8" war ein Messfehler —
zwei verschieden grosse Abfragefenster.

## Aus dem Docker-Betrieb uebernommen

`docker/worker/entrypoint.sh` setzt vor jedem Worker-Start zwei Dinge:
`app_Key` und `app_UpdateEnable`. Der Key war schon uebernommen, die zweite
nicht — auf dem Rechner des Commanders nachgesehen: NICHT gesetzt.

Das ist MakeMKVs Web-Kontakt; darueber holt es die Disc-Schluessel fuer
4K-UHD nach (Meldung 3338, auf Windows gemessen). „Fuer Windows umschreiben"
heisst hier: Registry statt settings.conf, DWORD statt Text — die
Nachbarwerte (`app_BackupDecrypted`) zeigen die Form.

Gesetzt wird NUR, was fehlt: Wer den Web-Kontakt bewusst abgeschaltet hat,
behaelt ihn abgeschaltet. Ein Waechter-Test vergleicht kuenftig die
`app_*`-Namen aus der entrypoint.sh gegen die Windows-Seite — sonst driften
die beiden Betriebe auseinander.

871 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 15:30:29 +02:00
HitonabiandClaude Opus 5 e67138e9ca feat(ui): Laufende Disc-Erkennung ist jetzt sichtbar
Ampel / ampel (push) Successful in 1m26s
Commander: „das die disc erkennung noch läuft muss sichtbar sein"

Er hat recht, und die Wartezeit ist gemessen: `makemkvcon info` laeuft an
einer Blu-ray in seine 120-Sekunden-Grenze (Disc nach 119 s erkannt). Zwei
Minuten, in denen NICHTS zu sehen war — weder die Disc noch ein Hinweis. Das
sah aus wie ein leeres Laufwerk, also wie ein Fehler.

## Der Zustand war da, er wurde nur weggeworfen

`_auto_prescan` legt beim Start `{"_laeuft": True}` in den Vorrat.
`laufwerke_mit_disc` liess so einen Eintrag KOMPLETT weg — damit keine
halbfertige Disc-Karte mit „Wird erkannt…" als Titel und einem „Rippen
starten"-Knopf erscheint. Richtig gedacht, falsch geloest.

Jetzt gibt es ein eigenes Feld `disc_wird_erkannt`. Die Oberflaeche kann
„wird gelesen" zeigen, ohne einen Titel zu behaupten, den es noch nicht gibt:
eine Karte mit Spinner ueber der Laufwerksliste und eine eigene Zeile im
Server-Status.

## Und der Haken dahinter

Genau waehrend der Erkennung ist die Laufwerks-Auskunft am teuersten:
makemkvcon haelt das Laufwerk, und `device_info` wartet mit. Gemessen:

    /devices waehrend des Scans     14,0 s
    Zeitgrenze des Schnappschusses   5,0 s   ->  devices = None

`None` heisst „konnte nicht nachsehen", und die Oberflaeche behaelt dann
ihren Stand — nach einem frischen Laden also GAR NICHTS. Die Anzeige waere
ausgerechnet in ihrer eigenen Phase leer geblieben.

`laufwerke_notdurft()` antwortet deshalb ohne ioctl: letzter bekannter Stand
plus die aktuelle Erkennungs-Marke aus dem Vorrat. Beim Kaltstart wenigstens
der Laufwerksname — die LISTE der Laufwerke ist billig, teuer ist erst das
Anfassen. Nichts wird erfunden: kein Typ, kein Modell. Und wer noch nie
erfolgreich gelesen hat, schweigt weiter mit `None`.

Im Browser gegengeprueft: „Disc wird gelesen — Laufwerk G:" mit Spinner,
Server-Status „Disc wird gelesen — das dauert bis zu zwei Minuten", danach
der richtige Titel.

859 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 15:12:35 +02:00
HitonabiandClaude Opus 5 7b0c41ddfe fix(ui): Datum 1.1.1970, Disc nicht erkannt, kein Abbrechen — eine Ursache je Fall
Ampel / ampel (push) Successful in 1m16s
Vier Befunde aus einem Bildschirmfoto vom 29.08.2026.

## 1. „das Datum ist falsch" — und der Typ auch

    TYP „DISC"   STARTZEIT „1.1.1970, 01:00:00"   STATUS „running"

⚠️ Das war MEIN Fehler von heute Vormittag. Ich hatte `type`, `device` und
`startTime` in das Job-Ereignis aufgenommen — mit Feldnamen, **die es in der
Datenbankzeile nicht gibt**. Dort heissen sie `disc_type` und `created_at`;
die Uebersetzung macht `_job_row_to_model`, und sie bildet auch `running` auf
`processing` ab.

Die Folge war schlimmer als das Problem davor: Statt eines FEHLENDEN Feldes
kam ein LEERES. `new Date(null)` ist der 1.1.1970, und „DISC" war der
Rueckfall, den ich zwei Stunden vorher fuer genau diesen Fall eingebaut hatte.
Aus „offensichtlich kaputt" war „sieht plausibel aus" geworden.

Und mein Test hat es nicht gefunden, weil ich seine Beispielzeile selbst
erfunden habe — mit meinen falschen Feldnamen. Ein Test, der dieselbe Annahme
macht wie der Code, prueft nichts. Der neue geht von der ECHTEN Zeile aus.

Der Waechter bekommt jetzt dieselbe Uebersetzung eingespritzt, die auch
`/jobs` benutzt.

## 2. „Es ist eine Disk im laufwerk, aber er erkennt sie dort nicht"

Die Laufwerksliste wurde an DREI Stellen gebaut: `/devices` haengte die
erkannte Disc an, der Ereignis-Waechter und der SSE-Schnappschuss nicht. Das
Dashboard liest den Schnappschuss — und schloss aus der fehlenden Disc auf ein
leeres Laufwerk, waehrend ihr Titel eine Zeile weiter oben stand.

Jetzt gibt es `laufwerke_mit_disc()`, und ein Test zaehlt die Aufrufe von
`device_info` — bei zwei ist er rot.

## 3. „man kann einen rip garnicht abbrechen"

Dieselbe Ursache wie 1: Es gab genau EINEN Abbrechen-Knopf, in der Kachel
„laufender Job". Die erscheint nur bei Status `processing`; der Strom lieferte
`running`. Keine Kachel, kein Knopf, kein Weg zurueck — und aus demselben
Grund stand „Aktiv (0)", waehrend der Rip lief.

Zusaetzlich gibt es den Knopf jetzt in der Job-Zeile selbst, wo man ihn sucht.

## 4. „bei ‚Platz für rippy' sollte eher das arbeitsverzeichnis und der
##     Ablagepfad sein"

Dort stand eine nackte Zahl fuer den ERSTEN Ort. Welcher Ordner das war, sah
man nicht — und die zweite Zeile fiel ganz weg, wenn beide auf derselben
Platte lagen. Das stimmt fuer die ZAHL und war der falsche Schluss fuer den
ORT. Jetzt stehen beide da, mit Pfad, plus der Hinweis, dass der Platz
geteilt wird.

Im Browser gegengeprueft: „Disc erkannt — wartet auf ‚Rippen starten'",
BLU-RAY mit richtigem Datum, beide Pfade, keine Konsolenfehler.

852 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 15:00:11 +02:00
HitonabiandClaude Opus 5 b2acddbdfa fix(windows): Drueberinstallieren, Linux-Reste im Windows-Betrieb, Ordner-Waehler
Ampel / ampel (push) Successful in 1m27s
Vier Fragen des Commanders vom 29.08.2026, drei davon mit Codefolge.

## 1. „Was passiert wenn man die Setup.exe einfach drueber installiert?"

Bis hierher: nicht zuverlaessig. Rippy startet mit Windows, laeuft also fast
immer — dann ist `Rippy.exe` gesperrt, `shutil.copy2` warf PermissionError,
und die neue Fassung landete als `Rippy.exe.neu` daneben. Dazu die Meldung
„wird beim naechsten Start uebernommen".

**Diese Zusage hat niemand eingeloest.** `.neu` kam im ganzen Projekt genau
einmal vor: an der Stelle, die es schrieb. Wer drueberinstallierte, behielt
still die alte Fassung, und das Setup meldete Erfolg.

Jetzt wird der laufende Rippy vorher beendet (`dienst_beenden` gibt es seit
der Deinstallation und wartet auch die zwei Sekunden ab, die Windows fuer die
Dateihandles braucht). Eine von einer aelteren Setup-Fassung liegengelassene
`.neu` wird dabei uebernommen. Bleibt die Datei DANN noch gesperrt, gibt es
einen klaren Fehler statt einer Zusage — Rippy im Infobereich beenden und das
Setup erneut starten.

Der Tausch laeuft bewusst im SETUP und nicht beim Dienststart: Windows sperrt
eine laufende .exe, und `Rippy.exe` waere genau die zu ersetzende Datei.

## 3. Linux-Reste im Windows-Betrieb (Docker/Headless unveraendert)

**`caps.py`: `os.path.isdir("/app")`.** Damit hielt sich der eigenstaendige
Windows-Rippy fuer einen FREMDEN Worker — und das UI warnte vor fehlender
Pfad-Uebersetzung auf einer Maschine ohne Container und ohne Freigabe.
„Extern" heisst jetzt, was es meint: Rippy laeuft woanders als dieser Worker.

**`caps.py`: `shutil.which("makemkvcon")` + `os.path.ismount(daten_dir)`.**
Beide unter Windows immer falsch (Programme liegen nicht im PATH, ein
normaler Ordner ist kein Mount). Die Schluessel-Auskunft blieb dauerhaft
„unbekannt", obwohl MakeMKV samt Datenverzeichnis da war. Der Mount-Test
bleibt fuer den Container, wo er einen Zweck hat.

**`rohdaten.py`: `/app/temp/raw` und `/app/media` fest.** Dieses Modul findet
die Rohdaten eines Jobs wieder — fuer den Wiederholen-Dialog und fuer
„Rohdaten mitloeschen". Unter Windows fand es NIE etwas: Der Dialog meldete
„keine Rohdaten", das Aufraeumen loeschte nichts, und die Bruchstuecke eines
abgebrochenen Rips blieben liegen (bei 4K-UHD bis 100 GB).

Sieben Tests wurden dabei rot, und zwar zu Recht: Sie pruefen Container-Regeln,
liefen aber unter Windows. Die Wurzeln sind jetzt einspritzbar — beide
Betriebsfaelle auf jedem Rechner pruefbar statt vom laufenden abhaengig.

## 4. „Der Durchsuchen button fehlt. Wie es der Installer auch macht"

Neu: `OrdnerWaehler` — Pfadfeld plus „Durchsuchen …", benutzt fuer Ablage und
Arbeitsverzeichnis. Es waere der DRITTE fest eingebaute Ordner-Browser
geworden (RipTargetModal, StorageMounts); dieser hier ist wiederverwendbar.
`/browse` weiss seit dem 28.08. selbst, in welchem Betrieb es laeuft.

⚠️ Beim Einbau fiel der Import unter den Tisch. `vite` pruefte das NICHT — das
Buendel blieb byte-gleich gross, und zur Laufzeit waere es der naechste leere
Bildschirm gewesen. Aufgefallen nur, weil die erwartete Anzahl Ersetzungen
nicht stimmte. Im Browser gegengeprueft: alle sieben Laufwerke, Navigation in
D:\, keine Konsolenfehler.

843 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 14:28:01 +02:00
HitonabiandClaude Opus 5 954f293871 fix(windows): Rippen ging nicht — drei Container-Annahmen im Rip-Pfad
Commander: „Das Rippen der disk funktioniert nicht. makemkvcon endete mit
Code 11 — letzte Meldung: Das Öffnen der Disk schlug fehl — keine MKV-Datei
entstanden"

In diesem einen Satz steckten drei Fehler. Alle drei an seinem Laufwerk
gemessen, nicht hergeleitet.

## 1. Die Quellenangabe — das war der Abbruch

    dev:\.\G:  MSG:2024 „Unknown device" -> MSG:5010 Öffnen schlug fehl
    dev:G:      68 TINFO-Zeilen, „Die Aufgabe wurde erfolgreich abgeschlossen"

`f"dev:{device_path}"` stand an VIER Stellen. Unter Linux richtig (/dev/sr0),
unter Windows nicht: `rippy.drives.windows` meldet den Geraete-Namensraum
`\.\G:`, den `CreateFileW` fuer die IOCTLs braucht — MakeMKV kennt ihn
nicht.

## 2. „Ö" statt „Ö"

`Ö` als UTF-8 (C3 96), gelesen als cp1252. `subprocess` stand auf `text=True`
ohne `encoding=`, nahm also die Gebietsschema-Kodierung; Rippy stellt seine
Konsole beim Start aber auf UTF-8 (sonst stirbt der Start an einer
Umlaut-Logzeile). Direkt gemessen kommen die Bytes je nach Umgebung als UTF-8
ODER cp1252 — deshalb wird jetzt bestimmt statt behauptet.

## 3. Die Ursache fehlte im Fehlertext

Gesucht wurde nach ENGLISCHEN Textbausteinen („evaluation period has
expired"). Auf einem deutschen Windows trifft keiner. Jetzt zaehlen die
Meldungs-NUMMERN, die sprachunabhaengig sind.

⚠️ Beim ersten Anlauf hatte ich 5021 aus dem Kopf mit „Volume-Key unbekannt"
beschriftet — falsch. Meine eigene Ausgabe zeigte nur meine Beschriftung
statt MakeMKVs Text, sodass es fast durchgegangen waere. Der echte Wortlaut
steht jetzt im Code.

## Nebenbefund: der Beta-Key lag zweimal am falschen Ort

`makemkv_daten.DATEN_DIR` war fest `/root/.MakeMKV`. Nach der Reparatur des
Ordners blieb MSG:5051 trotzdem stehen — weil MakeMKV unter Windows seine
Einstellungen in der REGISTRY haelt (HKCU\Software\MakeMKV), nicht in einer
settings.conf. Nachgesehen statt vermutet.

Und der wichtigste Teil davon: **Ein abgelehnter Schluessel ist schlimmer als
gar keiner.** Ohne Key las MakeMKV die Disc noch (68 TINFO), mit dem
abgelehnten verweigerte es alles (0 Titel). Deshalb wird jetzt geprueft und
bei Ablehnung zurueckgenommen.

## Beweis

Echter Rip ueber `ripping.run_makemkv`, kuerzester Titel:

    Status success, Code 0, „Evangelion 2.22_t03.mkv" 217,9 MB
    5036 „Das Kopieren wurde abgeschlossen. 1 Titel wurden gesichert."

828 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 13:52:06 +02:00
HitonabiandClaude Opus 5 678fe276f9 feat(windows): Arbeitsordner im Setup, Beta-Key auf Knopfdruck
Ampel / ampel (push) Successful in 1m18s
Zwei Wuensche des Commanders vom 28.08.2026.

## 1. „kannst du noch einbauen das man den arbeitsordner usw. in den
##     Einstellungen setzen kann und direkt im setup?"

In den Einstellungen ging es seit 35370fd (freies Pfadfeld plus die Laufwerke
als Ein-Klick-Wahl). Im SETUP wurde bisher nur nach Programmordner und Ablage
gefragt — der Arbeitsordner tauchte erst auf, wenn schon installiert war.

Das ist die falsche Reihenfolge: Der Roh-Rip einer 4K-UHD ist bis zu 100 GB
gross. Wer erst NACH der ersten vollen Platte erfaehrt, wo die liegen, hat es
zu spaet erfahren — genau so lief am 25.07.2026 die VM-Platte voll.

Neu ist auch ein Waechter, der alle drei Arten von halb angeschlossenem Feld
faengt (kein Eingabefeld, keine Vorbelegung, wird beim Installieren nicht
mitgeschickt). Der dritte Fall ist der gemeinste: Man waehlt etwas, es
passiert nichts, und niemand sagt warum.

## 2. „der könnte theoretisch auch automatisch ausgelesen werden, ich glaube
##     das web rippy kann das"

Er hatte recht. `makemkv_key.refresh_loop()` laeuft seit jeher taeglich mit
(main.py, Startereignis). GEMESSEN am 28.08.2026: Forum antwortet in 3,3 s,
gueltiger Key mit 62 Zeichen, und in seinen Einstellungen lag bereits einer.

Es war nur nichts davon zu sehen. Kein Knopf, keine Meldung, keine Zeitangabe
— im Feld stand ein Key, und ob der von Hand kam oder von selbst, wusste
niemand. Eine Automatik, die man nicht sehen kann, ist fuer den Benutzer
keine.

Dazu kam eine echte Luecke: Die Schleife startet 60 Sekunden nach dem Server.
Wer gleich nach dem Einrichten eine Blu-ray einlegt, rippt ohne Key — MakeMKV
faellt in den 30-Tage-Testmodus. Das Setup holt ihn jetzt selbst.

Der geholte Key wandert NICHT ueber die Leitung zurueck: Er steht in den
Einstellungen, und ein zweiter Weg zu demselben Wert ist ein zweiter Weg, ihn
zu verlieren.

Rechtlich unveraendert: oeffentliche Beta-LIZENZ der Software, KEIN
Disc-Schluessel. Rippy liefert und verteilt keine Disc-Schluessel.

803 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 11:53:12 +02:00
HitonabiandClaude Opus 5 35370fd552 fix(windows): Cover, Arbeitsverzeichnis und die zurueckgelassenen _MEI-Ordner
Drei Befunde des Commanders vom 28.08.2026, alle drei gemessen.

## 1. „Was ist mit dem Cover auf Windows Rippy?"

Es gab keins, weil es keinen Treffer gab. `_scan_video` verlangte woertliche
Gleichheit:

    if movie.get("title", "").lower() == kandidat.lower():

An seiner Disc gemessen:

    'Evangelion 2.22'    1 Treffer   Evangelion: 2.0 You Can (Not) Advance
    'Evangelion'        20 Treffer   irgendein Evangelion

Der EINZIGE Treffer auf den vollen Disc-Titel war der richtige Film, mit
Poster — und wurde verworfen. Jetzt zaehlt die SPEZIFITAET der Anfrage: Wer
auf den vollen Disc-Titel hoechstens drei Treffer bekommt, hat gefragt wie
jemand, der weiss was er sucht. Ergebnis an derselben Disc:

    Evangelion: 2.0 You Can (Not) Advance / 2009 / 80 % / Poster + dt. Text

## 2. „Warum heisst das hier noch container platte? … Waere es moeglich das
##     Arbeitsverzeichnis zu aendern? momentan geht das nicht."

Beide Haelften gehen auf EINE Zeile zurueck: `MEDIA_ROOT = "/app/media"` war
zugleich Vorgabe UND Pfadgrenze. Auf Windows gibt es den Ordner nicht:

* `/storage-targets` fing den OSError und gab still [] zurueck — die Auswahl
  hatte genau einen Eintrag. Das ist „momentan geht das nicht".
* Dessen Text war fest verdrahtet „(Container-Platte)".
* `/browse` antwortete auf jeden Pfad mit 422.

Die Grenze faellt nicht weg, sie wird betriebsabhaengig: Container und
Kopflos-Betrieb bedienen ein Netz, die native App den Menschen davor.
Gemessen auf seinem PC: 7 Laufwerke zur Auswahl, X/Y/Z mit je 2,2 TB frei.

## 3. „Failed to remove temporary directory: …_MEI0000b0882"

GEMESSEN: 20 zurueckgelassene _MEI-Ordner mit 1,1 GB. Ursache: `Popen` ohne
`env=` reicht PyInstallers Auspack-Zeiger an die Kinder weiter (--dienst und
--oeffnen). Beide laufen dann im Ordner des Elternprozesses, der sich zuerst
beendet und ihn loeschen will. Waere das TEILWEISE geglueckt, haetten Dienst
und Fenster mitten im Betrieb ihre Dateien verloren.

Nebenbefund: test_setup_fenster scheiterte in jeder Umgebung ohne pywebview.

794 Tests gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 16:43:41 +02:00
HitonabiandClaude Opus 5 66642b8d93 feat(ui): die Oberflaeche weiss jetzt, worauf sie laeuft
Commander-Befund 28.08.2026, zum Windows-Fenster:

    Worker erreichbar:   0 von 1
    Kein Worker antwortet — Pruefen: docker compose ps
    Container-Platte:    unbekannt
    Freigaben:           keine eingehaengt

Kein Satz davon ergibt auf einem Windows-PC einen Sinn. Sein Urteil: "Du hast
ja quasi nur rippy genommen und die docker installation fuer Windows gebaut.
Das gilt fuer die ganze standalone version fuer Windows, auch fuer die
settings und die Anleitung usw."

## Die Ursache war nicht die Anzeige

Die naheliegende Reparatur waere ein `if (windows)` an dreissig Stellen
gewesen -- dieselbe Falle noch einmal, nur mit einer zweiten Sorte Vermutung.

Es fehlte etwas anderes: Das UI hat nie erfahren, worauf es laeuft. config.py
kennt das Profil seit V2-2, weitergegeben wurde es nie. Also hat das UI
angenommen.

Neu: GET /betrieb meldet FAEHIGKEITEN, keinen Modus-Namen.

    externe_worker        Gibt es andere Maschinen, die Jobs uebernehmen?
    freigaben_einhaengen  Kann Rippy Netzwerk-Freigaben selbst einhaengen?
    container_pfade       Sind Pfade wie /app/media ueberhaupt gemeint?
    werkzeuge_verwalten   Kann Rippy MakeMKV/HandBrake selbst beschaffen?

Ein Modus-Name wuerde das UI zwingen, aus einem Namen auf Verhalten zu
schliessen -- und das bricht beim naechsten Betriebsfall: Ein
Docker-All-in-One hat Container-Pfade, aber keinen zweiten Worker.

## Was sich sichtbar aendert (auf Windows nachgemessen)

    Server-Status  "Rippy arbeitet: auf diesem Rechner" statt Worker-Zaehler
                   "Platz fuer Rippy: 59.2 von 232 GB frei" statt "unbekannt"
                   Freigaben-Block faellt weg
    Einstellungen  kein Reiter "Worker"; Speicherziele BLEIBT (dort steht die
                   Ablage -- "wo will ich das hinspeichern" war die Frage),
                   aber mit Pfadfeld statt Container-Auswahl und ohne die
                   Maske zum Einhaengen
    Anleitung      kein docker, kein Encoding-Worker-Abschnitt, stattdessen
                   der echte Ordner (C:\Users\...\Videos\Rippy)

## Zwei Fehler, die dabei aufgefallen sind

* MEDIA_ROOT = "/app/media" war in main.py fest verdrahtet. shutil.disk_usage
  warf unter Windows, die Liste blieb leer -- daher "unbekannt", obwohl auf
  dem Laufwerk 59 GB frei waren. Eine Nichtauskunft, die wie eine Auskunft
  aussieht. platz_orte() liefert die Orte jetzt je Betrieb, und ein noch
  nicht angelegter Ordner faellt auf das naechste vorhandene Elternteil
  zurueck.
* Der SSE-Schnappschuss enthielt den Server-Zustand NICHT, und der Waechter
  schickt ihn nur alle 15 Sekunden. Nach jedem Neuladen stand deshalb bis zu
  eine Viertelminute "unbekannt" da. Jetzt ist er im Schnappschuss, und das
  UI uebernimmt ihn auch von dort.

Dazu: der Windows-Skip in test_api_smoke.py ist weg. Er stammte aus der Zeit
vor V2-4, als main.py fcntl brauchte; seit der Treiberwahl ueber den Port
laedt es auf beiden Plattformen (57 Routen, gemessen). Damit laufen 20 Tests
mehr auch lokal statt nur auf der Ampel.

Ampel lokal: 654 gruen, ruff sauber. Docker-Zweig durch Unit-Tests gedeckt,
am echten Container noch nicht gegengeprueft -- das kommt beim Deploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 13:45:00 +02:00
HitonabiandClaude Opus 5 f4a8d77598 feat(windows): Rippy arbeitet eigenstaendig — Rippen, Werkzeuge, Fähigkeiten
Ampel / ampel (push) Failing after 48s
WAS: Der Ablauf ist aus tasks.py heraus (ablauf.py, ohne Celery), die
LocalQueue wird bedient, ein Laeufer arbeitet Auftraege im selben Prozess
ab, und Rippy meldet sich mit gemessenen Faehigkeiten selbst als Arbeiter.

WARUM: "Rippy fuer Windows soll standalone funktionieren" (Commander). Bis
hierher konnte die Windows-App alles ANZEIGEN und nichts TUN — ein Rip waere
eingereiht worden und fuer immer liegengeblieben, weil niemand ihn holt.

DIE TRENNUNG: rip_disc hing an GENAU DREI Celery-Stellen in 234 Zeilen —
self.update_state, _transcode_queue, transcode_files.apply_async. Alle drei
sind Fragen der ZUSTELLUNG, nicht des Ablaufs. Sie sind jetzt Rueckrufe:
tasks.py reicht die Celery-Fassung herein, standalone.py die lokale. OHNE
Rueckruf komprimiert derselbe Prozess weiter — genau das, was ein
Ein-Prozess-Rippy braucht. Der Ablauf selbst ist Zeile fuer Zeile derselbe;
der Docker-Betrieb merkt vom Umbau nichts (Task-Namen, Argumente, Queues
unveraendert).

EINE ZUSTELL-STELLE statt drei: celery_client.abschicken() bedient alle
Auftragsarten. Vorher rief jede Stelle send_task selbst auf — der
Standalone-Betrieb haette an drei Stellen umgebogen werden muessen, beim
naechsten Auftragstyp an einer vierten.

WEITERER BLOCKER GEFUNDEN: ablauf.py holte detect_disc_type fest aus dem
LINUX-Treiber, in einem try/except. Unter Windows waere es damit IMMER None
gewesen und Rippen "hart verriegelt" — Rippy haette alles angezeigt und
nichts gerippt, ohne dass irgendwo ein Fehler stuende. Jetzt fragt es den
Treiber-Port.

WERKZEUGE: ripping.py und caps.py suchten nur im PATH. Auf dem Commander-PC
gemessen, vorher/nachher:

  vorher   check_makemkv_installed()  -> False (obwohl installiert)
           erkenne_encoder()          -> nur CPU
  nachher  MakeMKV   1.18.4  C:\Program Files (x86)\MakeMKV\makemkvcon64.exe
           HandBrake 1.11.2  ueber die API geholt, in 2,7 s
           Encoder   cpu-x264, cpu-x265, cpu-av1, VCE, VCE-AV1
           107 Presets, Ryzen 7 9700X, 16 Kerne, avx512f

VCE ist die Hardwarebeschleunigung der Radeon — die hat Rippy auf diesem
Rechner vorher nie gesehen, weil es HandBrake gar nicht fand.

NEUE ROUTEN: GET /system/werkzeuge (was liegt wo, in welcher Fassung, gibt
es Neueres) und POST /system/werkzeuge/{name}/holen. HandBrake kommt
vollautomatisch von GitHub. MakeMKV wird NICHT mitgeliefert — Rippy laedt
die offizielle Datei und startet sie (Black-Box-Trennung, KONZEPT.md § 6).
makemkv.com antwortete beim Bauen mit HTTP 525; das wird im Klartext
gemeldet, und eine selbst geholte Datei bleibt moeglich.

HERZSCHLAG: /capabilities las die workers-Tabelle, die bisher nur der
Celery-Herzschlag fuellte. Im Standalone-Betrieb stand dort "0 Worker" und
die Encoder-Auswahl im UI blieb LEER — auf einem Rechner, der alles kann.
Jetzt meldet sich der Prozess selbst, mit dem, was caps.py MISST.

GEMESSEN, aus der fertigen EXE (29,0 MB):
  bereit nach 1 s, keine Fehler im Log
  Werkzeuge: beide gefunden, mit Version und Pfad
  Worker: 1 (TobisNicerPC), 5 Encoder, 107 Presets

Die ganze Kette ist als Test festgehalten (test_kette.py): zustellen ->
einreihen -> Laeufer -> ablauf -> Job endet in einem EHRLICHEN Zustand.
Ohne Laufwerk geprueft, und das ist der wichtigere Fall: Ein Rip auf ein
totes Geraet muss zuegig scheitern, nicht auf "pending" haengenbleiben.

GEMESSEN: ruff sauber, 512 Tests gruen + 15 uebersprungen (vorher 489).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 11:27:54 +02:00
HitonabiandClaude Opus 5 059183c651 feat(windows): V2-4 fertig — RippySetup.exe, Taskmanager-Eintrag, Deinstallation
Ampel / ampel (push) Failing after 46s
WAS: Eine 28,6-MB-Datei, drei Betriebsarten. Doppelklick installiert,
`--dienst` laeuft im Hintergrund, `--deinstallieren` raeumt sich weg.
Dazu packaging/windows/build.py, das sie baut.

DIE ZWEI COMMANDER-WUENSCHE, GEMESSEN:

1. EINTRAG IM TASKMANAGER
     ProcessName   Rippy
     Beschreibung  Rippy — automatisches Ripping
     Produkt       Rippy
     Version       2.0.0
   Der Prozessname kommt vom Dateinamen (bei der Installation kopiert sich
   die Datei als Rippy.exe), die Beschreibung aus einer eingebetteten
   Versions-Ressource. Ohne die steht dort nichts, und Windows SmartScreen
   sieht eine EXE ohne Herausgeberangabe genauso misstrauisch wie ein Mensch.

2. EINTRAG IN PROGRAMME UND FEATURES
   Aus der Registry zurueckgelesen, nicht behauptet:
     DisplayName      Rippy
     DisplayVersion   2.0.0
     Publisher        Rippy
     InstallLocation  <Zielordner>
     UninstallString  "<pfad>\Rippy.exe" --deinstallieren
     EstimatedSize    29241  (KiB)
     NoModify/NoRepair 1
   HKCU statt HKLM: Der Eintrag erscheint genauso in "Apps & Features", die
   Installation laeuft aber ohne UAC-Abfrage durch.

VOLLER KREISLAUF GEMESSEN: installieren -> Rippy.exe + rippy.ico liegen da,
Registry-Eintrag da, Autostart da; Dienst starten -> nach 1 s bereit, alle
sieben Endpunkte HTTP 200, Oberflaeche kommt; deinstallieren -> nach ~4 s
sind Datei, Ordner, Registry-Eintrag, Autostart UND das Aufraeum-Skript weg.

VIER FEHLER, DIE NUR DIE FERTIGE EXE ZEIGT:

1. rippy/store baute die Engine BEIM IMPORT, mit PostgreSQL als Vorgabe.
   Im Windows-Paket gibt es psycopg2 bewusst nicht -> die EXE starb sofort.
   Die Engine entsteht jetzt beim ersten Zugriff. Und der Fehler war
   grundsaetzlicher als der fehlende Treiber: Ein Modul, das beim Import
   schon eine Verbindung aufbaut, laesst sich gar nicht mehr umstellen —
   store.verbinden() kaeme immer zu spaet.

2. celery_client baute den Broker-Client beim Import. Dasselbe Muster, und
   `from celery_client import celery_client` loeste es sogar dann aus, wenn
   nie ein Rip angestossen wird. Jetzt traege, mit KeinBroker als klarer
   Ansage statt eines Importfehlers.

3. Die Selbstloeschung beim Deinstallieren ging als EIN Argument an cmd.
   Python maskiert dabei die inneren Anfuehrungszeichen — cmd suchte einen
   Dateinamen mit Backslashes davor, fand nichts und meldete nichts (>nul).
   Registry und Autostart waren weg, die 28-MB-Datei blieb liegen: ein still
   scheiternder Hintergrundprozess, wie ihn AGENTS.md beschreibt. Jetzt ein
   Aufraeum-Skript, das 15-mal versucht (die Onefile-EXE laeuft als ZWEI
   Prozesse; der Starter haelt die Datei nach dem Ende noch offen).

4. deinstallieren() raeumte den STANDARD-Ordner auf statt des tatsaechlichen.
   Wer nach --ziel D:\Rippy installiert hatte, dessen Dateien waeren
   geblieben, waehrend ein fremder Ordner angefasst worden waere. Der Pfad
   steht in der Registry (InstallLocation) und wird jetzt dort gelesen.

WAS NOCH NICHT GEHT, ausdruecklich: Ein RIP laesst sich unter Windows noch
nicht anstossen — die Zustellung laeuft weiter ueber Celery. Die Umstellung
auf die LocalQueue ist V2-5. Oberflaeche, Laufwerks-Erkennung, Auswurf und
alles Lesende laufen.

GEMESSEN: ruff sauber, 456 Tests gruen + 15 uebersprungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 11:00:39 +02:00
HitonabiandClaude Opus 5 3d7f3b1680 feat(drives): V2-4 (Teil 2) — main.py laedt unter Windows, Treiber gemessen
Ampel / ampel (push) Failing after 46s
WAS: rippy/drives/__init__.py waehlt den Treiber fuer DIESE Maschine.
main.py und prescan.py fragen ihn, statt fest den Linux-Treiber zu
importieren. Damit ist der Blocker fuer den nativen Windows-Betrieb weg.

GEMESSEN, vorher (frische Python-3.12-Umgebung unter Windows):
  >>> import main
  ModuleNotFoundError: No module named 'fcntl'

GEMESSEN, nachher:
  main.py laedt unter Windows: JA
    Treiber:   rippy.drives.windows
    Routen:    61
    Laufwerke: ['\\.\G:']

Der Import stand in rippy/drives/linux.py -> detection.py -> fcntl. Genau
dafuer gibt es den Port: Der Aufrufer sagt WAS, nicht WIE. test_treiberwahl.py
haelt ausserdem fest, dass BEIDE Treiber denselben Satz Funktionen anbieten —
sonst faellt eine fehlende erst im Betrieb als AttributeError auf, und zwar
auf der anderen Plattform.

DER TREIBER AN ECHTER HARDWARE (LG BU40N extern, Laufwerk G:, LEER):
  GetDriveTypeW("G:\\")              -> 5 (DRIVE_CDROM)
  list_optical_devices()             -> ['\\.\G:']
  CreateFileW(r"\.\G:")             -> Handle 380
  CHECK_VERIFY2 bei leerem Laufwerk  -> ERROR_NOT_READY (21)
  drive_status()                     -> 1 (CDS_NO_DISC)
  detect_disc_type()                 -> 'no_disc'
  device_info()['status']            -> 'empty'
  MEDIA_REMOVAL (entriegeln)         -> Erfolg,   0,1 ms
  EJECT_MEDIA                        -> Erfolg, 664,8 ms

Die 664,8 ms sind der Beleg, dass wirklich etwas passiert ist: Ein
abgelehnter Steuercode kommt in rund 2 ms zurueck.

BEFUND, DER EIN PLACEBO VERHINDERT HAT: IOCTL_STORAGE_LOAD_MEDIA (Schublade
einziehen) antwortet auf diesem Laufwerk mit ERROR_INVALID_FUNCTION. Bei
flachen und externen Laufwerken ist das die Regel. Deshalb gibt es bewusst
KEINE einziehen()-Funktion — ein Knopf "Schublade schliessen", der auf der
Haelfte aller Laufwerke stillschweigend nichts tut, waere genau die Sorte
Placebo, die in Etappe 19 aufgeraeumt wurde.

Dazu eine DeprecationWarning behoben: '\G' im Docstring ist keine gueltige
Escape-Sequenz und waere in einer kuenftigen Python-Version ein harter Fehler.

NOCH NICHT gemessen: das Verhalten MIT eingelegter Disc (Typ-Erkennung ueber
die Groesse, und ein Auswurf, bei dem wirklich etwas drin ist).

GEMESSEN: ruff sauber, 429 Tests gruen + 13 uebersprungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:55:15 +02:00
HitonabiandClaude Opus 5 95705c8d88 feat(ui): V2-3 (Teil 2) — das UI haengt am Ereignis-Strom, Taktgeber raus
Ampel / ampel (push) Successful in 47s
WAS: EventStreamProvider haelt EINE SSE-Verbindung fuer die ganze
Anwendung. Dashboard, Log-Kasten, Log-Seite, Laufwerksliste und
Worker-Liste beziehen ihren Zustand daraus. Sieben von neun setInterval
sind weg.

GEMESSEN AN DEN TAKTGEBERN, je offenem Tab:
  Dashboard      5 Endpunkte / 4 s   75/min  ->  0
  Log-Kasten     2 Endpunkte / 5 s   24/min  ->  0
  Laufwerke      1 Endpunkt  / 5 s   12/min  ->  0
  Log-Seite      1 Endpunkt  /10 s    6/min  ->  0 (+1 Abruf beim Oeffnen)
  Worker-Liste   1 Endpunkt  /15 s    4/min  ->  0 (+1 Abruf beim Oeffnen)
                                     ------
                                     121/min ->  ~2 einmalige Abrufe

Zwei Taktgeber bleiben bewusst: FirstRunWizard (laeuft nur VOR der
Einrichtung) und RipTargetModal (nur solange der Dialog offen ist).

DIE REGEL IST UMGEZOGEN, NICHT VERSCHWUNDEN: Ein Abriss ist keine
Aussage ueber die Welt. Der Provider BEHAELT bei einem Fehler den letzten
Stand und setzt nur `verbunden` auf false; es wird nie eine Liste geleert.
Jede Komponente uebernimmt einen Wert nur, wenn er wirklich da ist —
`devices === null` heisst "konnte nicht nachsehen", nicht "keine
Laufwerke". Das war der Fehler hinter "wird oft neu geladen".

EIN PLACEBO WENIGER: Oben rechts stand ein fest verdrahtetes "ONLINE" mit
pulsierendem Punkt — es leuchtete gruen, auch wenn die API tot war. Jetzt
zeigt es LIVE oder VERBINDUNG WEG, und im Tooltip steht, wann die letzte
Meldung kam.

DER SERVER SCHIEBT JETZT AUCH DEN SERVER-ZUSTAND: Neuer Ereignistyp
system.status (Hardware, Worker, Ablageziele) im 15-Sekunden-Takt des
Waechters — EINMAL im Server statt 15/min je Tab. Nur mitgeschickte
Schluessel werden uebernommen; ein fehlender heisst "behalte deinen Stand".

DAZU EIN FORMATFEHLER GEFUNDEN UND BEHOBEN: /logs bildet ts -> timestamp
ab, mein Snapshot lieferte die rohe DB-Zeile. Das UI haette "Invalid Date"
gezeigt — und zwar NUR im Live-Betrieb, nicht beim manuellen Neuladen.
Jetzt gibt es _log_zeile() einmal, benutzt von beiden.

UND EINEN ZWEITEN: system_lesen lief per asyncio.get_event_loop() in einem
Worker-Thread — dort ist das NICHT die laufende Schleife. Die Coroutine
waere nie gelaufen. Die Schleife wird jetzt im Startup festgehalten.

GEMESSEN: ruff sauber, 378 Tests + 3 uebersprungen, `npm run build`
durch (1650 Module). Der Live-Beweis steht noch aus — er kommt mit dem
Deploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:32:50 +02:00
HitonabiandClaude Opus 5 55db9eb13f feat(api): V2-3 (Teil 1) — Ereignis-Bus und SSE-Endpunkt /api/v2/events
Ampel / ampel (push) Failing after 41s
WAS: rippy/bus mit Ereignis-Schema, In-Process-Treiber und Ringpuffer.
Dazu der SSE-Strom in der API: erst ein Snapshot, danach nur Deltas,
mit lueckenloser Wiederaufnahme ueber Last-Event-ID.

WARUM: Ein offener Tab plus ein Worker verursachen heute rund 133
Anfragen pro Minute (Dashboard 75 + Log-Kasten 24 + Laufwerke 12 +
Worker-Liste 4 + Log-Seite 6 + Tray 12). Das Rate-Limit stand einmal
UNTER dieser Zahl — daher die sich leerende Job-Liste im Sekundentakt.
Mit einer offenen Verbindung sind es null.

DREI EIGENSCHAFTEN, ALLE AUS v1-FEHLERN:

1. Der Bus traegt NUR Nachrichten ueber Aenderungen, nie den Zustand.
   Wer den Zustand will, fragt den Store. Damit kann ein verpasstes
   Ereignis auch keinen Zustand loeschen — anders als beim fuenffachen
   `catch(() => [])` im alten UI, wo jeder fehlgeschlagene Abruf
   "es gibt keine Jobs" bedeutete.
2. Eine zu grosse Luecke wird ANGESAGT, nicht verschluckt. nachliefern()
   gibt None ("hol dir ein ganzes Bild") statt [] ("nichts verpasst") —
   dieselbe Unterscheidung wie timeout-Rueckgabe 124 bei den Netzpfaden.
   Stillschweigend weiterzumachen waere schlimmer: Das UI hielte sich
   fuer aktuell und waere es nicht.
3. Der Snapshot meldet unlesbare Laufwerke als None, nicht als leere
   Liste. "Konnte nicht nachsehen" ist etwas anderes als "gibt es nicht".

Ein unbekannter Ereignistyp fliegt beim Senden HOCH statt durchzugehen.
Ein Tippfehler waere sonst der stillste aller Fehlschlaege: Nachricht
raus, kein Empfaenger, nirgends ein Hinweis.

Ein langsamer Zuhoerer (Tab im Hintergrund, lahmes Handy) bremst den
Sender nicht — er wird markiert und bekommt beim naechsten Mal einen
Snapshot. Ein Rip darf nicht auf einen Browser warten.

GEMESSEN: ruff sauber, 359 Tests gruen + 3 uebersprungen (vorher 346).
13 neue Bus-Tests laufen auf jeder Plattform; die vier SSE-Tests haengen
an main.py und laufen damit in der Ampel.

NOCH OFFEN in V2-3: das UI auf den Strom umstellen (neun setInterval)
und die Ereignisse an den Zustandsaenderungen ausloesen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:09:33 +02:00
HitonabiandClaude Opus 5 dd1d0b7365 refactor(core): V2-1 — die vier Ports, ein Store, ein Laufwerks-Treiber
Ampel / ampel (push) Successful in 40s
WAS: rippy/ports.py beschreibt Store/Queue/Bus/Drives als Protocol. Zwei
weitere Doppelungen sind zusammengelegt: db.py (API+Worker) wird
rippy/store, und der Auswurf (api/devices.py + ripping.wirf_disc_aus)
wird rippy/drives/linux. Verhalten unveraendert.

WARUM: Drei Betriebsarten tragen nur, wenn ein Modus die Auswahl der
Treiber hinter vier Nahtstellen ist statt ein eigener Codestand
(KONZEPT-V2.md §1). Diese Etappe zieht die Nahtstellen ein, ohne schon
einen zweiten Treiber zu haben — die kommen in V2-2 (SQLite/LocalQueue)
und V2-4 (Windows).

DIE UNANGENEHMERE DOPPELUNG WAR DER AUSWURF: Er stand zweimal da, mit
UNTERSCHIEDLICHEN Vertraegen — devices.eject wirft OSError, ripping.
wirf_disc_aus gibt False zurueck und wirft nie. Beides ist richtig fuer
seine Seite (Browser-Meldung gegen "ein Rip stirbt nicht an einer
klemmenden Schublade"). Jetzt liegt EINE Mechanik darunter
(auswerfen_mit_grund) und beide Vertraege unveraendert darueber.
Die API-Fassung war ausserdem NIE getestet — jetzt schon, inklusive
"reicht ENOENT/EPERM unveraendert weiter".

get_settings hatte den einzigen echten Verhaltensunterschied der beiden
db.py: die Worker-Fassung schluckte jeden Fehler und gab {} zurueck.
Nicht still entschieden, sondern sichtbar gemacht — der Parameter
bei_fehler_leer steht jetzt in der Signatur, mit der offenen Frage im
Docstring. {} heisst fuer den Aufrufer "nichts gesetzt", nicht "konnte
nicht nachsehen"; das ist dieselbe Klasse wie catch(() => []) im alten
UI. Zu entscheiden in V2-2.

ZWEITER BEINAHE-FEHLER DIESER ETAPPE: linux.py importierte detection
auf Modulebene — und das zieht fcntl. Damit waere ripping.py und ueber
es der NATIVE WINDOWS-WORKER nicht mehr ladbar gewesen. Diesmal haben
die Tests es sofort gefangen (4 Sammelfehler). Behoben an der Wurzel:
Konstanten und die reine classify() leben jetzt in drives/cdrom.py,
ganz ohne fcntl. Nebengewinn — die classify-Tests liefen bisher NUR in
der Ampel ("erst nach dem Push bewiesen") und laufen jetzt ueberall.

Ausserdem: .dockerignore-Testmuster brauchen **, sonst greifen sie nur
in der obersten Ebene. Im laufenden Container nachgezaehlt: 30 test_*.py
lagen in den Images.

GEMESSEN: ruff sauber, 301 Tests gruen + 1 uebersprungen (vorher 290;
+4 neue eject-Tests, +7 classify-Tests die jetzt lokal laufen). Kein
Modul liegt mehr doppelt im Repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:54:28 +02:00
HitonabiandClaude Opus 5 b98dc5eef5 refactor(core): V2-0 — gemeinsames Paket rippy statt drei Zwillingsdateien
Ampel / ampel (push) Successful in 41s
WAS: Neues Paket src/rippy (core/drives/rip). detection.py,
makemkv_daten.py und notify.py lagen je ZWEIMAL im Repo — unter
docker/api UND docker/worker, byte-identisch. Jetzt gibt es sie einmal;
beide Container importieren dieselbe Datei. Verhalten unveraendert.

WARUM: Es gab kein geteiltes Paket zwischen den Containern, deshalb die
Kopien. docker/api/db.py sagt es im Kopfkommentar selbst: "Wer die
Struktur aendert, aendert BEIDE Dateien." Ein Waechter-Test
(test_zwillinge_sind_byteweise_identisch) hat das mechanisch
abgesichert — er war noetig, weil die Konstruktion falsch war. Erste
Etappe des v2-Plans (KONZEPT-V2.md §8.3).

IM EINZELNEN:
- src/rippy/{core/notify, drives/detection, rip/makemkv_daten}.py
- conftest.py in der Wurzel legt src/ auf den sys.path (die Ampel ruft
  pytest dort auf).
- Beide Dockerfiles kopieren src/rippy nach /app/rippy — /app ist
  Arbeitsverzeichnis und uvicorn-App-Dir, also ohne PYTHONPATH findbar.
- worker_setup_paket packt das Paket ausdruecklich mit ins Zip: die
  Schleife sah nur die oberste Ebene, ein Unterordner waere nie
  mitgekommen und der Windows-Worker beim Start gestorben.
- Die zwei Test-Dateien fuer makemkv_daten sind zu einer verschmolzen
  (die Faelle aus beiden). Damit entfaellt auch der importlib-Umweg im
  Worker-Test: der war noetig, weil bei "pytest -q" aus der Wurzel
  docker/api zuerst eingesammelt wird und jeder weitere Import nur noch
  den sys.modules-Cache trifft — die Worker-Kopie wurde also nie
  angefasst. Netto -13 doppelte Tests, +2 neue (siehe unten).

EIN FEHLER, DEN DER UMBAU FAST AUSGELIEFERT HAETTE: In tasks.py steht
der detection-Import in einem "try/except ImportError" — absichtlich,
denn der native Windows-Worker hat kein fcntl und soll trotzdem
starten. Das except verschluckt aber JEDEN ImportError, auch einen
falschen Modulpfad. Auf Linux haette der Worker ab sofort still
detect_disc_type=None gesetzt und jeden Rip verweigert, ohne dass
irgendwo ein Fehler stuende — genau die Klasse "still scheiternder
Hintergrund-Prozess" aus AGENTS.md. Gefunden ueber eine zweite Suche
mit eingerueckten Treffern.

Waechter dagegen: src/rippy/test_paket.py prueft mit
importlib.util.find_spec (fuehrt nichts aus, laeuft also auch auf
Windows ohne fcntl), dass es die drei Modulpfade wirklich gibt. Beim
Wegnehmen von detection.py gegengeprueft: Test wird rot, nach
Wiederherstellen gruen.

GEMESSEN: ruff sauber, 290 Tests gruen + 1 uebersprungen (lokal, ohne
die zwei Linux-only-Module). Kein Modul liegt noch doppelt im Repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:38:29 +02:00
Hitonabi a32a1bb075 fix: bare except in main.py
Ampel / ampel (push) Failing after 38s
2026-07-27 16:56:36 +02:00
Hitonabi e165e2a0d3 feat: Komplettierung der aktuellen Rippy-Etappe
Ampel / ampel (push) Failing after 29s
- Versionsanzeige für den Windows-Worker im UI inkl. Prüfung
- Absicherung der install.sh gegen fehlendes systemd
- Serien-Episoden-Erkennung und Laufzeitabgleich anhand TMDB-Daten (Heuristik)
- Umstellung der Windows-Worker-Installation auf SchTasks (Dienst-Ersatz)
- Kodi-Bibliotheks-Refresh über JSON-RPC integriert
2026-07-26 21:13:27 +02:00
HitonabiandClaude Opus 5 bfb13f44a5 fix(ui+api): das "Neuladen" war HTTP 429 - und "Neu" komprimierte immer
Ampel / ampel (push) Successful in 30s
Zwei Commander-Befunde, beide mit derselben Wurzel: Rippy hat sich selbst
ausgebremst und dann geschwiegen.

## "Wenn der Worker installiert ist, wird dieser Bereich oft neu geladen"

Gemessen statt geraten. Die Antworten von /jobs und /capabilities waren ueber
zwanzig Sekunden byteweise identisch, alle Endpunkte antworteten unter 30 ms -
es wurde also gar nichts neu geladen. Im nginx-Log standen dagegen 97 Antworten
mit HTTP 429.

Drei Fehler griffen ineinander:

1. Das Limit war zu klein fuer Rippy selbst: 100 Anfragen/min, waehrend ein
   offener Tab 111/min verursacht (Dashboard 75 + Log-Kasten 24 + Laufwerke 12)
   und der Windows-Tray weitere 12/min dazulegt.
2. Der nginx gab die Client-Adresse nicht weiter. Fuer die API kam damit ALLES
   von 172.19.0.6 - Browser, zweiter Tab und Tray teilten sich einen Eimer
   (812 von 876 Anfragen). Das erklaert die Kopplung an den Worker: tray.py
   fragt /api/jobs ueber Port 80, also durch denselben Proxy.
3. Ein abgewiesener Abruf leerte das UI. `catch(() => [])` heisst "es gibt
   keine Jobs" - richtig waere "ich weiss gerade nichts Neues". Fuer einen Takt
   stand "Keine Jobs", die Zaehler sprangen auf (0), vier Sekunden spaeter war
   alles zurueck.

Behoben: X-Real-IP im nginx, Grenze auf 600/min mit vorgerechneter Herleitung,
jeder Fehlschlag laesst den alten Stand stehen (null statt []), axios bekommt
eine Zeitgrenze, und das Dashboard trennt schnelle Daten (Jobs/Laufwerke, 4 s)
von langsamen (Hardware/Worker/Ablagen, 12 s) - 75/min werden zu 30/min.
Ein greifendes Limit steht ab jetzt im Log, gedrosselt auf eine Meldung pro
Client und Minute.

## "Hier gibt es den Button 'neu' aber WAS wird dann gemacht?"

Immer die Komprimierung - auch bei einem Job, dessen RIP abgebrochen war. Am
26.07.2026 waeren aus 5,1 GB Bruchstueck (von rund 40 GB) brav ein Film
geworden, der bei 12 % aufhoert.

Die Phase war nach `status = "failed"` nicht mehr feststellbar, also wird sie
jetzt vermerkt (rip_fertig in den Job-Metadaten: false beim Rip-Start, true bei
der Uebergabe an die Kompression). Daraus folgt die Beschriftung: "Neu
komprimieren", "Neu rippen" - oder bei Bestandsjobs ohne Vermerk ein Dialog,
der beide Wege erklaert und die Groesse der Rohdaten als Entscheidungshilfe
nennt. Geraten wird nicht. Fuer den Rip-Fall gibt es POST
/jobs/{id}/retry-rip: neuer Job mit neuer ID (sonst laege das Bruchstueck im
Roh-Verzeichnis des neuen Rips), Titel/Ablage/Sprachwahl uebernommen, mit
ehrlicher Absage wenn keine Disc im Laufwerk liegt.

10 neue Tests (289 gruen), darunter eine Kopplungspruefung: der Name der
Phasen-Marke muss in worker/tasks.py und api/phasen.py zusammenpassen - genau
diese Sorte Auseinanderdriften hat die Zombie-Erkennung ein Release lang blind
gemacht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:49:10 +02:00
HitonabiandClaude Opus 5 19f3dc5330 fix(zombies): "running" fehlte - ein abgestuerzter RIP wurde NIE gefunden
Ampel / ampel (push) Successful in 30s
Der Commander: "Ausserdem ist gerade mitten im Rip das Laufwerk ausgegangen...
glaube ich zumindest." Gemessen war es etwas anderes, und der Fund ist groesser
als der Vorfall.

WAS MESSBAR WAR:
  Job 2182d525   status=running progress=12   (Rip gestartet 14:00:24)
  makemkvcon     laeuft NICHT (per /proc geprueft, PID gegengeprueft)
  Rohdatei       5.167.382.528 Bytes, waechst in 10 s nicht
  Laufwerk       Status 4 (Disc drin), /dev/sr0 + /dev/sg1 da
  Worker-Start   14:07:02  <- mein `docker compose up -d --build`

Das Laufwerk ist also NICHT ausgegangen. Der Rip wurde von MEINEM Deploy
getoetet: `up -d --build` baut den worker-Container neu, und der laufende Rip
stirbt mit ihm. Genau davor warnt der SAVEPOINT seit v3.18 - die Warnung half
nichts, weil sie niemand liest und nichts sie prueft.

DER EIGENTLICHE FUND: Die Zombie-Erkennung lief um 14:09:04 und meldete
`{'geprueft': 0, 'aufgeraeumt': []}` - obwohl der tote Job direkt vor ihr lag.
Ursache:

  ARBEITS_STATI = ("ripping", "transcoding", "canceling")   # zombies.py
  db.update_job(job_id, status="running", ...)              # tasks.py - der Rip

Der Rip setzt "running", gesucht wurde "ripping". Dieser Wert steht
ausschliesslich in Celerys Task-META und NIE in einer Job-Zeile (nachgeprueft:
kein einziger Schreiber im ganzen Baum). Die Zombie-Erkennung aus v3.14 wurde
gebaut, um genau einen abgestuerzten Rip zu finden - und hat ihn nie gesehen.

Besonders tueckisch: `geprueft: 0` sah bei jedem Worker-Start wie "nachgesehen,
alles gesund" aus, waehrend sie nach einem Status suchte, den es nicht gibt.
Deshalb blieb der Job auf "processing 12 %" stehen - mit einer Restzeit-Schaetzung
von 1 h 30 min obendrauf, die es fuer einen toten Prozess nicht geben duerfte.

GEBAUT:
  * "running" in ARBEITS_STATI.
  * Ein Test, der das Auseinanderlaufen MECHANISCH verhindert: Er liest tasks.py,
    sammelt jeden Status, den der Worker per db.update_job in eine Job-Zeile
    schreibt, und verlangt, dass jeder davon entweder ein Arbeitsstatus oder ein
    Endzustand ist. Ein Kommentar haette das nicht verhindert.
  * GET /health/arbeit + eine Sperre in deploy.sh: Laeuft ein Job, bricht der
    Deploy ab (uebersteuerbar mit RIPPY_TROTZDEM=1 - dann ist es eine
    Entscheidung und kein Versehen). Ein Satz Code gegen eine verlorene Stunde.
  * Server-Status zeigt jetzt die eingehaengten FREIGABEN mit freiem Platz, nicht
    nur die Container-Platte (Commander-Wunsch). Genau dort liegen die Rohdaten,
    und bei externem Encoden muessen sie dort liegen - wer wissen wollte, ob noch
    Platz fuer eine Disc ist, sah die falsche Zahl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:16:05 +02:00
HitonabiandClaude Opus 5 f146f33f5d feat(installer): Verknuepfung auf dem Desktop - mit Rippy-Icon
Ampel / ampel (push) Successful in 30s
Commander: "Und wenn man den worker installiert braucht man auch eine exe auf dem
Desktop abgelegt wird zum starten you know?"

Berechtigt: Die Startdateien lagen nur im Programmordner. Wer den Worker von Hand
starten wollte, musste erst "C:\Program Files\Rippy Worker" suchen - und genau
das war heute noetig, als der Worker stand.

Jetzt legen beide Installer eine Verknuepfung "Rippy Worker" auf den Desktop
(GUI mit Haekchen, Kommandozeile per -KeineDesktopVerknuepfung abschaltbar). Drei
Details, die den Unterschied machen:

  * Das ICON wird mit ausgeliefert. rippy.ico lag im Repo, ging aber nie an die
    Zielmaschine - die Verknuepfung haette das Batch-Standardsymbol getragen und
    waere zwischen den anderen Icons unfindbar gewesen. Jetzt im Worker-Paket.
  * WindowStyle 7 (minimiert): start-tray.bat startet pythonw, also ohne
    Konsolenfenster - aber die .bat selbst blitzt sonst kurz auf. Gilt jetzt auch
    fuer die Autostart-Verknuepfung, wo derselbe Blitz war.
  * Der DEINSTALLER nimmt sie mit, an beiden moeglichen Orten (eigener Desktop
    und Desktop aller Benutzer). "Rueckstandsfrei entfernbar" ist ein
    MUSS-Kriterium; ein totes Symbol auf dem Desktop waere genau so ein Rueckstand.

Dieselbe Rechte-Ueberlegung wie beim Autostart: bei einer Installation unter
"Programme" auf den Desktop ALLER Benutzer, sonst auf den eigenen - bei einer
UAC-Erhoehung ueber ein fremdes Admin-Konto waere der eigene der falsche.

Layout headless gerendert und angesehen (Fenster 648 -> 672 px, nichts
ueberlappt), beide .ps1 mit echtem PowerShell 5.1 geprueft, .exe neu gebaut.
Und die CRLF-Falle aus dem Savepoint hat prompt wieder zugeschlagen: Mein
Python-Rewrite machte aus install-gui.ps1 LF - zurueckgedreht, `file` bestaetigt
BOM und CRLF fuer beide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 15:46:15 +02:00
HitonabiandClaude Opus 5 148ac494c2 feat(sprachen): Rippy fragt vor dem Rip, welche Sprachen du willst
Ampel / ampel (push) Successful in 29s
Commander-Anforderung: "Die Disc hat Material in X Sprachen und X Untertiteln -
Rippy muss VOR dem Rip fragen: Was genau willst du haben? In der Automatik muss
das ebenfalls einstellbar sein."

Die Auskunft lag laengst vor und wurde weggeworfen: Derselbe Titel-Scan, der die
Titel-Tabelle fuellt, liefert in derselben makemkvcon-Ausgabe die Streams mit.
Ein zweiter Info-Lauf haette eine Minute Wartezeit gekostet - jetzt kommt beides
aus einem Aufruf.

Format an der Akira-Blu-ray im Laufwerk gemessen (AGENTS Regel D):

  SINFO:<titel>,<stream>,<attribut>,<code>,"<wert>"
  1 = Typ ("Audio"/"Subtitles"), 3 = Sprachcode, 4 = Sprachname,
  6 = Codec, 14 = Kanaele, 30 = Beschreibung

DIE FALLE dabei: Die Sprache steht in 3/4, NICHT in 28/29. Die tragen auf JEDEM
Stream "eng"/"English" - auch auf einer deutschen Tonspur und auf dem
Videostream; das ist MakeMKVs eigene Anzeigesprache. Wer 28 nimmt, haelt jede
Disc fuer englisch. Ein Test haelt das fest.

Angewendet wird die Wahl bei der KOMPRESSION, nicht beim Rippen - drei Gruende:
der Rip bleibt vollstaendig und verlustfrei (Muss-Feature laut KONZEPT); HandBrake
hat dafuer dokumentierte Schalter (--audio-lang-list / --subtitle-lang-list, die
genau die ISO-639-2-Codes nehmen, die MakeMKV liefert - beides gegengeprueft);
und wer spaeter andere Sprachen will, komprimiert neu statt die Disc wieder
einzulegen. Genau das sagt der Dialog auch, sonst glaubt man, es werde
unvollstaendig gerippt.

Nichts angeklickt heisst "alles behalten" - das Verhalten von vorher. Auch die
Einstellung ist bewusst LEER vorbelegt: Ein stilles "deu" wuerde bei einem
japanischen Original die Originaltonspur wegwerfen, ohne dass jemand gefragt hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 15:10:21 +02:00
HitonabiandClaude Opus 5 58f4991d92 fix(mounts): 3 min 15 s hingen in EINER Zeile - os.makedirs auf dem toten Mount
Ampel / ampel (push) Successful in 29s
Nachtrag, weil die letzte Runde die Wiederanbindung nicht schneller machte
(202 s statt 150 s). Die Zeitstempel im Log zeigten, wo die Zeit sitzt:

  12:53:56  API gestartet
  12:54:03  "antwortet nicht - wird neu verbunden"    <- Erkennung: 7 s, gut
  12:57:18  "eingehaengt"                             <- Reparatur: 3 min 15 s

Die Erkennung war also schon schnell; die REPARATUR fraess die Zeit. Und zwar
nicht in den Mount-Versuchen, sondern in der ersten Zeile von mounten():

  os.makedirs(ziel, exist_ok=True)

`exist_ok` prueft mit os.path.isdir, und ein `stat` auf einen toten CIFS-Mount
blockiert im Kernel bis zum SMB-Timeout. Ausgerechnet der Aufruf, der nur "lege
den Ordner an, falls er fehlt" bedeutet, hing drei Minuten - BEVOR irgendeine der
sorgfaeltig begrenzten Pruefungen dran war. Dritter Fund derselben Sorte an einem
Tag: os-Aufruf auf einen Netzpfad ohne Zeitgrenze.

Jetzt klaert `pfad_lage()` die Lage mit einem abbrechbaren Kind-Prozess
(`timeout 4 ls -d`, drei Antworten: da / weg / unklar), und makedirs laeuft nur
bei "weg". Dieselbe Falle in `reparieren()` (os.path.ismount als Vorbedingung -
gebraucht wird es nicht, `umount -l` auf einen leeren Pfad kostet nichts) und in
`aushaengen()` (ismount + rmdir).

Dazu steht die DAUER jetzt im Log ("neu verbunden (4.2s)"). Sie war die
entscheidende Spur; wer sie ablesen kann, muss sie nicht rekonstruieren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 15:01:09 +02:00
HitonabiandClaude Opus 5 2a90538473 fix: Deinstaller, Verwaltungsfenster, Dashboard-Widerspruch, Mount-Tempo
Ampel / ampel (push) Successful in 30s
Vier Meldungen des Commanders, alle nachgemessen.

1. DEINSTALLER LIEF NICHT MEHR - reproduziert mit echtem PowerShell:

     Der Typ [System.Windows.Forms.MessageBox] wurde nicht gefunden.

   `Add-Type -AssemblyName System.Windows.Forms` stand EINE ZEILE ZU SPAET, die
   MessageBox wurde davor benutzt. Der Deinstaller starb also in seiner ersten
   Arbeitszeile, jedes Mal. Zwei weitere Maengel gleich mit:
   Kodierung war ASCII trotz Umlauten, und `Remove-Item -Recurse -Force
   $PSScriptRoot` loescht den Ordner, in dem das laufende Skript liegt - das
   klappt auf Windows nicht zuverlaessig (venv-DLLs sind geladen). Jetzt raeumt
   ein losgeloestes cmd nach, sobald PowerShell weg ist. Der erzeugte Deinstaller
   ist gegengeprueft: parst, BOM da, Umlaute intakt.

2. VERWALTUNGSFENSTER (Doppelklick aufs Tray). Zeigt Status, Aufgaben und Log,
   plus Knoepfe fuer Rippy, Log-in-Rippy und Deinstallieren - Deinstallieren geht
   damit auch aus dem Tray-Menue. Eigener PROZESS statt Fenster im Tray, weil
   pystray und tkinter beide den Haupt-Thread wollen; tkinter statt WinForms,
   weil es bei jeder Windows-Python-Installation dabei ist. Headless gerendert
   und angesehen. Alles Fachliche kommt von Rippy (/capabilities, /jobs), damit
   dort nicht eine zweite, abweichende Wahrheit steht.

3. DASHBOARD-WIDERSPRUCH. Oben stand "Akira im Laufwerk erkannt", die
   Server-Status-Karte gleichzeitig "Bereit - keine Disc in Arbeit / Disc
   einlegen". Zwei Aussagen, ein Blick. Die Karte fragt jetzt /devices und sagt
   "Disc erkannt - wartet auf Rippen starten" samt Titel.

4. MOUNT-TEMPO. Der Commander: "150 Sekunden? Das ist verrueckt langsam." Recht
   hat er. Gemessen ging die Zeit fast komplett in einen FEHLVERSUCH: `umount -l`
   ist lazy, der Abbau passiert spaeter, und wer direkt danach mountet, riskiert
   dass der Abbau hinter dem neuen Mount landet. Der erste Reparaturversuch
   scheiterte dadurch regelmaessig - und der zweite kostete 30 s mount-Timeout
   plus Pruefungen. Jetzt werden 1,5 s auf den Abbau gewartet, bevor neu
   gemountet wird; die Wache prueft erstmals nach 3 s statt 10 s und benutzt zum
   ERKENNEN die einfache schnelle Probe (die Doppelprobe steckt dort, wo der
   Wettlauf lauert: direkt nach dem Mount).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:52:53 +02:00
HitonabiandClaude Opus 5 4cf7acbb96 fix(mounts): DIE URSACHE gefunden - die CIFS-Verbindung stirbt mit dem Container
Ampel / ampel (push) Successful in 31s
Der Savepoint v3.17 fuehrte das als "Ursache liegt beim NAS, nicht gefunden".
Gemessen ist es etwas ganz anderes, und es liegt bei uns:

  /proc/fs/cifs/DebugData  ->  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 eingehaengt, 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 laeuft in den CIFS-Timeout.

Damit erklaert sich alles, was vorher widerspruechlich aussah: warum es nach
JEDEM Deploy passiert, warum `mount` Erfolg meldet, warum /proc/mounts genau eine
korrekte Schicht zeigt, und warum nur ein echtes Neu-Verbinden hilft. Das NAS ist
unschuldig (eine Sitzung, Status 1, 630 Credits, Ping 0,47 ms).

Zweiter Fund, der den Rest erklaert: Direkt nach einem frischen Mount antwortete
die Freigabe - und Sekunden spaeter nicht mehr. Das ist ein Wettlauf mit
`umount -l`: lazy heisst, der Abbau passiert spaeter, und faellt er samt
Propagation hinter den neuen Mount, zeigt der Pfad wieder auf die Leiche. Eine
einzige Probe kann das nicht sehen - deshalb prueft `wirklich_erreichbar()`
zweimal mit drei Sekunden Abstand, und zwar sowohl beim Mounten als auch in der
Wache.

Dazu: erste Pruefung der Wache schon nach 10 s statt 60 s. Genau dann ist die
Lage nach einem Deploy kaputt.

Ehrlich offen bleibt die strukturelle Folge: Der api-Container HAELT die
NAS-Verbindung. Startet er mitten in einem Rip neu, verliert auch der Worker sein
Ziel. Das saubere Gegenmittel waere ein Mount auf dem HOST statt im Container -
ein eigener Umbau, und er widerspraeche "Speicherziele ueber das UI einhaengen".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:32:51 +02:00
HitonabiandClaude Opus 5 549727f648 fix(mounts): Wache, die die Freigabe nach einem Rebuild von selbst zurueckholt
Ampel / ampel (push) Successful in 28s
Dreimal in Folge reproduziert, jetzt bei JEDEM Deploy: Nach `docker compose up -d
--build` ist die CIFS-Freigabe tot. `mount` meldet Rueckgabewert 0, /proc/mounts
zeigt genau eine korrekt aussehende Schicht, die Erreichbarkeits-Probe antwortet
direkt nach dem Mount sogar - und Sekunden spaeter laeuft jeder Zugriff in die
Zeitgrenze. Derselbe Ablauf ein bis zwei Minuten spaeter stellt sie zuverlaessig
her (POST /storage-mounts/rippy/repair, mehrfach belegt).

Die Ursache liegt am NAS und ist nicht gefunden. Aber die Wirkung ist teuer: Nach
jedem Update war jeder Rip auf die NAS kaputt, ohne dass irgendwo etwas davon zu
sehen war - und der Commander haette es jedes Mal von Hand richten muessen.
Wenn die Heilung bekannt und billig ist, gehoert sie automatisiert, auch ohne die
Ursache zu kennen.

Die Wache sieht jede Minute nach und verbindet stumme Freigaben neu. Zwei Dinge
sind dabei wichtiger als die Heilung selbst:

1. NIE waehrend ein Job laeuft. Neu verbinden heisst `umount -l`; mitten in einem
   Rip oder Encode waere das ein Datenverlust. Die Wache steht still, solange
   irgendein Job nicht durch ist - auch bei einem wartenden, der jeden Moment
   anlaufen kann (db.hat_arbeit).
2. Gemeldet wird nur der UEBERGANG. Ist das NAS ausgeschaltet, waere ein Log je
   Minute ein Wasserfall.

Der Zustand steht in /health/vorraete, damit man sehen kann, dass die Wache lebt
- dieselbe Lehre wie heute Nachmittag beim stillen except.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 14:24:09 +02:00
HitonabiandClaude Opus 5 8ccca90e3c fix(api): "konnte nicht nachsehen" ist nicht "ist weg"
Ampel / ampel (push) Successful in 28s
Gemessen nach dem letzten Deploy: Der ERSTE Zugriff auf die CIFS-Freigabe stallt
nach einem Container-Neustart mehrere Sekunden (die SMB-Sitzung wird neu
aufgebaut), danach antwortet sie in 0,01 s - zehn von zehn Versuchen, NAS per
Ping bei 0,47 ms. Die Freigabe ist also gesund, nur der erste Griff ist teuer.

In diesem Fenster lief die Pruefung in ihre Zeitgrenze, der Vorrat notierte
"keine Rohdaten", und der Knopf "Neu komprimieren" verschwand - obwohl 74 GB
dalagen. Der Nutzer haette daraus geschlossen, seine Daten seien weg. Genau
diese Sorte Fehlschluss ("aus einem Zustandswert auf einen Mechanismus") hat das
Projekt schon zweimal bezahlt.

Deshalb drei Antworten statt zwei: "da", "weg", "unklar". 124 ist der
Rueckgabewert von `timeout`, wenn es das Kind abgeschossen hat - das heisst
NICHT angesehen. Bleibt eine Pruefung unklar und der Vorrat hatte vorher einen
Treffer, wird die letzte bekannte Antwort gehalten statt Abwesenheit behauptet.

Wer eine Ja/Nein-Antwort braucht (verzeichnis_da), bekommt im Zweifel weiter
Nein - das ist die richtige Richtung fuer eine einzelne Abfrage.

Live davor geprueft: Die Schleife LEBT jetzt (alter_sekunden 30 -> 21 -> 12 -> 3
ueber 100 s) und heilt sich selbst - sobald die Freigabe antwortete, stand
mit_treffer=1 und can_retry=true.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:34:39 +02:00
HitonabiandClaude Opus 5 2554c2633b fix(api): os.path.isdir hing im Kernel und toetete die Schleife - harte Zeitgrenze
Ampel / ampel (push) Successful in 28s
Ursache gefunden, nicht geraten. Der neue Diagnose-Endpunkt sagte
`rohdaten.alter_sekunden: null` - die Funktion war NIE EINMAL fertig geworden,
und kein Fehler war gemeldet. Der Blick auf die Threads des API-Prozesses zeigte
zwei im Zustand **D** (uninterruptible sleep, im Kernel blockiert):

  tid=1600034 name=uvicorn state=D
  tid=1600037 name=uvicorn state=D

Der Mechanismus: Die Schleife startete ihren ersten Durchlauf, waehrend Rippy
beim Container-Start die CIFS-Freigabe neu einhaengte. Ihr `os.path.isdir` blieb
im Kernel stecken, `asyncio.to_thread` kam nie zurueck, die Schleife erreichte
ihr `sleep` nie - und war damit fuer immer tot. Sichtbar war nur, dass
can_retry dauerhaft false blieb.

Ein Timeout um den Aufruf haette nichts geholfen: Ein im Kernel haengender
Thread laesst sich aus Python nicht abbrechen, jeder Versuch haette einen
weiteren Thread verbrannt, bis der Pool leer ist.

Ein Kind-PROZESS laesst sich abbrechen. Geprueft wird jetzt mit
`timeout 4 ls -d <pfad>` - dasselbe Werkzeug, das mounts.ist_erreichbar seit dem
24.07.2026 fuer genau dieses Problem benutzt (dort fuer den toten NAS-Mount).
Laeuft es in die Zeitgrenze, gilt das Verzeichnis als "nicht da": Ein Ort, den
man nicht in Sekunden ansehen kann, ist fuer einen Rip ohnehin unbrauchbar.

os.listdir bleibt fuer /app/media selbst - das ist ein lokales Verzeichnis, die
Freigaben sind Unterordner davon. Und os.path.isdir bleibt fuer den Rueckfall
auf /app/temp/raw: ein Docker-Volume, dort kann nichts haengen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:28:02 +02:00
HitonabiandClaude Opus 5 0d8426d353 diag(api): Vorrats-Schleifen pruefbar machen - der stille except hat gekostet
Ampel / ampel (push) Successful in 28s
Live-Befund: /jobs/<id>/rohdaten findet die 74,1 GB, aber can_retry blieb ueber
80 Sekunden false - der Vorrat der Hintergrund-Schleife war leer. Direkt im
Container aufgerufen fuellt die Funktion ihn korrekt. Warum die Schleife im
Server-Prozess nichts tat, war NICHT zu sehen: Der `except Exception: pass`
verschluckte jeden Grund.

Zwei Konsequenzen, unabhaengig von der Ursache:

1. Die Schleife MELDET ihren Fehler jetzt (print in den Container-Log) statt ihn
   zu verschlucken. Ein Hintergrund-Prozess, der still scheitert, ist schlimmer
   als einer, der laut scheitert.

2. Neuer Diagnose-Endpunkt GET /health/vorraete: nennt fuer Ping- und
   Rohdaten-Vorrat, wie ALT der letzte Durchlauf ist. Bleibt so ein Vorrat leer,
   ist am Endpunkt selbst naemlich nichts zu sehen - er antwortet nur dauerhaft
   "nichts gefunden".

Die naheliegende Erklaerung (asyncio-Tasks ohne Referenz werden vom GC geholt)
ist widerlegt: Die aeltere Ping-Schleife laeuft nach demselben Muster und lebt -
ueber 72 Sekunden blieb jeder /capabilities-Aufruf unter 10 ms, waehrend ein
toter Ping-Vorrat je 30 s einen synchronen Nachping von ~1 s gekostet haette.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:23:32 +02:00
HitonabiandClaude Opus 5 9fbaed4af8 fix(api): Rohdaten-Suche in den Hintergrund - /jobs darf nie am NAS haengen
Ampel / ampel (push) Successful in 28s
Bei der Live-Gegenprobe gemessen: Waehrend der CIFS-Mount nach einem
Container-Neustart hochkam, brauchten /jobs und /jobs/<id>/rohdaten jeweils
10,0 Sekunden - genau der CIFS-Timeout. Das Dashboard fragt /jobs alle vier
Sekunden ab; ein schlafendes NAS haette es damit dauerhaft eingefroren.

Dieselbe Loesung wie beim Celery-Ping in /capabilities (v3.15): eine
Hintergrund-Schleife im 30-Sekunden-Takt sieht nach, wo Rohdaten liegen, und
/jobs liest nur noch ab. Kennt der Vorrat einen Job noch nicht (frischer
Fehlschlag), zaehlt der billige lokale Ort auf der Container-Platte - der
antwortet immer sofort.

Live geprueft, nachdem der Mount stand: /jobs 0,022 s, /rohdaten 0,014 s,
can_retry = true, 79.604.951.639 Bytes (74,1 GB) gefunden. Der 80-GB-Rohschnitt
des Commanders ist damit erstmals ueber das UI wieder erreichbar.

Der leere Treffer davor war KEIN Codefehler: der Mount stand in dem Moment
schlicht noch nicht. Beleg nachgeliefert - im Container gemessen findet die
Suche den Pfad in 0,000 s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:15:42 +02:00
HitonabiandClaude Opus 5 108368d583 fix(jobs): Rohdaten werden gesucht statt geraten - "Neu komprimieren" ging nicht
Ampel / ampel (push) Successful in 28s
Bei der Live-Gegenprobe des neuen /rohdaten-Endpunkts aufgefallen, und der Fund
ist groesser als der Endpunkt: Job 95afdc89 hatte `can_retry = false`, obwohl
79,6 GB intakter Rohschnitt unter /app/media/rippy/<id> lagen. Der Knopf
"Neu komprimieren" existierte gar nicht.

Der SAVEPOINT v3.16 schrieb dazu: "Rohschnitt 79,6 GB intakt -> 'Neu
komprimieren' genuegt, kein Neu-Rip." Das war falsch, und zwar doppelt:

  _kann_neu_komprimieren  suchte in /app/temp/raw/<id> und unter dem AKTUELLEN
                          workDir -> Knopf erschien nicht
  retry_transcode         berechnete raw_dir aus demselben aktuellen workDir
                          -> haette am falschen Ort gesucht

Ursache in beiden Faellen: Der Rip war mit einer Wahl NUR FUER DIESEN RIP auf
die NAS gelegt worden (gibt es seit v3.15), die Einstellung selbst stand auf
leer. Damit zeigte nichts mehr auf die Datei. Wieder derselbe Fehler, den dieses
Projekt schon mehrfach bezahlt hat: aus einem Zustandswert (der heutigen
Einstellung) auf einen Mechanismus (wohin damals gerippt wurde) geschlossen,
statt nachzusehen.

Neues Modul rohdaten.py sucht jetzt an allen Orten, die ueberhaupt in Frage
kommen: Container-Standard, eingestelltes Arbeitsverzeichnis und jedes
Ablageziel unter /app/media. Der Suchraum ist geschlossen, weil
_arbeitsverzeichnis() im Worker nur diese zulaesst; verwechseln kann man nichts,
weil Roh-Verzeichnisse exakt wie die Job-ID heissen (vollstaendige UUID) und
fertige Ablagen "Titel (Jahr) [kurz-id]".

Nicht in die Job-Zeile geschrieben, obwohl das sauberer waere: create_all legt
nur fehlende TABELLEN an, keine Spalten - und Bestandsjobs (genau dieser Fall)
haetten den Wert ohnehin nicht.

11 Tests, darunter der echte Fall, ein toter CIFS-Mount und der Klassiker
"/app/media-boese".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 13:09:44 +02:00
HitonabiandClaude Opus 5 8b7bdfe798 feat(api): drei Endpunkte, die das Raten beenden - plus Restzeit in /jobs
GET /worker-setup/pfad-map - was ein externer Worker fuer RIPPY_PATH_MAP
eintragen muss, abgeleitet aus Rippys eigenen Mounts. DAS war der stille
Blocker: /app/media/... sind Container-Pfade, ein externer Worker sieht sie nur
uebersetzt, und dieses Mapping setzte NIEMAND. Externes Encoden konnte deshalb
nie funktionieren, obwohl das Celery-Routing einwandfrei arbeitete (Job
95afdc89: angenommen, 182 ms spaeter abgelehnt). Hat Rippy keine Freigabe, sagt
der Endpunkt das im Klartext samt Abhilfe - statt ein leeres Mapping zu liefern.

GET /presets - Preset-Namen der Worker plus Empfehlung MIT Begruendung. Die
Namen standen bisher fest verdrahtet an vier Stellen im UI.

GET /jobs/{id}/rohdaten + DELETE /jobs/{id}?rohdaten=true - was liegen bleibt,
wenn ein Job aus der Liste fliegt. Grund (v3.14): Entfernen loescht bewusst
keine Dateien, aber Job und Rohdaten haengen nur an der Job-ID - der Rohschnitt
ist danach UNERREICHBAR. Damals verwaisten so 75 GB unsichtbar, gefunden erst
per SSH.

/jobs liefert jetzt eta_sekunden + eta_text. Die Schaetzung entsteht in der API
und nicht im Browser, aus drei Gruenden: ein Seitenwechsel setzte die Messreihe
zurueck, zwei offene Tabs zeigten verschiedene Zahlen, und fuer einen externen
Encoder-Worker gaebe es gar keine - genau dort wollte der Commander sie sehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 12:58:06 +02:00
HitonabiandClaude Opus 5 35cfcbcb07 style: echte Umlaute im ganzen Projekt + Installer im Rippy-Look
Ampel / ampel (push) Successful in 27s
Commander-Rueckmeldung: Umlaute fehlen. Ausloeser war sichtbar der Installer -
"Diese Maschine uebernimmt die Video-Kompression fuer Rippy" stand woertlich im
Screenshot.

## Die .ps1-Falle war loesbar, nicht unumgehbar

Bisher galt: ausgelieferte .ps1 MUESSEN ASCII sein, weil PowerShell 5.1 sie
ohne BOM als ANSI liest. Das ist nur die halbe Wahrheit - gemessen mit echtem
powershell.exe 5.1:

    ohne BOM:  $s = "Größe: äöü"   + Unerwartetes Token -> Skript kaputt
    mit  BOM:  Groesse: aeoeue         + laeuft, Length 16 korrekt

Alle drei Skripte sind jetzt UTF-8 MIT BOM und tragen echte Umlaute; unter 5.1
gegengeprueft (BOM vorhanden, Parser fehlerfrei, Text korrekt gelesen). Der
Kopfkommentar sagt das jetzt richtig statt "ASCII-only".

## Umstellung: Text ja, Bezeichner nein

Umlaute gehoeren in Kommentare und Anzeigetexte, nicht in Funktionsnamen oder
Datenschluessel. Deshalb je Sprache das passende Werkzeug:

- Python: ueber den TOKENIZER - angefasst wurden ausschliesslich COMMENT- und
  STRING-Tokens. 156 Stellen. Code ist damit garantiert unberuehrt.
- TypeScript: nur // und /* */ Kommentare sowie JSX-Text (kann per Definition
  kein Bezeichner sein). 24 Stellen. `const waehlen`, `let laeuft`, `plaetze`,
  `GeraetInfo` sind nachweislich unversehrt.
- install.sh: Anzeigetext, aber die Shell-Funktionen (gruen/rot/gelb/titel) und
  der Schalter --nur-pruefen bleiben ASCII - das sind Schnittstellen.
- Markdown: 0 Aenderungen, die Doku hatte schon Umlaute.

ZWEI FEHLER MEINES KONVERTERS, beide von Werkzeugen gefangen:

1. In f-Strings steht in {...} CODE, kein Text. Aus f"{groesse}" wurde
   f"{größe}", waehrend die Variable groesse hiess - Ruff meldete F821
   "Undefined name". Der Konverter lagert Einsetzungen jetzt aus.
2. Ein Dict-Schluessel wurde umbenannt: die Wortliste enthaelt das PRAEFIX
   "uebersprung", der Tabu-Schutz prueft aber ganze Woerter. bericht[...] ist
   wieder ASCII - Umlaute in Datenschluesseln brechen JSON-Runden und DB-Felder.

## Installer im Rippy-Look

Statt hellgrau jetzt dieselben Toene wie das Web-UI (aus lib/design.ts
uebernommen): slate-900 Flaeche, dunkle Eingabefelder, Amber-Hauptknopf wie
"Los geht's" im Wizard. Oben ein Kopfbereich mit dem Farbverlauf
amber -> indigo -> purple und dem Disc-Symbol - in WinForms per Paint-Ereignis
gezeichnet, weil es dort keine Verlaeufe von der Stange gibt.

Geprueft, nicht gehofft: Das Fenster wurde headless in ein PNG gerendert
(DrawToBitmap) und angesehen - Verlauf, Symbol, Umlaute und Farben sitzen.
RippyWorkerSetup.exe neu gebaut (57344 -> 60416 Bytes); Umlaute und das
requireAdministrator-Manifest sind in der .exe verifiziert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 22:37:23 +02:00
HitonabiandClaude Opus 5 11597eb00c perf+cleanup(api): /capabilities war 1,0 s; vier tote Endpunkte entfernt
Ampel / ampel (push) Successful in 28s
Beides in main.py, deshalb ein Commit.

## 1. Das "Laggen" hatte genau eine Ursache

Gemessen ueber alle 15 Endpunkte, die das UI beim Laden braucht:

    /capabilities      1,010 s
    /system/updates    0,491 s   (haengt am Knopf, nicht am Seitenaufbau)
    /metadata/status   0,412 s   (dito)
    die anderen 12   < 0,025 s

/capabilities ist der einzige langsame, der beim SEITENAUFBAU zuschlaegt - und
fuenf Stellen holen ihn (Dashboard, Einstellungen, Worker-Tab, Wizard,
Rip-Dialog). Jede Seite zahlte eine Sekunde.

Die Ursache ist kein Fehler, sondern das Wesen des Celery-Pings: er sammelt
Antworten bis zum Timeout und kann nicht frueher aufhoeren, weil er nicht
weiss, wie viele Worker noch antworten wollen. Den Timeout zu kuerzen wuerde
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 (neben Disc-Watcher und
Key-Refresh, die es dort schon gibt), der Endpunkt liest nur ab. Vorrat aelter
als 30 s - Schleife noch nicht angelaufen oder gestorben - dann EINMAL synchron
pingen: lieber langsam als falsch ("alles offline", obwohl alles laeuft).

## 2. Vier tote Endpunkte raus

Jeder ein Ueberrest eines ersetzten Entwurfs, keiner mit Aufrufer (mechanisch
gegengeprueft: alle api.*-Aufrufe des UI gegen alle Routen):

  POST /prescan                  Metadaten-Vorschau-Seite ist seit v3.4 weg.
                                 Die PreScan-Klasse bleibt - sie hat 5 echte
                                 Fundstellen, der Watcher ruft sie im Prozess.
  POST /jellyfin/format          Macht seit v3.2 der Worker (medien.py), und
                                 zwar an der richtigen Stelle: er kennt den
                                 Ausgabeordner und ist nach dem Rip am Zug.
                                 Mit ihm fallen nfo_generator.py und
                                 image_downloader.py weg (sonst unbenutzt).
  GET  /stream/jobs              Der unangenehmste: erst Placebo, am 23.07.
                                 "repariert" statt entfernt - aber ein
                                 EventSource im UI gab es nie (das Dashboard
                                 nutzt setInterval(..., 4000)). Also keine
                                 harmlose Leiche, sondern eine Endlosschleife
                                 je Verbindung, die jeder aufmachen konnte.
  GET  /worker-setup/windows-gui Ohne Aufrufer seit die .exe den .bat-Umweg
                                 ersetzt hat (v3.9). install-gui.ps1 selbst
                                 lebt weiter, sie steckt in der .exe.

main.py: 1726 -> 1682 Zeilen, dazu 279 Zeilen in zwei geloeschten Modulen.

Tests halten beide Seiten fest: die vier Routen muessen WEG bleiben, und die
drei, an denen die Worker-Installation haengt (/worker-setup/paket, /windows,
/windows-exe), muessen DA sein. Ausserdem eine Doppelung entfernt - mein
eigener _sicherer_dateiname-Test aus dem Vorcommit pruefte dasselbe wie der
bestehende test_dateiname_validierung_blockt_pfad_tricks, und der war die
ganze Zeit korrekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:41:48 +02:00
HitonabiandClaude Opus 5 ef0a574a70 fix(worker,api): Platten-Schutz griff nicht, Fortschritt log, Auswurf tat nichts
Vier Funde aus der Durchsicht, alle auf der VM gemessen.

1. DER PLATTEN-SCHUTZ AUS c065967 WAR WIRKUNGSLOS

_original_aufheben() entschied per os.stat().st_dev, ob umgehaengt oder
kopiert werden muss. Im Worker-Container 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 Geraet. /app/temp
(Docker-Volume) und /app/media (Bind-Mount) sind zwei Mounts DERSELBEN
ext4-Partition. Die Pruefung sah "gleiches Dateisystem", uebersprang die
Platzpruefung, und shutil.move kopierte doch - 75 GB bei 37 GB frei. Der
Schutz haette genau den Schaden zugelassen, gegen den er gebaut wurde.

Jetzt wird os.rename VERSUCHT statt vorhergesagt: klappt es, ist es
umgehaengt und fertig; kommt EXDEV, steht die Kopie fest und ERST DANN wird
der Platz geprueft. Das ist keine Vermutung mehr, sondern die Antwort des
Kernels. Vier Tests in test_original_aufheben.py, darunter genau der Fall,
der die Platte fuellte. Die zwei alten Tests in test_medien.py sind dorthin
gewandert - sie taeuschten per gefaelschtem os.stat "verschiedene
Dateisysteme" vor, also genau die Annahme, an der der Schutz scheiterte.

2. DIE FORTSCHRITTSANZEIGE ZEIGTE DEN SCAN, NICHT DEN ENCODE

get_progress_from_line matchte jede Zahl vor einem Prozentzeichen. HandBrake
gibt Prozente aber in drei Phasen aus (Formatstrings aus dem Binary gelesen):

    Scanning title %d of %d, preview %d, %.2f %%          → laeuft VOR dem
                                                            Encode bis 100 %
    Encoding: task %d of %d, %.2f %%       (%.2f fps, avg  → der echte Wert
    Encoding: task %d of %d, Searching for start time, ... → Vorlauf

Dazu warf `if progress > 0` im Aufrufer jeden Wert unter 1,00 % weg. Live
beobachtet: Anzeige stand auf 99 %, der Encode bei 1,06 %; sie fiel erst auf
1, als der Encode die 1-%-Marke ueberschritt. Jetzt wird nur die
Encoding-Zeile gelesen, `task N of M` mitgerechnet (sonst springt die
Anzeige bei Zwei-Pass-Presets mitten in der Datei zurueck), und -1 heisst
"keine Angabe" - dasselbe Muster wie bei get_progress_from_prgv.

3. "AUTOMATISCHER AUSWURF" WURDE VON NIEMANDEM GELESEN

Die Einstellung (Standard: ein, "Disc nach erfolgreichem Ripping automatisch
auswerfen") kam in keiner Zeile Backend-Code vor. DVD/Blu-ray warfen deshalb
NIE aus, Audio-CDs IMMER, weil abcde `-x` fest verdrahtet bekam. Jetzt
entscheidet die Einstellung beides: wirf_disc_aus() per CDROMEJECT-ioctl
(fcntl-guarded, der native Windows-Worker laedt das Modul auch) und `-x` nur
noch, wenn gewuenscht.

4. PFAD-PRUEFUNG FIEL AUF PRAEFIX-NAMEN HEREIN

Elf Stellen prueften mit nacktem startswith(MEDIA_ROOT). "/app/media-boese/x"
beginnt mit "/app/media", liegt aber ausserhalb - 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: _zielbasis() benutzte os.path.normpath - unter Windows werden
daraus Backslashes, die MEDIA_ROOT-Pruefung greift nicht mehr, und das
gewaehlte Ziel faellt still auf den Standard zurueck. 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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:05:11 +02:00
HitonabiandClaude Opus 5 8bb075c656 feat(rip): Arbeitsverzeichnis je Rip waehlbar statt global vorgegeben
Ampel / ampel (push) Successful in 28s
Commander-Vorgabe 25.07.2026: "Du sollst es nicht automatisch setzen, es soll
auswaehlbar sein - z. B. beim Rippen starten, oder VORHER wenn Vollautomatik
eingeschaltet ist."

Bisher gab es nur die globale Einstellung workDir, und die war ein freies
Textfeld - man musste den Container-Pfad (/app/media/...) kennen. Beim
Akira-Rip landeten deshalb 74 GB Rohdaten auf der 148-GB-VM-Platte, obwohl
eine NAS-Freigabe mit 2,3 TB eingehaengt war.

- RipTargetModal: neue Auswahl "Arbeitsverzeichnis fuer die Rohdaten",
  gespeist aus /storage-targets (mit Kennzeichnung als Netzwerk-Freigabe und
  freiem Platz je Ziel). Leer = "Standard aus den Einstellungen", der Wert
  wird zur Orientierung mit angezeigt. Bei Musik ausgeblendet - CD-Rips gehen
  direkt als FLAC ins Ziel, ohne Roh-Zwischenstufe.
- POST /jobs nimmt work_dir entgegen, mit derselben Pfad-Haerte wie das Ziel
  (_validiere_ziel: muss unter /app/media liegen), und legt es in die
  Job-Metadaten.
- _arbeitsverzeichnis(einstellungen, job_wahl) im Worker: Wahl dieses Rips ->
  Setting -> Container-Default. Damit bleibt die Einstellung genau das, was
  bei Vollautomatik-Rips greift, weil dort niemand gefragt wird. Der Text im
  Einstellungen-Tab sagt das jetzt auch so.
- posixpath statt os.path in _arbeitsverzeichnis: das sind immer
  Container-Pfade, auch wenn ein nativer Windows-Worker das Modul laedt (der
  uebersetzt erst spaeter per pfad_lokal). os.path.normpath machte unter
  Windows Backslashes daraus, wodurch die MEDIA_ROOT-Pruefung nicht mehr
  griff - lokal als Testfehler aufgefallen.
- Test deckt die Reihenfolge und die Ausbruchsversuche ab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 19:59:37 +02:00
HitonabiandClaude Opus 5 f449c4ee34 fix(uhd): 4K-UHD geloest - makemkvcon holt Schluessel unter Linux nie
Ampel / ampel (push) Successful in 28s
Richtigstellung des Vortags-Befunds. Dort stand, MakeMKVs Schluessel-Kanal
sei abgeschaltet. Das war FALSCH: die Herleitung stuetzte sich auf zwei
Hostnamen aus alten Forumsbeitraegen (hkdata.fairuse.org,
hkdata.crabdance.com), die zwar wirklich nicht mehr aufloesen, von MakeMKV
aber laengst nicht mehr benutzt werden. Aufgedeckt durch den Einwand des
Commanders, unter Windows ginge es sofort.

Gegenprobe mit demselben Laufwerk und derselben Disc (Akira UHD, MKB v76):

                        Linux (Worker)      Windows
  Verbindungen          KEINE EINZIGE       185.84.108.20:443
  Meldung 3338          nie                 "Downloading latest HK"
  _private_data.tar     2048 B, 0 Keys      6,4 MB, 604 Keys
  Disc                  volume key unknown  TCOUNT:5, geht auf

Gegengeprueft mit leerem UND gefuelltem Speicher, mit und ohne --noscan,
mit dev:/dev/sr0 und disc:0, mit geloeschter update.conf. Linux fragt nie.
Die Meldungsvorlage "Downloading latest %1 to %2 ..." steckt sehr wohl im
Linux-Binary - sie loest nur nicht aus. Gleiches Symptom im MakeMKV-Forum,
seit Jahren offen (t=25782, t=34022). Der Dienst lebt; der Worker erreicht
185.84.108.20:443 sogar problemlos.

BEWIESEN: Nach Uebernahme des Windows-Schluesselspeichers oeffnet
makemkvcon auf der VM die Akira-UHD - "Operation successfully completed",
TCOUNT:5, fuenf Titel, identisch zum Windows-Ergebnis. Erster belegter
UHD-Disc-Zugriff auf der Rippy-Maschine.

- makemkv_daten.py (beide Zwillinge): zaehle_schluessel,
  private_data_pruefen, schluesselspeicher_status, private_data_schreiben.
  Die Pruefung lehnt einen Speicher OHNE hkd_*.bin ab - sonst laedt jemand
  den leeren Vorrat einer frischen Installation hoch, nichts aendert sich,
  und niemand versteht warum. Modulkopf komplett neu, inkl. der
  Fehldiagnose als Warnung fuer spaeter.
- API: GET/POST /system/keystore. Der Rohkoerper der Anfrage IST die Datei
  (binaer - JSON/Base64 waere Ballast, Multipart kann die API nicht).
  Groessengrenze 64 MB = client_max_body_size in nginx.conf.
- UI: neuer Block "Disc-Schluessel fuer 4K-UHD" UEBER dem KEYDB-Block, mit
  Schluessel-Anzahl, Upload und Anleitung fuer den Windows-Weg. KEYDB.cfg
  ist jetzt als Notnagel beschriftet. Worker-Plakette zeigt die Anzahl;
  0 heisst sichtbar "4K-UHD scheitert".
- tasks.py: UHD-Fehlertext sagt den Windows-Weg an und nennt die Anzahl
  bekannter Schluessel dieses Workers.
- caps.py meldet schluessel je Worker.
- Alle Falschaussagen korrigiert: UI (3), Anleitung (2), README (3),
  KONZEPT §8 + §10, Worker-Dockerfile, makemkv_key.py (dort stand "Den
  AACS-Schluessel zieht MakeMKV via LibreDrive ohnehin selbst aus dem
  Laufwerk" - gilt fuer Blu-ray, NICHT fuer UHD).
- SAVEPOINT v3.11, ROADMAP Etappe 18 (Etappe 17 mit Nachtrag), AGENTS.

Offen: voller UHD-Rip inkl. Transcode-E2E; und ob sich der Abruf unter
Linux doch anstossen laesst.

Quellen (AGENTS Regel D):
- Linux laedt keine Hashed Keys, gleiches Symptom:
  https://forum.makemkv.com/forum/viewtopic.php?t=25782
  https://forum.makemkv.com/forum/viewtopic.php?t=34022
- Schluessel als hkd_*.bin in _private_data.tar:
  https://forum.makemkv.com/forum/viewtopic.php?t=32675
- Meldungsformat: https://www.makemkv.com/developers/usage.txt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 11:18:09 +02:00
HitonabiandClaude Opus 5 0935766f61 feat(uhd): KEYDB.cfg-Unterstuetzung - MakeMKVs Schluessel-Kanal liefert nichts mehr
Ampel / ampel (push) Successful in 28s
4K-UHD scheiterte an "The volume key is unknown for this disc". Am 25.07.2026
im Worker-Container nachgemessen: Laufwerk und MakeMKV sind in Ordnung
(LibreDrive v06.3 / Meldung 1011, Disc wird gelesen, AACS-Dump geschrieben) -
MakeMKV versucht gar nicht erst, online einen Schluessel zu holen. Belege:
_private_data.tar enthaelt nur die Index-Datei und KEINE hkd_*.bin; auch mit
geloeschter update.conf (Meldung 5074 belegt den Web-Kontakt) und mit
app_UpdateEnable="1" kam keiner; hkdata.fairuse.org und hkdata.crabdance.com
loesen weltweit nicht mehr auf (NXDOMAIN gegen Fritz!Box, 8.8.8.8, 1.1.1.1).
Betroffen war Akira UHD (MKB v76, Pressung Dez. 2020) - also gerade KEINE
Neuerscheinung. Der bisherige Fehlertext ("Disc neuer als die
Schluessel-Datenbank, mit einem der naechsten Updates rippbar") war falsch.

Einziger heute funktionierender Weg ist eine vom Nutzer selbst mitgebrachte
KEYDB.cfg. Rippy liefert KEINE Schluessel mit, laedt keine herunter und
verteilt keine - es stellt nur den Platz bereit und zeigt an, was dort liegt.

- Datenverzeichnis persistent gemountet (MAKEMKV_DATA_HOST, Default
  /srv/rippy/makemkv): KEYDB.cfg und AACS-Dumps ueberleben jeden Rebuild.
  Vorher loeschte jeder "up -d --build" beides - inklusive des Dumps, auf den
  die Fehlermeldung selbst verwies.
- entrypoint.sh und tasks.py schreiben settings.conf ergaenzend statt
  zerstoerend. Der entrypoint bricht bei nicht beschreibbarem Verzeichnis
  nicht mehr ab - mit "restart: unless-stopped" waere das ein Crashloop
  gewesen, in dem auch reines DVD-Rippen tot ist.
- Neues Zwillings-Modul makemkv_daten.py (docker/api + docker/worker,
  byteweise identisch; test_zwillinge_sind_byteweise_identisch wacht darueber
  und wurde durch absichtliches Verstellen als wirksam nachgewiesen).
- API: GET/POST/DELETE /system/keydb, GET /system/aacs-dumps(/{dateiname}).
  JSON-Body statt Multipart - python-multipart ist bewusst nicht installiert
  und wuerde die API beim Import toeten. nginx client_max_body_size 64m,
  sonst scheitert der Upload mit 413, bevor die API ihn sieht.
- UI (Einstellungen -> System): Status, Hochladen per Datei-Dialog, Entfernen,
  Dump-Download, KEYDB-Plakette je Worker (nur wo das Verzeichnis wirklich
  gemountet ist - ein Remote-Transcode-Worker truege sonst eine Warnung,
  die ihn nichts angeht).
- parse_msg() + log_cb: MakeMKV-Meldungen landen im Rippy-Log (gedrosselt:
  Code 1003 raus, keine Wiederholungen, max. 40 je Rip). Nebenbei behoben:
  der alte Parser (split(",", 4)[3]) schnitt jede Meldung am ersten Komma ab.
- Fuenf Stellen richtiggestellt, die behaupteten, MakeMKV-Updates braechten
  die neueste Disc-Schluessel-Datenbank mit (UI, Anleitung, README,
  Worker-Dockerfile, makemkv_key.py).

NICHT bewiesen: ein erfolgreicher UHD-Rip - es lag keine KEYDB.cfg mit dem
Akira-Schluessel vor. Belegt sind der Befund und die neue Mechanik. So steht
es auch im SAVEPOINT und in der ROADMAP.

Quellen (AGENTS Regel D):
- Datenverzeichnis + Dateiname GROSS/case-sensitiv:
  https://forum.makemkv.com/forum/viewtopic.php?t=30636
- hkd_*.bin in _private_data.tar:
  https://forum.makemkv.com/forum/viewtopic.php?t=32675
- headless settings.conf / app_UpdateEnable:
  https://forum.makemkv.com/forum/viewtopic.php?t=20364
- KEYDB.cfg-Zeilenformat (libaacs):
  https://github.com/ShiftMediaProject/libaacs/blob/master/KEYDB.cfg
- MSG-/PRGV-Format: https://www.makemkv.com/developers/usage.txt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 01:26:08 +02:00
HitonabiandClaude Opus 4.8 f74f4e54f6 fix(api): Job-Meta (poster_path) an die Jobliste durchreichen
Ampel / ampel (push) Successful in 32s
GET /jobs nutzt response_model=List[Job]; das Job-Model hatte kein meta-Feld,
also schnitt FastAPI die Disc-Metadaten (poster_path) weg -> Dashboard.tsx
bekam job.meta = undefined -> Filmstreifen-Platzhalter statt TMDB-Poster,
sowohl in der Jobliste als auch im aktiven Rip-Header (beide aus /jobs).

Additiv: meta: Optional[Dict] ins Job-Model + in _job_row_to_model parsen
(json.loads wie im Detail-Endpunkt, defensiv gegen kaputtes JSON). Keine
UI-Aenderung noetig -- posterUrl() rendert dann die vorhandenen Poster.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 00:03:41 +02:00
HitonabiandClaude Opus 4.8 46a8f50c34 fix(api): remount nicht-blockierend (Netz-Mount darf API-Start nicht haengen)
Ampel / ampel (push) Successful in 28s
Vorfall 24.07.: Beim API-Start blockierte der synchrone CIFS-Schreibtest in
alle_remounten()/mounten() im Kernel (wait_for_response), als der SMB-Server
langsam war -> ~5 min "Waiting for application startup", kein Endpoint bedient
(bis der soft-Mount per Timeout abbrach). startup_event() lief isoliert sauber,
also war es der blockierende Netz-Mount, nicht die App-Logik.

Fix: remount als Hintergrund-Task (asyncio.create_task) statt await -> die API
kommt sofort hoch, die Mounts stellen sich her sobald der Server antwortet.
Test (test_api_smoke.py): haelt den Nicht-blockierend-Vertrag per Quelltext-
Inspektion fest, im Stil der anderen Verdrahtungs-Tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 21:50:32 +02:00
HitonabiandClaude Opus 4.8 3f47b504c2 feat(api): MakeMKV-Beta-Key automatisch erneuern
Ampel / ampel (push) Successful in 28s
Der kostenlose Beta-Key wechselt ~monatlich und laeuft zum Monatsende ab; bisher
musste er von Hand nachgetragen werden, sonst blockt Blu-ray-Ripping irgendwann
still. Neu: taeglicher Forum-Abgleich (t=1053) -> Key in die Settings
(makemkvAppKey). Der Worker liest ihn VOR jedem Rip (tasks.py) -> wirkt ohne
Rebuild ab dem naechsten Rip.

- docker/api/makemkv_key.py: extract_key (pure, testbar) + fetch_current_key +
  refresh_once/refresh_loop; loggt in die App-Logs (add_log).
- docker/api/main.py: refresh_loop als startup-Task.
- docker/api/test_makemkv_key.py: Parser-Tests - fingen den Bug, dass Beta-Keys
  laenger als 64 Zeichen sind (Regex {50,} statt {64}).

Betrifft nur die Software-Lizenz (oeffentlicher Gratis-Key), NICHT Disc-Schluessel
- die zieht MakeMKV via LibreDrive selbst. Ruff gruen, 4 Tests gruen, Live-Fetch
gegen das echte Forum verifiziert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 21:27:38 +02:00
HitonabiandClaude Fable 5 20cdfedb85 feat(installer): fertige .exe mit Rippy-Icon + ohne Konsolenfenster (statt .vbs)
Ampel / ampel (push) Successful in 28s
Commander-Einwand: .vbs ist abgekuendigt (Win11 24H2 Feature-on-Demand) und
wird oft als gefaehrlich geflaggt. Loesung: echte .exe.
- RippyWorkerSetup.exe (50 KB): install-gui.ps1 via ps2exe kompiliert —
  eingebettetes Rippy-Disc-Icon (deploy/worker-windows/rippy.ico), kein
  Konsolenfenster (-noConsole), WinForms (-STA). E2E getestet: Fenster
  oeffnet, conhost-Zaehler unveraendert (kein Konsolenfenster).
  Vorgebaut auf Windows (build-exe.ps1) + committet — eine Windows-.exe
  kann nicht von Linux (Rippy-Host) gebaut werden. WICHTIG: friert KEINE
  Versionen ein — HandBrake wird zur Laufzeit (GitHub latest) und der
  Worker-Code live von Rippy geholt; nur bei GUI-Aenderungen neu bauen.
- API: GET /worker-setup/windows-exe (FileResponse); Dockerfile kopiert die
  .exe ins Image. .gitattributes: *.exe/*.ico als binary.
- UI (Worker -> Windows): 'Installer herunterladen (.exe)' als Haupt-Weg
  (generisch, GUI fragt Adresse ab; die einzutragende IP wird als Hinweis
  angezeigt). .vbs-Erzeugung entfernt. CLI bleibt Profi-Alternative.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 19:15:42 +02:00
HitonabiandClaude Fable 5 4fb29d16cf feat(worker): grafischer Windows-Installer (GUI statt CLI)
Ampel / ampel (push) Successful in 28s
Commander-Wunsch: die CLI-Installation ist grausam. Jetzt:
- install-gui.ps1 (WinForms, ASCII-only): Fenster mit Feldern (Rippy-IP,
  Worker-Name, Autostart-Checkbox), Install-Knopf mit Live-Log, danach
  'Worker starten'-Knopf. Gleiche Schritte wie der CLI-Installer
  (Worker-Code + HandBrake latest + venv + Tray/Start/Uninstall).
  Braucht nur Python 3.10+, keine externe GUI-Runtime.
- API: GET /worker-setup/windows-gui serviert die GUI; Dockerfile kopiert
  sie ins worker_dist/.
- UI (Worker-Tab, Windows): 'Grafischen Installer herunterladen (.bat)' als
  Haupt-Weg — die .bat wird CLIENT-seitig mit der eingetragenen LAN-IP
  erzeugt (Blob-Download), Doppelklick laedt+startet die GUI von Rippy.
  Der CLI-Befehl bleibt als Profi-Alternative.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 18:47:57 +02:00
HitonabiandClaude Fable 5 dc1ceda0c2 fix(capabilities): Node-Zuordnung eindeutig bei mehreren Workern pro Host
Ampel / ampel (push) Successful in 28s
Laufen zwei Worker auf demselben Rechner (Befund 24.07.: alter + neuer
Windows-Worker auf TobisNicerPC), war die Node-Zuordnung mehrdeutig — der
Hostname-Match traf irgendeinen. Jetzt disambiguiert der Name-Teil des
Celery-Knotens (WORKER_NAME), sonst Fallback auf den ersten Host-Treffer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 18:33:22 +02:00
HitonabiandClaude Fable 5 4ba02047db Drei Praxis-Bugs: tote NAS-Mounts reparierbar, HandBrake-Versionen konsistent, Encoder-Wahl beim Rip
Ampel / ampel (push) Successful in 28s
1) Speicher-Mounts robust (Befund: toter CIFS-Mount nach NAS-Ausfall/Rebuild —
   mounted:false, verschwand aus 'Verfuegbare Ziele', Neu-Anlegen -> 409, man
   sass fest):
   - mounts.py: ist_erreichbar() (listdir, soft-Mount bricht schnell ab),
     ist_gemountet() faengt OSError toter Mounts, aushaengen() mit
     umount -l Fallback, reparieren() (lazy abhaengen + frisch mounten).
   - /storage-mounts liefert 'reachable'; POST bei existierendem Namen:
     aktiv -> 409, tot -> automatische Reparatur mit neuen Angaben;
     neuer POST /storage-mounts/{name}/repair (gespeicherte Zugangsdaten).
   - /storage-targets crasht nicht mehr an totem Mount (os.path.ismount
     OSError abgefangen).
   - UI: eigene 'Netzwerk-Mounts'-Liste mit Status (aktiv/nicht erreichbar/
     getrennt) + Reparieren- und Entfernen-Knopf — tote Mounts sind sichtbar
     und wiederherstellbar statt zu verschwinden.

2) HandBrake-Versionen konsistent (Befund: Docker 1.6.1, Windows-Skript
   fest 1.9.2, Update-Check meldet 1.11.2 — verwirrend):
   - Windows-Installer zieht jetzt DYNAMISCH die neueste Version (GitHub
     latest, Fallback 1.11.2) — passt zum Update-Check.
   - Update-UI erklaert klar: Docker = stabiles Debian-Paket (bewusst aelter,
     kein Fehler), Windows = neueste. MakeMKV-Update zeigt den Befehl.

3) Encoder-/Worker-Auswahl beim Rip (Feature):
   - Celery worker_direct=True: jeder Worker konsumiert zusaetzlich seine
     Direkt-Queue. API-Helper transcode_queue(node) routet gezielt an den
     gewaehlten Worker, faellt aber sicher auf die geteilte transcode-Queue
     zurueck, wenn er offline ist (kein Haengenbleiben).
   - /capabilities liefert den Celery-Node je Worker; POST /jobs nimmt
     transcode_node (-> Job-meta); rip_disc + retry-transcode routen danach.
   - Rip-Dialog: Encoder-/Worker-Dropdown, sichtbar ab 2 Online-Workern.
   - ping_worker-Task zum Verifizieren des gezielten Routings.
   - Nebenfund gefixt: DeviceDiscovery leitete die Titel-Auswahl (titles)
     gar nicht an die API weiter — jetzt titles + transcode_node.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 18:19:57 +02:00
HitonabiandClaude Fable 5 3987c31b1e Update-Check im System-Tab, Worker-Adressfeld mit Heimnetz-Warnung, Save-Knopf raus aus dem Worker-Tab
Ampel / ampel (push) Successful in 28s
1. Update-Erkennung (Commander-Frage 'wie kriegen wir Updates mit?'):
   GET /system/updates vergleicht installierte Versionen (Worker-Info) mit
   makemkv.com (Versionsnummer der Download-Seite) und der HandBrake-
   GitHub-Release-API (releases/latest), 12 h gecacht. Einstellungen ->
   System: 'Auf Updates prüfen' + fertiger Update-Befehl bei MakeMKV.
   Selbst-Update gibt es bewusst NICHT (waere Docker-Socket-Zugriff);
   dafuer ist MAKEMKV_VERSION jetzt per .env uebersteuerbar — Update ohne
   Code-Aenderung. HandBrake-Abweichung wird als Info dargestellt
   (Debian-Paket hinkt der offiziellen Version bewusst hinterher).
2. Worker-Anbindung: editierbares Adressfeld statt stillschweigend
   window.location.hostname — Befund: ueber die externe Domain kopierte
   Befehle liefen in den Reverse-Proxy (401) und Worker koennen Redis/
   Postgres eh nur im Heimnetz erreichen. Amber-Warnung bei oeffentlich
   aussehender Adresse, Anleitung ergaenzt.
3. 'Einstellungen speichern' ist im Worker-Tab ausgeblendet — dort gibt
   es nichts zu speichern, der Knopf verwirrte nur.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 15:39:20 +02:00