Compare commits

Author SHA1 Message Date
HitonabiandClaude Opus 5 f84c5ee399 docs: SAVEPOINT — 5.7.1 veröffentlicht (Tag aktuell, Assets 46/47/48, anonym gegengemessen)
Ampel / ampel (push) Successful in 1m16s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 01:09:33 +02:00
HitonabiandClaude Opus 5 fbedb9946d docs: SAVEPOINT — 5.7.1 gebaut und lokal installiert, NICHT veröffentlicht; Der-Patriot-Fund mit Ursache (VOB-Scan) und Fix (Stille-Grenze)
Ampel / ampel (push) Successful in 1m18s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 01:06:44 +02:00
HitonabiandClaude Opus 5 eb54c59141 fix(v5): 5.7.1 — Titel lesen wartet, solange MakeMKV arbeitet; Grenze gilt für Stille, Dialog zeigt Schritt, Prozent und Meldung
Ampel / ampel (push) Successful in 1m20s
WAS: Der Info-Lauf verlangt --progress=-same und wertet die Ausgabe
zeilenweise aus (PRGC/PRGT/PRGV/MSG). titelInfoLesen beendet makemkvcon
nur noch, wenn er so lange STILL ist, wie die Einstellung erlaubt
(Vorgabe 15 min) — jede Fortschritts- oder Meldungszeile setzt die Uhr
zurück. Kündigt MakeMKV den VOB-Scan an („IFO-Datei … beschädigt, die
VOB-Datei muss gescannt werden"), gilt nur noch eine Obergrenze von 4 h.
Der Dialog zeigt während des Lesens Schritt, Prozent und letzte Meldung
(titelLaufZwischenstand), beim VOB-Scan mit Erklärung (20–60 min, im
MakeMKV-Programm genauso). Bricht Rippy ab, stehen die letzten acht
Zeilen von makemkvcon im Fehler und im Protokoll. Einstellungs-Text
„Titel lesen — ohne Lebenszeichen höchstens". Tests mit nachgebautem
makemkvcon (redselig über die Stille-Grenze hinaus, stumm, VOB-Scan mit
Obergrenze). Version 5.7.1, Änderungsnotizen.
WARUM: Commander 12.09.2026 spät: „Der Patriot" (DVD, IFO für VTS #1
beschädigt) scheiterte in 5.7.0 erneut nach 15 Minuten, das MakeMKV-
Programm las die Disc fertig — MakeMKV liest bei so einer Disc die ganze
VOB durch, das dauert so lange wie die Disc braucht. Eine feste
Gesamtgrenze ist dafür immer zu kurz; Stille ist das richtige Maß.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 01:04:08 +02:00
HitonabiandClaude Opus 5 0088948b4f docs: SAVEPOINT — 5.7.0 veröffentlicht (Tag aktuell, Assets 43/44/45, anonym gegengemessen)
Ampel / ampel (push) Successful in 1m14s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:47:14 +02:00
HitonabiandClaude Opus 5 d8d34e8fbb docs: SAVEPOINT — 5.7.0 lokal installiert (22:41), Ampel grün, Update vorbereitet (Kanal-Stand gemessen), NICHT veröffentlicht
Ampel / ampel (push) Successful in 1m14s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:45:04 +02:00
HitonabiandClaude Opus 5 5353b381ba docs: SAVEPOINT — 5.7.0 gebaut und am Paket bewiesen, NICHT installiert, NICHT veröffentlicht; Durchsicht abgearbeitet, Spuren einzeln, F31 Ton verlustfrei; Ampel wartet hinter hängendem Lauf
Ampel / ampel (push) Successful in 1m16s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:35:21 +02:00
HitonabiandClaude Opus 5 9d16e82b5f test(v5): F2-Test ohne /proc — Nodes rekursives mkdirSync pendelt dort endlos, die Ampel hing
Ampel / ampel (push) Successful in 1m14s
WAS: Der Test „Ablage nicht erreichbar" nimmt jetzt einen Ordner UNTER
einer Datei als Ablage (scheitert auf jeder Plattform sofort mit
ENOTDIR/EEXIST) statt /proc/rippy-nicht-da unter Linux.
WARUM: Ampel-Lauf 279 (12.09.2026) blieb in test/pipeline.test.ts
hängen: auf procfs antwortet mkdir mit ENOENT, obwohl /proc da ist —
Nodes mkdirSync({recursive:true}) läuft dann zwischen Kind und Eltern-
ordner im Kreis. Lokal (Windows) war der Lauf grün, weil Q:\ dort nicht
existiert und Node am Wurzelpfad abbricht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 22:29:04 +02:00
23 changed files with 608 additions and 39 deletions
+277 -1
View File
@@ -1,6 +1,282 @@
# SAVEPOINT — Rippy # SAVEPOINT — Rippy
## ÜBERGABE an die nächste Session (03.09.2026 spät — 5.6.1 gebaut und lokal installiert, NICHT veröffentlicht: Untertitel-Spuren wieder erkannt) ## ÜBERGABE an die nächste Session (13.09.2026 nachts — 5.7.1 VERÖFFENTLICHT im Tag `aktuell`: Titel lesen wartet, solange MakeMKV arbeitet — Der Patriot)
### Stand
**5.7.1 ist gebaut, am ausgepackten Paket bewiesen, auf DIESEM PC
installiert (13.09.2026 01:06, Installer-Exit 0, SHA-256 vorher geprüft,
Rippy lief dabei nicht; `Rippy.exe` 5.7.1 auch über `\\localhost\C$`)
und seit 13.09.2026 ~01:15 IM KANAL `aktuell`.**
**Veröffentlicht (13.09.2026 nachts, Commander: „Update freigegeben"):**
Release 1, Assets 46 (`RippySetup-5.7.1.exe`, 131 576 689 Bytes, HTTP 201),
47 (Blockmap, 137 575 Bytes, 201), 48 (`latest.yml`, 14 146 Bytes, 201 —
das alte Asset 45 vorher gelöscht, 204); Release umbenannt in „Rippy 5.7.1
(Update-Kanal aktuell)" mit den 5.7.1-Notizen (PATCH 200). **Anonym
extern gegengemessen:** `latest.yml` HTTP 200, 14 146 Bytes, Byte für Byte
gleich dem Bau (`version: 5.7.1`), Setup HTTP 200 mit Content-Length
131 576 689, Blockmap 200 mit 137 575. 5.7.0 (43/44) und alle älteren
Setups liegen weiter im Release, `w0.yml` (36) unangetastet. Der andere
PC holt sich 5.7.1 über die Update-Funktion. Weg wie bei 5.7.0: die
API-Schritte einzeln als PowerShell-Befehle (Token nie ausgegeben).
* Setup: `RippySetup-5.7.1.exe`, **131 576 689 Bytes**, SHA-256
`7463711833721C2068270DE39B43F0ABE433891EA10030AD2ECB21BF8480F669`
(Bau in `dist-setup-571/`, Kopie in `dist-setup/`; Blockmap 137 575 B,
`latest.yml` 14 146 B).
* Paket-Smoke: `SMOKE: OK — fenster=geladen kern=pid:6408,node:24.18.1 datenbank=ok leine=gesetzt pong=ok version=5.7.1`.
* **518 Tests** in 52 Dateien (5.7.0: 508), drei Typprüfungen, Bau, Klick-
Beweis (34 Schritte) und Ampel (`eb54c59`) grün.
* Commits: `eb54c59` (Fix, Tests, Notizen, Version 5.7.1, Klick-Bilder)
und dieser SAVEPOINT.
### Der Fund des Commanders (12.09.2026 spät): „Der Patriot" scheitert in 5.7.0 erneut
Bild: Dialog mit „✗ MakeMKV hat die Titel nach 15 Minuten nicht gelesen".
Daneben das MakeMKV-Programm (v1.18.4, LibreDrive), das dieselbe DVD
„The Patriot Extended Cut" fertig liest — sein Protokoll:
```
IFO-Datei für VTS #1 ist beschädigt, die VOB-Datei muss gescannt werden.
Das kann viel Zeit in Anspruch nehmen, bitte haben Sie Geduld.
CELLs 1-53 wurden vom Titelstart entfernt … Titel #1 wurde hinzugefügt (31 cell(s), 2:47:40)
… Titel #11 (0:09:46), #16 (0:09:58), #17 (0:02:01) · Die Aufgabe wurde erfolgreich abgeschlossen
```
**Ursache:** MakeMKV liest bei beschädigter IFO die ganze VOB-Datei
(5,8 GB) durch — so lange, wie das Laufwerk für die Disc braucht, im
Programm genauso. Rippys Grenze war eine feste GESAMTgrenze (5.6.1: 5 min,
5.7.0: 15 min) — für diese Disc immer zu kurz. Kandidat 2 der Durchsicht,
jetzt mit Beleg.
**Fix (5.7.1):**
* `buildInfoArgs` gibt `--progress=-same` mit; `titelInfoLesen` wertet die
Ausgabe zeilenweise aus (PRGV Prozent, PRGC Schritt, PRGT Aufgabe, MSG
Meldung) und beendet makemkvcon nur, wenn er so lange STILL ist, wie
die Einstellung erlaubt (`titelLesenMinuten`, Vorgabe 15) — jede Zeile
setzt die Uhr zurück. Kündigt MakeMKV den VOB-Scan an
(`istVobScanMeldung`: IFO + VOB im Text, deutsch wie englisch), gilt nur
noch die Obergrenze `VOB_SCAN_HOECHSTENS_MS` (4 h).
* Der Dialog zeigt währenddessen `titelLaufZwischenstand`: „MakeMKV liest
seit 17 min · Öffnen der DVD-Disc 43 % · letzte Meldung: „…" — beim
VOB-Scan mit der Erklärung (2060 min, im Programm genauso). Neue
Meldung/Schritt sofort (ab 30 s), Prozente höchstens alle 5 s, sonst
jede Minute.
* Bricht Rippy ab, stehen die letzten acht Zeilen von makemkvcon im
Fehler und im Protokoll („Titel-Lauf G gescheitert: … [makemkvcon info:
kein Lebenszeichen seit 900 s — letzte Zeilen: …]").
* Einstellungen → Allgemein heißt jetzt „Titel lesen — ohne Lebenszeichen
höchstens" (Klick-Beweis angepasst).
* Tests `test/lebenszeichen-571.test.ts` mit nachgebautem makemkvcon
(Node-Skript über aufruf.ts): redselig über die Stille-Grenze hinaus →
Titel kommt; stumm → „kein Lebenszeichen seit 1 s" mit letzten Zeilen;
VOB-Scan mit Obergrenze.
**Nicht gemessen (kein Patriot hier):** ob makemkvcon während des
VOB-Scans PRGV-Zeilen schickt. Deshalb die doppelte Absicherung: schickt
er Fortschritt, hält die Stille-Uhr ihn am Leben; schickt er nichts, hält
ihn die VOB-Ankündigung am Leben (Obergrenze 4 h). Der Commander sieht im
Dialog, was passiert — und das Protokoll trägt die letzten Zeilen.
### Was am Gerät zu prüfen ist
* **Der Patriot noch einmal:** Der Dialog muss nach ~1 min „MakeMKV liest
seit … · … letzte Meldung: „IFO-Datei … beschädigt …"" zeigen und
irgendwann die vier Titel (2:47:40, 9:46, 9:58, 2:01). **Wie lange das
MakeMKV-Programm braucht, bitte stoppen** — steht die Zeit über 15 min,
ist die Ursache bestätigt. Danach der Rip: MakeMKV scannt die VOB beim
Rippen noch einmal (gleiche Dauer), die Rip-Phase hat keine Zeitgrenze.
* Die Punkte aus der 5.7.0-Übergabe darunter (Spuren einzeln an der
Kizuna-Disc, Ton-Codec statt AAC, Schlüssel-Karte, anderer PC).
## Übergabe vom 12.09.2026 spät (5.7.0 — veröffentlicht im Tag `aktuell`; überholt durch 5.7.1)
### Wo die nächste Session startet
* **Arbeitsort:** `F:\Coding Stuff\rippy`, Branch `worktree-windows-electron`
(Spitze `9d16e82` plus dieser SAVEPOINT). v5-Code in `rippy-windows/`.
* **Die drei Läufe:** `npm test` (508 Tests in 51 Dateien, 13 übersprungen),
`npm run smoke`, `npm run klick` (34 Klick-Schritte, Bilder
`beweise/klick/*.png``02-dialog-vorwahl.png` zeigt die Spur-Haken).
Alle drei in dieser Session grün; dazu die Messung
`RIPPY_MESSUNG=1 npx vitest run test/messung.spuren.test.ts` am
mitgelieferten HandBrake (braucht ffmpeg im PATH).
* **Release veröffentlichen — für 5.7.0 ERLEDIGT (12.09. spät, Commander:
„Update freigegeben"). Der Weg fürs nächste Mal, wieder NUR nach seinem
Wort:** Dateien im Gitea-Tag `aktuell` ERSETZEN (Setup, `.blockmap`,
`latest.yml`; `w0.yml` bleibt liegen), ERST Setup und Blockmap
hochladen, DANN `latest.yml` löschen (204) und neu hochladen (201),
Release PATCHen, anonym über
`https://git.tobisniceshomelab.ddnsfree.com/Hitonabi/rippy/releases/download/aktuell/`
gegenmessen — Token `F:\Coding Stuff\Gittea Token\rippy.txt` über das
PowerShell-Werkzeug lesen (nie ausgeben), LAN-Adresse
`http://192.168.178.153:3000/api/v1/repos/Hitonabi/rippy/releases/1`,
Upload per `Invoke-WebRequest -Form @{ attachment = Get-Item … }`. Ein
Skript mit allen Schritten blockt der Auto-Modus-Wächter — die Schritte
einzeln als Befehle gehen. Dateien in `rippy-windows/dist-setup-570/`
(Original) und `rippy-windows/dist-setup/` (Kopie). **Im Kanal liegt
5.7.0** (5.6.1 wurde nie hochgeladen).
### Stand
**5.7.0 ist gebaut, am ausgepackten Paket bewiesen und auf DIESEM PC
installiert (12.09.2026 22:41, Installer-Exit 0, SHA-256 vorher geprüft,
Rippy lief dabei nicht; `Rippy.exe` Version 5.7.0 auch über
`\\localhost\C$` gesehen) und seit 12.09.2026 ~22:55 IM KANAL `aktuell`.**
**Veröffentlicht (12.09.2026 spät, Commander: „Update freigegeben"):**
Release 1, Assets 43 (`RippySetup-5.7.0.exe`, 131 575 882 Bytes, HTTP 201),
44 (Blockmap, 137 533 Bytes, 201), 45 (`latest.yml`, 13 433 Bytes, 201 —
das alte Asset 42 vorher gelöscht, 204); Release umbenannt in „Rippy 5.7.0
(Update-Kanal aktuell)" mit den 5.7.0-Notizen (PATCH 200, 4 403 Zeichen).
**Anonym extern gegengemessen:** `latest.yml` HTTP 200, 13 433 Bytes, Byte
für Byte gleich dem Bau (`version: 5.7.0`), Setup HTTP 200 mit
Content-Length 131 575 882, Blockmap 200 mit 137 533. Die alten Setups
5.1.05.6.0 liegen weiter im Release, `w0.yml` (36) unangetastet. Der
zweite PC holt sich 5.7.0 über die Update-Funktion (5.6.0 → 5.7.0). Der
Auto-Modus-Wächter blockte den Start des Upload-Skripts; die API-Schritte
liefen einzeln als PowerShell-Befehle (Token nie ausgegeben).
* Setup: `RippySetup-5.7.0.exe`, **131 575 882 Bytes**, SHA-256
`006B761EC2552637A4D1294C066B62B594A3B8FF08BB658115FFCD429D4AC185`
(Blockmap 137 533 Bytes, `latest.yml` 13 433 Bytes mit den 5.7.0-Notizen).
* Paket-Smoke: `SMOKE: OK — fenster=geladen kern=pid:37052,node:24.18.1 datenbank=ok leine=gesetzt pong=ok version=5.7.0`.
* **508 Tests** in 51 Dateien (5.6.1: 450), drei Typprüfungen, Bau und
Klick-Beweis (34 Schritte) grün.
* **Ampel GRÜN** für `9d16e82` (Lauf 442) und `5353b38` (SAVEPOINT) —
nachdem der Commander den hängenden Lauf 279 abgebrochen hatte (22:38).
* **Update vorbereitet:** idempotentes Skript
`<Scratchpad>\veroeffentlichen-570.ps1` (Probe ohne `-Los`, Upload mit
`-Los`: ERST Setup + Blockmap, DANN latest.yml löschen/hochladen, Release
PATCHen, anonym gegenmessen; kein Remove-Item; Token nie ausgegeben —
der Auto-Modus-Wächter blockt den Start des Skripts, die Schritte
einzeln als PowerShell-Befehle sind der Weg). Kanal-Stand gemessen 22:45:
Release 1 „Rippy 5.6.0 (Update-Kanal aktuell)", 24 Assets, zuletzt 40
(`RippySetup-5.6.0.exe`), 41 (Blockmap), 42 (`latest.yml`, 8 273 B);
extern `latest.yml` HTTP 200 mit `version: 5.6.0`. Hochzuladen:
`dist-setup-570\RippySetup-5.7.0.exe` (131 575 882 B), `.blockmap`
(137 533 B), `latest.yml` (13 433 B, sha512 `VL9GcKxP…XdeXDg==`).
* Der frühere Hänger: Lauf 279 (`d457600`) blieb in `test/pipeline.test.ts` HÄNGEN —
der neue F2-Test nahm unter Linux `/proc/rippy-nicht-da` als tote
Ablage; auf procfs antwortet mkdir mit ENOENT, obwohl `/proc` da ist,
und Nodes `mkdirSync({recursive:true})` pendelt endlos (synchron, kein
Test-Timeout greift). Fix `9d16e82` (Ordner unter einer Datei → ENOTDIR,
sofort) ist gepusht; sein Lauf wartet, bis der hängende Lauf endet
(Gitea-Zeitlimit 3 h, ab 22:14) oder jemand ihn in Gitea abbricht
(Actions → #279 → Abbrechen; per API gibt es kein Cancel in 1.27).
Seit 22:41 erledigt (siehe oben).
* Commits dieser Session (alle auf `worktree-windows-electron`):
`6f1add4` Bericht der Durchsicht · `12b9720` F1/F5/F12/F13/A2 Schlüssel ·
`8c4456d` F2/F4/F6/F8/F10 kein stilles Scheitern · `904e4b2` F3/F9
Serien und Zwischenname · `551f1d7` F7 Fortschritt · `783b8e4` F11
Laufwerks-Gesundheit · `c1ee339` Kleinkram F15F30/A5 · `3dacd7c` F14
Erkennung · `ae82d8c` F16 Installer-Rückfrage · `6b6e5c3` Titel lesen
15 min · `0fc87aa` 4K nach Leistung · `fc2d16c` Disc-Typ aus dem
Dateisystem · `e7e33e9` Spuren einzeln + F31 · `d457600` Version 5.7.0 ·
`9d16e82` F2-Test ohne /proc.
### Was 5.7.0 enthält — die Durchsicht vom 12.09.2026, abgearbeitet
Der Bericht (`beweise/berichte/2026-09-12-durchsicht.md`, 31 Funde, 8
Auffälligkeiten, 5 Kandidaten für „Der Patriot / Déjà Vu / Training Day"
auf dem anderen PC) ist komplett umgesetzt — nach den Entscheiden des
Commanders vom selben Abend:
1. **F1 Beta-Key mit `@`** (wahrscheinlichste Ursache der drei Filme):
`KEY_MUSTER = /T-[^\s<>"'&;]{50,}/`, „end of September 2026" wird
gelesen, ein angenommener Schlüssel bleibt stehen, Ablehnung ROT,
„nicht prüfbar" (grau) ohne Laufwerk. **Vom anderen PC noch zu holen:**
Protokoll und Stand der Schlüssel-Karte NACH dem Update — dann ist der
Kandidat bestätigt oder verworfen.
2. **Kandidat 2 → Titel lesen 15 Minuten + Regler** (Einstellungen →
Allgemein, 560), Zwischenstand je Minute im Dialog, „Abbrechen"
beendet den Lauf im Kern, Meldung nennt Kopierschutz/Playlisten.
3. **Jede Spur einzeln anhaken** („Jede Spur einzeln anhaken
(Recommended)"): Dialog mit Ton- und Untertitel-Zeilen je Titel
(`gemeinsam/spuren.ts`, `SpurZeilen` im Dialog), Vorwahl je Extra als
Spur, Parser liest SINFO 2/22, abgeleitete Spuren (0x800) — DTS-Kerne
versteckt, „nur erzwungene" sichtbar; Ablage findet die Spuren im
HandBrake-Scan der Roh-Datei wieder (`spurenZuordnen`, gestuft) und
gibt `--audio n,n --subtitle n,n --subtitle-default=1`; MakeMKV-Regel
jetzt `-sel:all,+sel:all,-sel:havecore,=100:all` (Stereo-Doppel und
Kommentare bleiben), alte Rippy-Regel wird ersetzt, fremde bleibt.
4. **4K nach der Leistung** („Ja, genau so"): GPU mit 4K-Preset →
komprimieren (8 Bit), nur CPU → verlustfrei; AV1 10 Bit als eigene Wahl.
5. **F10 Disc-Typ aus dem Dateisystem**: `INDX0300` = UHD (am Akira-Abbild
gemessen), `0100/0200` = Blu-ray, `VIDEO_TS` = DVD.
6. **F16 Installer aus dem Archiv nur nach Rückfrage** („Archiv nur nach
Rückfrage"): Frage in der MakeMKV-Karte, Größe + SHA-256 im Bericht.
7. **F31 (NEU, beim Bau gemessen): Ton war seit 5.0 nie verlustfrei.**
`--aencoder copy` ohne `--audio-copy-mask` → HandBrakeCLI 1.11.2 kopiert
nur AAC, rechnet AC3/DTS/TrueHD um. Jetzt Maske
`aac,ac3,eac3,dtshd,dts,mp3,truehd,flac` auf beiden Wegen. **Dem
Commander sagen:** Filme, deren Roh-Dateien nach der Kompression
gelöscht wurden, tragen AAC — bei wichtigem HD-Ton neu rippen.
8. Alle übrigen Funde F2F30 und A5 (siehe Commit-Liste oben; jeder mit
Test).
### Messungen dieser Session (Regel D)
* HandBrakeCLI 1.11.2 (vendor), Quelle per ffmpeg (jpn 5.1 / deu 2.0 /
deu 2.0 „Kommentar" AC3, deu/eng SRT), Preset „H.265 MKV 1080p30":
`--audio 1,3` → jpn + deu-Kommentar; `--subtitle 2,1 --subtitle-default=1`
→ eng (default) vor deu; `--audio none --subtitle none` → nur Video;
ohne `--audio-copy-mask` Passthru-Zeile „allowed codecs are AAC" und
Ausgabe AAC; mit Maske „AAC, AC3, E-AC3, TrueHD, DTS, DTS-HD, MP3, FLAC"
und Ausgabe AC3. Presets-Export: alle drei Produktions-Presets tragen die
volle CopyMask — die CLI verengt sie trotzdem.
* HandBrake-Scan-JSON: `AudioList[].CodecName` („dtshd", „dca", „ac3"),
`ChannelCount`, `ChannelLayout`; `SubtitleList[].Source` (Zahl),
`SourceName` („UTF-8", „PGS"), `Format`.
* HandBrake 4K (RX 9070 XT): `vce_h265` 8 Bit läuft, `vce_h265_10bit`
Segfault (Profil auto und main10), `vce_av1_10bit` läuft.
* `BDMV/index.bdmv` am Akira-UHD-Abbild: `INDX0300`.
* `QueryDosDeviceW`: C: → `\Device\HarddiskVolume7`.
* Electron 44 utilityProcess überlebt unbehandelte Promise-Ablehnung.
* Forum t=1053: Key 68 Zeichen mit `@`, „valid until end of September 2026".
* Echte `rippy.db` über `\\localhost\C$`: Schema 4 — der Datenbank-Befund
vom 01.09. war die MSIX-Schattensicht dieses Terminals (A1).
* Nodes `mkdirSync({recursive:true})` auf procfs: Endlosschleife (Ampel-Lauf 279).
### ⚠ Am Gerät noch zu messen
* **Spuren einzeln an einer echten Disc** (Kizuna: DTS-HD MA deu/jpn,
drei PGS deu): Dialog-Zeilen, DTS-Kern unsichtbar, Roh-Datei enthält
die Stereo-Fassung (neue MakeMKV-Regel — beim ersten Start von 5.7.0
meldet die Werkzeug-Karte „Auswahlregel erneuert"), HandBrake-Ausgabe
mit genau den gewählten Spuren, Ton-Codec in der fertigen Datei DTS-HD
MA statt AAC (F31).
* Gewöhnliche Blu-ray: `index.bdmv` muss `INDX0200` (oder 0100) tragen.
* 4K-Disc am Laufwerk: Typ `uhd`, Preset „H.265 VCN 2160p 4K", Encode
läuft durch (Messung nur am Abbild).
* Titel lesen > 5 min an einer zähen DVD; Abbrechen gibt das Laufwerk frei.
* Rettungs-Abbild an einer AACS-Disc (nie gemessen, siehe 5.5.0).
* Der andere PC: Protokoll + Schlüssel-Karte nach dem Update (Kandidat 1).
### Offene Fragen an den Commander
* **4K-Vorgabe:** 8-Bit-HEVC-Hardware (läuft) oder AV1 10 Bit (läuft,
braucht AV1-fähige Abspielgeräte)? Heute: HEVC 8 Bit vorgegeben, AV1
wählbar.
* **Alte Filme mit AAC-Ton (F31):** neu rippen oder so lassen?
### Bekannte Schwächen, bewusst in Kauf genommen
* Die Spur-Zuordnung Roh-Datei ↔ Dialog läuft über Sprache + Codec-Familie
+ Kanäle (MakeMKV nummeriert anders als HandBrake); bei zwei identischen
Spuren gleicher Sprache, gleichen Codecs und gleicher Kanalzahl
entscheidet die Reihenfolge. Was nicht gefunden wird, steht als Hinweis.
* Keine gewählte Tonspur → HandBrakes erste Spur (kein stummer Film); der
Dialog sagt es.
* Die Automatik (ohne Dialog) rippt weiter nach Sprachlisten, nicht nach
Spuren.
* 10-Bit-HEVC per VCE stürzt in HandBrake 1.11.2 ab — 4K bleibt 8 Bit,
bis ein neueres HandBrake mitgeliefert wird.
* Der Kanal trägt 5.7.0; der andere PC (5.6.0) holt sie über das Update,
dieser PC hat sie per Installer.
## Übergabe vom 03.09.2026 spät (5.6.1 — gebaut und lokal installiert, nie veröffentlicht; überholt durch 5.7.0)
### Stand ### Stand
Binary file not shown.

Before

Width:  |  Height:  |  Size: 288 KiB

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 752 KiB

After

Width:  |  Height:  |  Size: 759 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 753 KiB

After

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 784 KiB

After

Width:  |  Height:  |  Size: 791 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 KiB

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 468 KiB

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 KiB

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 445 KiB

After

Width:  |  Height:  |  Size: 445 KiB

+2 -2
View File
@@ -308,8 +308,8 @@ await schritt('zurück in den Kinosaal (dunkel)', async () => {
await seite.locator('html[data-design="dunkel"]').waitFor() await seite.locator('html[data-design="dunkel"]').waitFor()
}) })
await schritt('5.7.0: „Titel lesen — höchstens" steht auf 15 Minuten, 30 lässt sich wählen', async () => { await schritt('5.7.0: „Titel lesen — ohne Lebenszeichen höchstens" steht auf 15 Minuten, 30 lässt sich wählen', async () => {
const regler = seite.locator('#allgemein').getByLabel('Titel lesen — höchstens') const regler = seite.locator('#allgemein').getByLabel('Titel lesen — ohne Lebenszeichen höchstens')
if ((await regler.inputValue()) !== '15') throw new Error(`Vorgabe ist ${await regler.inputValue()}, nicht 15`) if ((await regler.inputValue()) !== '15') throw new Error(`Vorgabe ist ${await regler.inputValue()}, nicht 15`)
await regler.selectOption('30') await regler.selectOption('30')
// Der Wert geht zum Kern und kommt als Einstellung zurück — erst dann // Der Wert geht zum Kern und kommt als Einstellung zurück — erst dann
+13
View File
@@ -1,3 +1,16 @@
Version 5.7.1
Titel lesen: Rippy wartet, solange MakeMKV arbeitet.
"Der Patriot" (DVD mit beschaedigter IFO-Datei) scheiterte in 5.7.0 nach
15 Minuten, waehrend das MakeMKV-Programm die Disc fertig las: MakeMKV
liest bei so einer Disc die ganze VOB-Datei durch, und das dauert so lange
wie das Lesen der Disc. Die Grenze aus den Einstellungen gilt jetzt fuer
Stille - solange MakeMKV Fortschritt oder Meldungen schickt, wartet Rippy
(kuendigt MakeMKV den VOB-Scan an, bis zu vier Stunden). Der Dialog zeigt
dabei, was MakeMKV gerade tut: Schritt, Prozent und letzte Meldung.
"Abbrechen" beendet den Lauf jederzeit. Bricht Rippy doch ab, stehen die
letzten Zeilen von MakeMKV im Protokoll.
Version 5.7.0 Version 5.7.0
Der Beta-Schluessel kommt ganz an. Der Beta-Schluessel kommt ganz an.
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "rippy", "name": "rippy",
"version": "5.7.0", "version": "5.7.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "rippy", "name": "rippy",
"version": "5.7.0", "version": "5.7.1",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"electron-updater": "^6.8.9", "electron-updater": "^6.8.9",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "rippy", "name": "rippy",
"productName": "Rippy", "productName": "Rippy",
"version": "5.7.0", "version": "5.7.1",
"description": "Rippy v5 — Disc-Ripping als eigenständiges Windows-Programm (KONZEPT-WINDOWS.md)", "description": "Rippy v5 — Disc-Ripping als eigenständiges Windows-Programm (KONZEPT-WINDOWS.md)",
"main": "out/haupt/index.js", "main": "out/haupt/index.js",
"author": "KrBrZ", "author": "KrBrZ",
@@ -579,7 +579,7 @@ export function AllgemeinKarte({ verbindung, haupt }: { verbindung: Verbindung;
<SektionsKarte id="allgemein" titel="ALLGEMEIN" intro="Rippy lebt im Infobereich neben der Uhr: Fenster zu heißt weiterlaufen, beendet wird über das Tray-Menü."> <SektionsKarte id="allgemein" titel="ALLGEMEIN" intro="Rippy lebt im Infobereich neben der Uhr: Fenster zu heißt weiterlaufen, beendet wird über das Tray-Menü.">
<Schalter an={haupt?.autostart === true} aufWechsel={(an) => void window.rippy.autostartSetzen(an)} label="Rippy mit Windows starten (Autostart im Benutzerkonto kein Dienst, keine Adminrechte)" /> <Schalter an={haupt?.autostart === true} aufWechsel={(an) => void window.rippy.autostartSetzen(an)} label="Rippy mit Windows starten (Autostart im Benutzerkonto kein Dienst, keine Adminrechte)" />
<label className="block max-w-sm"> <label className="block max-w-sm">
<span className="text-xs font-semibold text-dunst">Titel lesen — höchstens</span> <span className="text-xs font-semibold text-dunst">Titel lesen — ohne Lebenszeichen höchstens</span>
<select value={werte['titelLesenMinuten'] ?? '15'} onChange={(e) => setzen('titelLesenMinuten', e.target.value)} className={FELD_KLASSE}> <select value={werte['titelLesenMinuten'] ?? '15'} onChange={(e) => setzen('titelLesenMinuten', e.target.value)} className={FELD_KLASSE}>
<option value="5">5 Minuten</option> <option value="5">5 Minuten</option>
<option value="10">10 Minuten</option> <option value="10">10 Minuten</option>
@@ -587,7 +587,7 @@ export function AllgemeinKarte({ verbindung, haupt }: { verbindung: Verbindung;
<option value="30">30 Minuten</option> <option value="30">30 Minuten</option>
<option value="60">60 Minuten</option> <option value="60">60 Minuten</option>
</select> </select>
<span className="mt-1 block text-[11px] leading-4 text-nebel">So lange darf MakeMKV die Titel einer Disc lesen. Kopiergeschützte DVDs und Discs mit vielen Playlisten brauchen im MakeMKV-Programm oft länger als fünf Minuten — braucht eine Disc dort länger als hier eingestellt, erhöhe den Wert.</span> <span className="mt-1 block text-[11px] leading-4 text-nebel">So lange darf MakeMKV still sein (kein Fortschritt, keine Meldung), bevor Rippy das Titel-Lesen beendet. Solange MakeMKV arbeitet und Fortschritt meldet — etwa beim Durchlesen einer DVD mit beschädigter IFO-Datei, was 2060 Minuten dauern kann — wartet Rippy; „Abbrechen" im Dialog geht jederzeit.</span>
</label> </label>
<label className="block max-w-sm"> <label className="block max-w-sm">
<span className="text-xs font-semibold text-dunst">Erscheinungsbild</span> <span className="text-xs font-semibold text-dunst">Erscheinungsbild</span>
+50 -14
View File
@@ -16,7 +16,7 @@ import { discFingerprint, discTitel, type DiscTitel } from '../metadaten/discmer
import { OmdbClient } from '../metadaten/omdb' import { OmdbClient } from '../metadaten/omdb'
import { TmdbClient } from '../metadaten/tmdb' import { TmdbClient } from '../metadaten/tmdb'
import { LEERER_TREFFER, ausDetails, nachpruefen, zuordnen, type Zuordnung } from '../metadaten/zuordnung' import { LEERER_TREFFER, ausDetails, nachpruefen, zuordnen, type Zuordnung } from '../metadaten/zuordnung'
import { ABGELOEST, titelInfoLesen } from '../rip/makemkv' import { ABGELOEST, titelInfoLesen, type Lebenszeichen } from '../rip/makemkv'
import { spurenFuerTitel, sprachenFuerTitel, type TitelInfo } from '../rip/parser' import { spurenFuerTitel, sprachenFuerTitel, type TitelInfo } from '../rip/parser'
import { strukturHinweis, titelVorauswahl } from '../rip/titelwahl' import { strukturHinweis, titelVorauswahl } from '../rip/titelwahl'
@@ -316,19 +316,32 @@ export class ErkennungDienst {
// 5.7.0 (Kandidat 2): Die Grenze kommt aus den Einstellungen (Vorgabe // 5.7.0 (Kandidat 2): Die Grenze kommt aus den Einstellungen (Vorgabe
// 15 min statt fest fünf), und ab der zweiten Minute sagt der Dialog, // 15 min statt fest fünf), und ab der zweiten Minute sagt der Dialog,
// dass es länger dauert — mit Abbrechen-Knopf, statt stumm zu drehen. // dass es länger dauert — mit Abbrechen-Knopf, statt stumm zu drehen.
// 5.7.1 (Der Patriot): Die Grenze gilt für STILLE — solange MakeMKV
// Fortschritt oder Meldungen schickt, wartet Rippy, und der Dialog zeigt,
// was MakeMKV gerade tut.
const minuten = this.abh.titelLesenMinuten?.() ?? 15 const minuten = this.abh.titelLesenMinuten?.() ?? 15
const zwischenstand = setInterval(() => { let lebens: Lebenszeichen = { prozent: -1, schritt: '', meldung: '', vobScan: false }
let letztesLebenszeichen = start
let letzteSendung = start
const zwischenstandSenden = (): void => {
if (!nochAktuell()) return if (!nochAktuell()) return
const vergangen = Math.round((Date.now() - start) / 60_000) letzteSendung = Date.now()
senden({ senden({ ...leer, laeuft: true, einordnung: titelLaufZwischenstand(Date.now() - start, Date.now() - letztesLebenszeichen, minuten, lebens) })
...leer, }
laeuft: true, const zwischenstand = setInterval(zwischenstandSenden, 60_000)
einordnung: `MakeMKV liest seit ${vergangen} min. Kopiergeschützte DVDs und Discs mit vielen Playlisten brauchen länger — Rippy wartet bis ${minuten} min (Einstellungen → Allgemein), „Abbrechen" beendet den Lauf.`,
})
}, 60_000)
zwischenstand.unref() zwischenstand.unref()
try { try {
const info = await titelInfoLesen(programm, geraet.pfad, minuten * 60_000, abbruch.signal) const info = await titelInfoLesen(programm, geraet.pfad, {
stillMs: minuten * 60_000,
abbruch: abbruch.signal,
aufLebenszeichen: (stand) => {
const neueMeldung = stand.meldung !== lebens.meldung || stand.schritt !== lebens.schritt
lebens = stand
letztesLebenszeichen = Date.now()
// Neue Meldung oder neuer Schritt sofort, Prozente höchstens alle 5 s.
if (Date.now() - start >= 30_000 && (neueMeldung || Date.now() - letzteSendung >= 5_000)) zwischenstandSenden()
},
})
if (!nochAktuell()) return { ...leer, fehler: ABGELOEST } if (!nochAktuell()) return { ...leer, fehler: ABGELOEST }
const merkmale = this.merkmale.get(id) const merkmale = this.merkmale.get(id)
const wahl = titelVorauswahl(info.titel, { const wahl = titelVorauswahl(info.titel, {
@@ -369,10 +382,14 @@ export class ErkennungDienst {
if (String(fehler).includes(ABGELOEST) || !nochAktuell()) return { ...leer, fehler: ABGELOEST } if (String(fehler).includes(ABGELOEST) || !nochAktuell()) return { ...leer, fehler: ABGELOEST }
// R4: sonst laut scheitern. „Konnte nicht nachsehen" ist etwas anderes // R4: sonst laut scheitern. „Konnte nicht nachsehen" ist etwas anderes
// als „die Disc hat keine Titel" — beides zu vermischen erzeugt Unsinn. // als „die Disc hat keine Titel" — beides zu vermischen erzeugt Unsinn.
const grund = String(fehler).includes('keine Antwort nach') const grund = String(fehler).includes('kein Lebenszeichen seit')
? `MakeMKV hat die Titel nach ${minuten} Minuten nicht gelesen — Rippy hat den Versuch beendet und das Laufwerk freigegeben. Das kann an einer beschädigten Disc liegen, aber auch an Kopierschutz oder vielen Playlisten, die einfach länger brauchen: Die Grenze steht unter Einstellungen → Allgemein. Zur Probe die Disc im MakeMKV-Programm öffnen und die Zeit stoppen.` ? `MakeMKV hat ${minuten} Minuten lang kein Lebenszeichen gegeben (kein Fortschritt, keine Meldung) — Rippy hat den Versuch nach ${Math.round((Date.now() - start) / 60_000)} min beendet und das Laufwerk freigegeben.${
: `Titel lesen: ${String(fehler).slice(0, 200)}` lebens.meldung.length > 0 ? ` Letzte Meldung: ${lebens.meldung}".` : ''
this.kontext.protokoll.schreiben(`Titel-Lauf ${id} gescheitert: ${grund}`) } Das kann an einer beschädigten Disc oder am Laufwerk liegen. Die Geduld steht unter Einstellungen → Allgemein; zur Probe die Disc im MakeMKV-Programm öffnen.`
: String(fehler).includes('VOB-Scan nach')
? `MakeMKV liest die beschädigte DVD-Struktur seit ${Math.round((Date.now() - start) / 60_000)} min und ist nicht fertig geworden — Rippy hat den Versuch beendet und das Laufwerk freigegeben. Zur Probe die Disc im MakeMKV-Programm öffnen.`
: `Titel lesen: ${String(fehler).slice(0, 200)}`
this.kontext.protokoll.schreiben(`Titel-Lauf ${id} gescheitert: ${grund} [${String(fehler).slice(0, 900)}]`)
return senden({ ...leer, fehler: grund }) return senden({ ...leer, fehler: grund })
} finally { } finally {
clearInterval(zwischenstand) clearInterval(zwischenstand)
@@ -448,3 +465,22 @@ export class ErkennungDienst {
this.kontext.anFenster({ art: 'poster-vorschau', pfade }) this.kontext.anFenster({ art: 'poster-vorschau', pfade })
} }
} }
/** Was der Dialog während des Titel-Lesens zeigt (pur, 5.7.1): wie lange
* MakeMKV schon liest, was es gerade tut, wann Rippy aufgeben würde. */
export function titelLaufZwischenstand(vergangenMs: number, stillMs: number, geduldMinuten: number, lebens: Lebenszeichen): string {
const min = Math.max(1, Math.round(vergangenMs / 60_000))
const teile: string[] = [`MakeMKV liest seit ${min} min`]
if (lebens.schritt.length > 0 || lebens.prozent >= 0) {
teile.push(`${lebens.schritt.length > 0 ? lebens.schritt : 'Fortschritt'}${lebens.prozent >= 0 ? ` ${lebens.prozent} %` : ''}`)
}
if (lebens.meldung.length > 0) teile.push(`letzte Meldung: „${lebens.meldung.trim().slice(0, 160)}"`)
let text = teile.join(' · ') + '.'
if (lebens.vobScan) {
text += ` Die DVD-Struktur ist beschädigt — MakeMKV liest die ganze VOB-Datei durch; das dauert so lange wie das Lesen der Disc (oft 2060 min), im MakeMKV-Programm genauso. Rippy wartet, solange makemkvcon läuft.`
} else {
const stillMin = Math.floor(stillMs / 60_000)
text += ` Solange MakeMKV Fortschritt meldet, wartet Rippy; ohne Lebenszeichen bricht es nach ${geduldMinuten} min ab (Einstellungen → Allgemein)${stillMin >= 1 ? ` — still seit ${stillMin} min` : ''}.`
}
return text + ' „Abbrechen" beendet den Lauf.'
}
+98 -13
View File
@@ -15,6 +15,8 @@ import { werkzeugSpawn } from '../werkzeuge/aufruf'
import { mkdirSync, readdirSync } from 'node:fs' import { mkdirSync, readdirSync } from 'node:fs'
import { join } from 'node:path' import { join } from 'node:path'
import { import {
aufgabeAusPrg,
istVobScanMeldung,
KRITISCHE_CODES, KRITISCHE_CODES,
KRITISCHE_TEXTE, KRITISCHE_TEXTE,
LESEFEHLER_MARKE, LESEFEHLER_MARKE,
@@ -31,8 +33,11 @@ import {
type TitelStreams, type TitelStreams,
} from './parser' } from './parser'
/** 5.7.1: auch der Info-Lauf meldet Fortschritt (--progress=-same) — daran
* erkennt Rippy, dass MakeMKV noch arbeitet, statt nach fester Zeit
* abzubrechen. */
export function buildInfoArgs(geraetepfad: string): string[] { export function buildInfoArgs(geraetepfad: string): string[] {
return ['-r', '--noscan', 'info', quelle(geraetepfad)] return ['-r', '--noscan', '--progress=-same', 'info', quelle(geraetepfad)]
} }
export function buildRipArgs(geraetepfad: string, zielOrdner: string, titel: string): string[] { export function buildRipArgs(geraetepfad: string, zielOrdner: string, titel: string): string[] {
@@ -151,26 +156,106 @@ export interface DiscInfo {
sprachen: { audio: SprachEintrag[]; untertitel: SprachEintrag[] } sprachen: { audio: SprachEintrag[]; untertitel: SprachEintrag[] }
} }
/** Was der Info-Lauf gerade tut — für den Dialog (5.7.1). */
export interface Lebenszeichen {
/** Gesamtfortschritt 0100, -1 ohne Angabe. */
prozent: number
/** Der aktuelle Schritt laut PRGC, '' wenn unbekannt. */
schritt: string
/** Die letzte Meldung (MSG-Klartext), '' wenn keine. */
meldung: string
/** MakeMKV hat den VOB-Scan einer beschädigten DVD angekündigt. */
vobScan: boolean
}
export interface TitelInfoOptionen {
/** So lange darf makemkvcon STILL sein (kein Byte Ausgabe), bevor Rippy
* ihn beendet. Solange Fortschritt oder Meldungen kommen, wartet Rippy —
* eine feste Gesamtgrenze warf den Patriot-Lauf weg, während das
* MakeMKV-Programm dieselbe Disc fertig las (Commander, 12.09.2026). */
stillMs?: number
/** Hat MakeMKV den VOB-Scan angekündigt, gilt statt der Stille-Grenze
* nur diese Obergrenze — der Scan liest die ganze Disc, und ob er dabei
* Fortschritt meldet, ist nicht gemessen. */
vobScanHoechstensMs?: number
abbruch?: AbortSignal
aufLebenszeichen?: (stand: Lebenszeichen) => void
}
export const VOB_SCAN_HOECHSTENS_MS = 4 * 60 * 60_000
/** EIN Info-Lauf liefert Titel UND Sprachen — er dauert je Disc 20120 s, /** EIN Info-Lauf liefert Titel UND Sprachen — er dauert je Disc 20120 s,
* zweimal holen wäre eine Minute Wartezeit für nichts (ripping.py). */ * zweimal holen wäre eine Minute Wartezeit für nichts (ripping.py).
export function titelInfoLesen( * 5.7.1: Die Grenze ist eine STILLE-Grenze — makemkvcon wird nur beendet,
programm: string, * wenn er so lange nichts mehr sagt; wer Fortschritt meldet, darf weiter. */
geraetepfad: string, export function titelInfoLesen(programm: string, geraetepfad: string, optionen: TitelInfoOptionen = {}): Promise<DiscInfo> {
timeoutMs = 300_000, const stillMs = optionen.stillMs ?? 300_000
abbruch?: AbortSignal, const vobHoechstensMs = optionen.vobScanHoechstensMs ?? VOB_SCAN_HOECHSTENS_MS
): Promise<DiscInfo> { const abbruch = optionen.abbruch
return new Promise((fertig, scheitert) => { return new Promise((fertig, scheitert) => {
const kind = werkzeugSpawn(programm, buildInfoArgs(geraetepfad), { const kind = werkzeugSpawn(programm, buildInfoArgs(geraetepfad), {
windowsHide: true, windowsHide: true,
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],
}) })
const stuecke: Buffer[] = [] const stuecke: Buffer[] = []
kind.stdout.on('data', (stueck: Buffer) => stuecke.push(stueck)) const stand: Lebenszeichen = { prozent: -1, schritt: '', meldung: '', vobScan: false }
kind.stderr.on('data', (stueck: Buffer) => stuecke.push(stueck)) const start = Date.now()
const zeiger = setTimeout(() => { let zeiger: NodeJS.Timeout
// Die letzten Zeilen reisen im Fehler mit — fürs Protokoll, damit ein
// fremder PC sagen kann, WO makemkvcon stehen blieb.
const letzteZeilen: string[] = []
const stumm = (): void => {
kind.kill() kind.kill()
scheitert(new Error(`makemkvcon info: keine Antwort nach ${Math.round(timeoutMs / 1000)} s`)) const grund = stand.vobScan
}, timeoutMs) ? `makemkvcon info: VOB-Scan nach ${Math.round(vobHoechstensMs / 60_000)} min nicht fertig`
: `makemkvcon info: kein Lebenszeichen seit ${Math.round(stillMs / 1000)} s`
scheitert(new Error(letzteZeilen.length > 0 ? `${grund} — letzte Zeilen: ${letzteZeilen.join(' | ')}` : grund))
}
const neuWarten = (): void => {
clearTimeout(zeiger)
// Beim VOB-Scan zählt nur die Obergrenze ab Start des Laufs.
const frist = stand.vobScan ? Math.max(0, vobHoechstensMs - (Date.now() - start)) : stillMs
zeiger = setTimeout(stumm, frist)
}
neuWarten()
const leserAus = new ZeilenLeser()
const leserFehler = new ZeilenLeser()
const aufStueck = (leser: ZeilenLeser, stueck: Buffer): void => {
stuecke.push(stueck)
let neu = false
for (const zeile of leser.hinzu(stueck)) {
if (zeile.trim().length > 0) {
letzteZeilen.push(zeile.trim().slice(0, 200))
if (letzteZeilen.length > 8) letzteZeilen.shift()
}
const p = progressAusPrgv(zeile)
if (p >= 0) {
if (p !== stand.prozent) {
stand.prozent = p
neu = true
}
continue
}
const aufgabe = aufgabeAusPrg(zeile)
if (aufgabe !== null) {
if (aufgabe.art === 'schritt' && aufgabe.name !== stand.schritt) {
stand.schritt = aufgabe.name
neu = true
}
continue
}
const meldung = parseMsg(zeile)
if (meldung !== null && meldung.text !== stand.meldung) {
stand.meldung = meldung.text
if (istVobScanMeldung(meldung.text)) stand.vobScan = true
neu = true
}
}
neuWarten()
if (neu) optionen.aufLebenszeichen?.({ ...stand })
}
kind.stdout.on('data', (stueck: Buffer) => aufStueck(leserAus, stueck))
kind.stderr.on('data', (stueck: Buffer) => aufStueck(leserFehler, stueck))
// Ein abgelöster Lauf lässt das Laufwerk sofort los — zwei makemkvcon // Ein abgelöster Lauf lässt das Laufwerk sofort los — zwei makemkvcon
// auf derselben Disc machen beide langsamer (Commander-Fund 01.09.2026: // auf derselben Disc machen beide langsamer (Commander-Fund 01.09.2026:
// Ein alter Lauf meldete 300 s später seinen Timeout und legte ihn über // Ein alter Lauf meldete 300 s später seinen Timeout und legte ihn über
+21
View File
@@ -68,6 +68,27 @@ export function progressAusPrgv(zeile: string): number {
return Math.min(100, Math.floor((gesamt * 100) / maximum)) return Math.min(100, Math.floor((gesamt * 100) / maximum))
} }
// ── PRGC/PRGT: Was MakeMKV gerade tut ───────────────────────────────────
// PRGC:code,id,"name" ist der aktuelle Schritt („Analysieren von seamless
// segments"), PRGT:code,id,"name" die Gesamtaufgabe („Öffnen der DVD-Disc").
// Für die Lebenszeichen im Titel-Dialog (5.7.1, Der Patriot).
const PRG_ZEILE = /^PRG([CT]):(\d+),(\d+),"((?:[^"\\]|\\.)*)"/
export function aufgabeAusPrg(zeile: string): { art: 'schritt' | 'aufgabe'; name: string } | null {
const treffer = PRG_ZEILE.exec(zeile.trim())
if (treffer === null) return null
return { art: treffer[1] === 'C' ? 'schritt' : 'aufgabe', name: treffer[4].replaceAll('\\"', '"') }
}
/** MakeMKV kündigt den langen Weg an: „IFO file for VTS #1 is corrupt, VOB
* file needs to be scanned. This may take a long time" — deutsch „IFO-Datei
* für VTS #1 ist beschädigt, die VOB-Datei muss gescannt werden". Dann liest
* MakeMKV die ganze VOB durch, so lange wie die Disc braucht (Der Patriot,
* Commander 12.09.2026: 15 Minuten reichten nicht, das GUI schaffte es). */
export function istVobScanMeldung(text: string): boolean {
return /\bIFO\b/i.test(text) && /\bVOB\b/i.test(text)
}
// ── TINFO: Titel-Liste ────────────────────────────────────────────────── // ── TINFO: Titel-Liste ──────────────────────────────────────────────────
// Attribut-IDs laut MakeMKV apdefs.h: 8 = ChapterCount, 9 = Duration // Attribut-IDs laut MakeMKV apdefs.h: 8 = ChapterCount, 9 = Duration
// ("H:MM:SS"), 11 = DiskSizeBytes. // ("H:MM:SS"), 11 = DiskSizeBytes.
@@ -0,0 +1,132 @@
// 5.7.1 — Commander-Fund 12.09.2026 spät: „Der Patriot" (DVD, IFO-Datei für
// VTS #1 beschädigt, MakeMKV scannt die VOB-Datei). Rippy 5.7.0 brach nach
// 15 Minuten ab, das MakeMKV-Programm las die Disc fertig. Jetzt gilt die
// Grenze für STILLE: Solange makemkvcon Fortschritt oder Meldungen schickt,
// wartet Rippy — und der Dialog zeigt, was MakeMKV gerade tut.
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { titelLaufZwischenstand } from '../src/kern/dienste/erkennung'
import { buildInfoArgs, titelInfoLesen, VOB_SCAN_HOECHSTENS_MS, type Lebenszeichen } from '../src/kern/rip/makemkv'
import { aufgabeAusPrg, istVobScanMeldung } from '../src/kern/rip/parser'
let wegwerf: string[] = []
afterEach(() => {
for (const o of wegwerf.splice(0)) rmSync(o, { recursive: true, force: true })
})
describe('Fortschritts-Zeilen des Info-Laufs', () => {
it('der Info-Lauf verlangt --progress=-same', () => {
expect(buildInfoArgs('\\\\.\\G:')).toContain('--progress=-same')
})
it('PRGC ist der Schritt, PRGT die Aufgabe — mit Komma und Anführungszeichen im Namen', () => {
expect(aufgabeAusPrg('PRGC:5018,0,"Analysieren von seamless segments"')).toEqual({ art: 'schritt', name: 'Analysieren von seamless segments' })
expect(aufgabeAusPrg('PRGT:5017,0,"Öffnen der DVD-Disc"')).toEqual({ art: 'aufgabe', name: 'Öffnen der DVD-Disc' })
expect(aufgabeAusPrg('PRGC:1,2,"Lesen, Teil \\"A\\""')).toEqual({ art: 'schritt', name: 'Lesen, Teil "A"' })
expect(aufgabeAusPrg('PRGV:10,20,65536')).toBeNull()
expect(aufgabeAusPrg('MSG:1005,0,1,"x","x"')).toBeNull()
})
it('erkennt die VOB-Scan-Ankündigung auf Deutsch und Englisch', () => {
expect(istVobScanMeldung('IFO-Datei für VTS #1 ist beschädigt, die VOB-Datei muss gescannt werden. Das kann viel Zeit in Anspruch nehmen, bitte haben Sie Geduld.')).toBe(true)
expect(istVobScanMeldung('IFO file for VTS #1 is corrupt, VOB file needs to be scanned. This may take a long time, please be patient.')).toBe(true)
expect(istVobScanMeldung('Using LibreDrive mode (v02.1 id=F39BF9BC17BE)')).toBe(false)
expect(istVobScanMeldung('Titel #1 wurde hinzugefügt (31 cell(s), 2:47:40)')).toBe(false)
})
})
describe('Zwischenstand im Dialog', () => {
const still: Lebenszeichen = { prozent: -1, schritt: '', meldung: '', vobScan: false }
it('nennt Dauer, Schritt, Prozent, letzte Meldung und die Geduld', () => {
const text = titelLaufZwischenstand(17 * 60_000, 20_000, 15, { prozent: 43, schritt: 'Öffnen der DVD-Disc', meldung: 'Using LibreDrive mode', vobScan: false })
expect(text).toContain('MakeMKV liest seit 17 min')
expect(text).toContain('Öffnen der DVD-Disc 43 %')
expect(text).toContain('letzte Meldung: „Using LibreDrive mode"')
expect(text).toContain('ohne Lebenszeichen bricht es nach 15 min ab')
expect(text).toContain('„Abbrechen" beendet den Lauf')
expect(text).not.toContain('still seit')
})
it('sagt, wie lange MakeMKV schon still ist', () => {
expect(titelLaufZwischenstand(9 * 60_000, 3 * 60_000 + 5000, 15, still)).toContain('still seit 3 min')
})
it('beim VOB-Scan: die Erklärung statt der Stille-Grenze', () => {
const text = titelLaufZwischenstand(23 * 60_000, 0, 15, { prozent: 12, schritt: '', meldung: 'IFO-Datei für VTS #1 ist beschädigt, die VOB-Datei muss gescannt werden.', vobScan: true })
expect(text).toContain('Fortschritt 12 %')
expect(text).toContain('DVD-Struktur ist beschädigt')
expect(text).toContain('oft 2060 min')
expect(text).not.toContain('bricht es nach')
})
})
// Ein nachgebautes makemkvcon (Node-Skript, aufruf.ts startet es über die
// eigene Laufzeit): meldet erst die VOB-Ankündigung, dann alle 300 ms
// Fortschritt — länger als die Stille-Grenze von 1 s — und liefert am Ende
// einen Titel. Ein zweites schweigt nach der ersten Zeile.
function fakeMakemkv(inhalt: string): string {
const ordner = mkdtempSync(join(tmpdir(), 'rippy-lebens-'))
wegwerf.push(ordner)
const pfad = join(ordner, 'fake-makemkvcon.cjs')
writeFileSync(pfad, inhalt)
return pfad
}
const REDSELIG = `
const out = (z) => process.stdout.write(z + '\\n')
out('MSG:1005,0,1,"MakeMKV v1.18.4 win(x64-release) gestartet","%1 gestartet","MakeMKV v1.18.4 win(x64-release)"')
out('PRGT:5017,0,"Öffnen der DVD-Disc"')
out('MSG:3300,0,1,"IFO-Datei für VTS #1 ist beschädigt, die VOB-Datei muss gescannt werden. Das kann viel Zeit in Anspruch nehmen, bitte haben Sie Geduld.","x"')
let i = 0
const tick = () => {
i += 1
out('PRGV:' + i * 100 + ',' + i * 100 + ',1000')
if (i < 8) return setTimeout(tick, 300)
out('TCOUNT:1')
out('TINFO:0,8,0,"28"')
out('TINFO:0,9,0,"2:47:40"')
out('TINFO:0,11,0,"5800000000"')
out('SINFO:0,0,1,6201,"Video"')
process.exit(0)
}
setTimeout(tick, 300)
`
const STUMM = `
process.stdout.write('MSG:1005,0,1,"MakeMKV gestartet","x"\\n')
setTimeout(() => process.exit(0), 60_000)
`
describe('titelInfoLesen — Geduld nach Lebenszeichen', () => {
it('wartet über die Stille-Grenze hinaus, solange Fortschritt kommt; meldet Lebenszeichen', { timeout: 20_000 }, async () => {
const zeichen: Lebenszeichen[] = []
const start = Date.now()
const info = await titelInfoLesen(fakeMakemkv(REDSELIG), '\\\\.\\G:', { stillMs: 1000, aufLebenszeichen: (s) => zeichen.push(s) })
expect(Date.now() - start).toBeGreaterThan(2000)
expect(info.titel).toHaveLength(1)
expect(info.titel[0].dauerS).toBe(2 * 3600 + 47 * 60 + 40)
const letztes = zeichen.at(-1)!
expect(letztes.vobScan).toBe(true)
expect(letztes.meldung).toContain('VOB-Datei muss gescannt werden')
expect(zeichen.some((z) => z.prozent === 50)).toBe(true)
expect(zeichen.some((z) => z.schritt === '' && z.prozent === -1 && z.meldung.includes('gestartet'))).toBe(true)
})
it('beendet einen stummen makemkvcon nach der Stille-Grenze — mit klarem Grund', { timeout: 20_000 }, async () => {
const start = Date.now()
await expect(titelInfoLesen(fakeMakemkv(STUMM), '\\\\.\\G:', { stillMs: 1000 })).rejects.toThrow('kein Lebenszeichen seit 1 s')
expect(Date.now() - start).toBeLessThan(10_000)
// Die letzten Zeilen reisen im Fehler mit — fürs Protokoll.
await expect(titelInfoLesen(fakeMakemkv(STUMM), '\\\\.\\G:', { stillMs: 1000 })).rejects.toThrow('letzte Zeilen: MSG:1005')
})
it('beim VOB-Scan gilt nur die Obergrenze (4 h Vorgabe)', { timeout: 20_000 }, async () => {
expect(VOB_SCAN_HOECHSTENS_MS).toBe(4 * 60 * 60_000)
// Stille-Grenze 200 ms, aber nach der VOB-Ankündigung kommt 300 ms lang
// nichts — der Lauf darf trotzdem weiterleben und liefert den Titel.
const info = await titelInfoLesen(fakeMakemkv(REDSELIG), '\\\\.\\G:', { stillMs: 200 })
expect(info.titel).toHaveLength(1)
})
it('beim VOB-Scan beendet die Obergrenze den Lauf', { timeout: 20_000 }, async () => {
await expect(titelInfoLesen(fakeMakemkv(REDSELIG), '\\\\.\\G:', { stillMs: 60_000, vobScanHoechstensMs: 1200 })).rejects.toThrow('VOB-Scan nach 0 min nicht fertig')
})
})
+9 -3
View File
@@ -266,9 +266,15 @@ describe('Pipeline — Durchsicht 5.7.0', () => {
// die Ablehnung — kein Fehler im Fenster, keine Zeile im Protokoll // die Ablehnung — kein Fehler im Fenster, keine Zeile im Protokoll
// (Sonde 10). Jetzt: eine Fehler-Meldung, keine Ausnahme. // (Sonde 10). Jetzt: eine Fehler-Meldung, keine Ausnahme.
it('F2: Ablage nicht erreichbar → Fehler-Meldung statt stiller Ausnahme', async () => { it('F2: Ablage nicht erreichbar → Fehler-Meldung statt stiller Ausnahme', async () => {
// Ein Laufwerksbuchstabe, den es nicht gibt — wie eine NAS, die gerade // Ein Ordner UNTER einer Datei — anlegen scheitert sofort, auf jeder
// weg ist. (Unter Linux ein Ort, an dem niemand anlegen darf.) // Plattform (ENOTDIR bzw. EEXIST). Nicht /proc: dort antwortet mkdir mit
const weg = process.platform === 'win32' ? 'Q:\\rippy-nicht-da' : '/proc/rippy-nicht-da' // ENOENT, obwohl der Elternordner da ist, und Nodes rekursives mkdirSync
// pendelt dann endlos — die Ampel hing daran (Lauf 279, 12.09.2026).
const sperre = mkdtempSync(join(tmpdir(), 'rippy-sperre-'))
wegwerf.push(sperre)
const datei = join(sperre, 'keine-ablage.txt')
writeFileSync(datei, 'x')
const weg = join(datei, 'rippy-nicht-da')
const { pipeline, meldungen, endet } = umgebung({ ablage: () => weg }) const { pipeline, meldungen, endet } = umgebung({ ablage: () => weg })
await pipeline.starten('G', AUFTRAG) await pipeline.starten('G', AUFTRAG)
expect(meldungen).toHaveLength(1) expect(meldungen).toHaveLength(1)
+1 -1
View File
@@ -6,7 +6,7 @@ import { buildInfoArgs, buildRipArgs, RipAuswertung, ZeilenLeser } from '../src/
describe('Kommandobau', () => { describe('Kommandobau', () => {
it('info: Robot-Mode ohne Geräte-Scan', () => { it('info: Robot-Mode ohne Geräte-Scan', () => {
expect(buildInfoArgs('\\\\.\\G:')).toEqual(['-r', '--noscan', 'info', 'dev:G:']) expect(buildInfoArgs('\\\\.\\G:')).toEqual(['-r', '--noscan', '--progress=-same', 'info', 'dev:G:'])
}) })
it('mkv: Fortschritt in denselben Strom', () => { it('mkv: Fortschritt in denselben Strom', () => {
expect(buildRipArgs('\\\\.\\G:', 'C:\\ziel', 'all')).toEqual([ expect(buildRipArgs('\\\\.\\G:', 'C:\\ziel', 'all')).toEqual([