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
+45 -7
View File
@@ -6,13 +6,16 @@
// jedem neuen Abonnenten sofort wiedergegeben — er wird nie auf »leer«
// zurückgesetzt, nur weil gerade keine Nachricht kam. Insbesondere die
// Laufwerksliste: Sie ändert sich NUR durch eine neue Liste vom Kern.
import { istKernNachricht, type GeraeteInfo, type KernNachricht } from '../gemeinsam/nachrichten'
import { istKernNachricht, type GeraeteInfo, type KernNachricht, type RipStatus } from '../gemeinsam/nachrichten'
export interface Verbindung {
verbunden: boolean
kern: Extract<KernNachricht, { art: 'kern-bereit' }> | null
pongLatenzMs: number | null
laufwerke: GeraeteInfo[]
/** Letzter Rip-Stand je Laufwerks-Kennung. Bleibt nach dem Ende stehen,
* bis ein neuer Rip startet (R2: kein Verschwinden ohne neue Auskunft). */
rips: Record<string, RipStatus>
/** Kurze Ereigniszeilen (neueste zuerst): Disc rein/raus, Auswurf, Fehler. */
meldungen: string[]
}
@@ -21,13 +24,22 @@ type Horcher = (stand: Verbindung) => void
const MAX_MELDUNGEN = 8
const stand: Verbindung = { verbunden: false, kern: null, pongLatenzMs: null, laufwerke: [], meldungen: [] }
const stand: Verbindung = {
verbunden: false,
kern: null,
pongLatenzMs: null,
laufwerke: [],
rips: {},
meldungen: [],
}
const horcher = new Set<Horcher>()
let gestartet = false
let port: MessagePort | null = null
function melden(): void {
for (const h of horcher) h({ ...stand, laufwerke: [...stand.laufwerke], meldungen: [...stand.meldungen] })
for (const h of horcher) {
h({ ...stand, laufwerke: [...stand.laufwerke], rips: { ...stand.rips }, meldungen: [...stand.meldungen] })
}
}
function meldung(text: string): void {
@@ -67,6 +79,20 @@ function kernNachricht(nachricht: KernNachricht): void {
meldung(nachricht.grund === 'ok' ? text : `${text}`)
break
}
case 'rip-status': {
stand.rips = { ...stand.rips, [nachricht.status.id]: nachricht.status }
if (nachricht.status.phase === 'fertig') {
meldung(
`Laufwerk ${nachricht.status.id}: Rip fertig — ${nachricht.status.dateien.length} Datei(en)` +
(nachricht.status.lesefehler ? ' ⚠ mit Lesefehlern' : ''),
)
} else if (nachricht.status.phase === 'fehler') {
meldung(`⚠ Laufwerk ${nachricht.status.id}: ${nachricht.status.text}`)
} else if (nachricht.status.phase === 'abgebrochen') {
meldung(`Laufwerk ${nachricht.status.id}: Rip abgebrochen`)
}
break
}
}
melden()
}
@@ -104,15 +130,27 @@ export function verbindungStarten(): void {
export function aufVerbindung(h: Horcher): () => void {
horcher.add(h)
h({ ...stand, laufwerke: [...stand.laufwerke], meldungen: [...stand.meldungen] })
h({ ...stand, laufwerke: [...stand.laufwerke], rips: { ...stand.rips }, meldungen: [...stand.meldungen] })
return () => horcher.delete(h)
}
export function auswerfen(id: string): void {
function anKern(nachricht: { art: string; id: string }, wasWar: string): void {
if (port === null) {
meldung(` Auswerfen ${id}: keine Verbindung zum Kern`)
meldung(`${wasWar}: keine Verbindung zum Kern`)
melden()
return
}
port.postMessage({ art: 'auswerfen', id })
port.postMessage(nachricht)
}
export function auswerfen(id: string): void {
anKern({ art: 'auswerfen', id }, `Auswerfen ${id}`)
}
export function ripStarten(id: string): void {
anKern({ art: 'rip-start', id }, `Rip ${id}`)
}
export function ripAbbrechen(id: string): void {
anKern({ art: 'rip-abbruch', id }, `Rip-Abbruch ${id}`)
}