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>
This commit is contained in:
Hitonabi
2026-09-13 01:04:08 +02:00
co-authored by Claude Opus 5
parent 0088948b4f
commit eb54c59141
21 changed files with 322 additions and 35 deletions
@@ -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')
})
})
+1 -1
View File
@@ -6,7 +6,7 @@ import { buildInfoArgs, buildRipArgs, RipAuswertung, ZeilenLeser } from '../src/
describe('Kommandobau', () => {
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', () => {
expect(buildRipArgs('\\\\.\\G:', 'C:\\ziel', 'all')).toEqual([