feat(v5): Etappe W-2 — Rippen: Parser, makemkvcon-Lauf, Werkzeug-Katalog
Ampel / ampel (push) Failing after 1m22s

rip/parser.ts: die reine Textverarbeitung mit allen bezahlten Fallen aus
dem Docker-Zweig — quelle() (dev:G: statt dev:\.\G:, der 404-Verwandte),
textVon() (UTF-8 streng, sonst cp1252 — nie wieder 'Öffnen'), parseMsg
(Regex statt split — Meldungen mit Komma), PRGV (-1 vs. echtes 0 %),
TINFO/SINFO (Sprache aus Attr 3/4, NICHT 28/29), laengsterTitel mit
Play-All-Schutz, episodenTitel, KRITISCHE_CODES (sprachfeste NUMMERN).

rip/makemkv.ts: Kommandobau + Lauf — ZeilenLeser (binaer, halbe Zeilen,
je Zeile dekodiert), RipAuswertung (pur), rippen() mit AbortSignal,
Lesefehler-Flag AUCH bei Rueckgabewert 0. werkzeuge/katalog.ts: die
Suchreihenfolge samt %VAR%-Gross/Klein-Falle und Registry-Rueckfall.
ablauf/rippen.ts: ein Rip je Laufwerk, Status-Push, 'erst nachsehen,
dann rippen' (unbekannt heisst trotzdem rippen). Fenster: Rippen-Knopf,
Fortschrittsbalken, Abbrechen; Auswurf waehrend des Rips abgelehnt.

Gemessen am echten System (30.08.2026): Katalog findet
makemkvcon64 (Program Files x86), HandBrakeCLI und flac (beide im
rc11-Werkzeugordner LOCALAPPDATA\Rippy\tools). NEUER BEFUND: Nach dem
abgebrochenen rc11-Rip vom Mittag sieht MakeMKV GAR KEIN Laufwerk mehr
(MSG 5042, leere DRV-Liste, mit und ohne --noscan), waehrend die
Storage-IOCTLs sauber antworten — MakeMKV spricht SCSI direkt. 5042 ist
jetzt ein KRITISCHER_CODE mit Klartext-Abhilfe. Der volle Rip-Beweis
(messung.rip.test.ts liegt bereit) braucht deshalb erst eine Hand am
Geraet: Disc neu einlegen oder USB ab-/anstecken.

96 Tests gruen, Smoke gruen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-30 18:12:44 +02:00
co-authored by Claude Fable 5
parent a45069af5e
commit 844bca37ff
13 changed files with 1495 additions and 23 deletions
+68
View File
@@ -0,0 +1,68 @@
// DER W-2-BEWEIS: ein voller, verlustfreier Rip der eingelegten Disc am
// echten Laufwerk — dauert 3090 Minuten. Läuft nur auf Verlangen:
//
// RIPPY_MESSUNG_RIP=1 RIPPY_MESSUNG_ZIEL=<ordner> \
// npx vitest run test/messung.rip.test.ts --reporter=verbose
//
// Fortschritt wird in 5-%-Schritten geloggt; das Ergebnis nennt Dateien
// und Größen. Das Protokoll wandert in den SAVEPOINT (beweisen statt
// behaupten).
import { statSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const messen = process.env.RIPPY_MESSUNG_RIP === '1' && process.platform === 'win32'
const zielWurzel = process.env.RIPPY_MESSUNG_ZIEL ?? ''
describe.skipIf(!messen)('Voller Rip am echten Laufwerk', () => {
it('rippt die eingelegte Disc verlustfrei', { timeout: 3 * 60 * 60 * 1000 }, async () => {
expect(zielWurzel.length, 'RIPPY_MESSUNG_ZIEL fehlt').toBeGreaterThan(0)
const { finden } = await import('../src/kern/werkzeuge/katalog')
const { rippen } = await import('../src/kern/rip/makemkv')
const { win32Api } = await import('../src/kern/laufwerk/win32')
const { listOptischeLaufwerke } = await import('../src/kern/laufwerk/disc')
const programm = finden('makemkv')
expect(programm.length).toBeGreaterThan(0)
const geraete = listOptischeLaufwerke(win32Api())
expect(geraete.length).toBeGreaterThan(0)
const start = Date.now()
let letzteStufe = -5
const ergebnis = await rippen({
programm,
geraetepfad: geraete[0],
zielOrdner: zielWurzel,
titel: 'all',
aufFortschritt: (prozent) => {
if (prozent >= letzteStufe + 5) {
letzteStufe = prozent
const minuten = ((Date.now() - start) / 60000).toFixed(1)
console.log(`RIP-FORTSCHRITT ${prozent}% nach ${minuten}min`)
}
},
aufMeldung: (code, text) => {
// Nur die tragenden Meldungen — sonst wird das Protokoll unlesbar.
if (code >= 5000 || text.includes('errors') || text.includes('gesichert') || text.includes('saved')) {
console.log(`RIP-MSG ${code}: ${text}`)
}
},
})
const minuten = ((Date.now() - start) / 60000).toFixed(1)
if (ergebnis.status === 'success') {
console.log(`RIP-ERGEBNIS success nach ${minuten}min · lesefehler=${ergebnis.lesefehler}`)
for (const datei of ergebnis.dateien) {
console.log(`RIP-DATEI ${datei} · ${(statSync(datei).size / 1e9).toFixed(2)} GB`)
}
} else {
console.log(`RIP-ERGEBNIS ${ergebnis.status} nach ${minuten}min: ${'fehler' in ergebnis ? ergebnis.fehler : ''}`)
}
expect(ergebnis.status).toBe('success')
})
})
describe.skipIf(messen)('Rip-Messung übersprungen', () => {
it('läuft nur mit RIPPY_MESSUNG_RIP=1 unter Windows', () => {
expect(true).toBe(true)
})
})