// 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 20–60 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') }) })