WAS: Der Rip endet, sobald die Roh-Dateien liegen; die Kompression laeuft danach in einer Warteschlange (eine zur Zeit), das Laufwerk ist frei fuer die naechste Disc. Jeder Rip ist jetzt ein Datensatz (Tabelle vorgaenge, Schema 4): Roh-Ordner, Zielordner, Phase, Auftrag als JSON. Darauf bauen: Roh-Aufraeumen nach Regel (Vorgabe 7 Tage, nur bei gelungener Kompression und nur wenn jede fertige Datei da und nicht leer ist), Wiederholen ab Kompression, Aufraeumen halber Dateien nach Abbruch/Fehler, Wiederaufnahme nach Neustart. Unkomprimierte Rips (4K, kein HandBrake) werden in den Zielordner VERSCHOBEN statt unter roh\ liegen zu bleiben. Sprachen je Titel reisen vom Info-Lauf (SINFO) bis in HandBrake; fremder Ton schaltet den deutschen Untertitel vor (--subtitle-default=1). Vor der Kompression prueft ein HandBrake-Scan die Roh-Datei. MakeMKVs Auswahlregel wird gesetzt, wenn keine steht, damit alle Sprachen in die Roh-Datei kommen. Staffel und erste Folge von Hand schlagen die Laufzeit-Raterei. Durchgehender Fortschritt, nach Groesse gewichtet. Erkennung v2: leere TMDb-Eintraege (Stubs) zaehlen nicht, Laufzeit wird nach dem Info-Lauf gegen TMDb gehalten, Titel-Struktur als Fingerabdruck, bdmt_deu als zweite Titel-Variante. TMDb liefert Logo, Tagline, Bewertung, FSK und Besetzung in derselben Anfrage. Laufwerks-Gesundheit aus dem Windows-Ereignisprotokoll (wevtutil): Disc oder Hardware. WARUM: Uebergabe-Punkte 1-5, 8-12 des SAVEPOINT vom 01.09.2026 — vom Commander gutgeheissen (7 Tage / Untertitel vorgewaehlt / entkoppelte Kompression waren seine Wahl). GEMESSEN (Regel D): HandBrake 1.11 Sprach-Schalter an einer ffmpeg- Testdatei (jpn/deu/deu/eng + deu/eng), HandBrake --scan --json Format, TMDb movie/149 + tv/46296 mit append_to_response, wevtutil /f:xml an diesem PC (338x cdrom 7, 8x cdrom 11 in 30 Tagen), makemkvcon --minlength, MakeMKV-Auswahlregel laut forum.makemkv.com t=4386. 348 Tests gruen (vorher 261), Typpruefung und Bau gruen. Oberflaeche folgt im naechsten Schritt. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
173 lines
6.9 KiB
TypeScript
173 lines
6.9 KiB
TypeScript
// Laufwerks-Gesundheit aus dem Windows-Ereignisprotokoll (5.4.0).
|
||
//
|
||
// Der Anlass (01.09.2026): Beim Spartacus-Defekt und beim 5042-Zustand
|
||
// des BU40N war die entscheidende Frage „Disc oder Hardware?" — und die
|
||
// Antwort stand im System-Protokoll, von Hand gelesen. Jetzt liest Rippy
|
||
// sie selbst.
|
||
//
|
||
// GEMESSEN (01.09.2026, Regel D, an diesem PC):
|
||
// wevtutil qe System "/q:<XPath>" /f:xml /c:200 /rd:true
|
||
// Provider 'cdrom', EventID 7 → „Fehlerhafter Block bei Gerät \Device\CdRom0."
|
||
// EventID 11 → „Der Treiber hat einen Controllerfehler auf \Device\CdRom0 gefunden."
|
||
// EventID 51 → Auslagerungs-/E/A-Fehler (bei 7 dabei)
|
||
// In 30 Tagen: 338 × 7, 8 × 11, 46 × 51 — die 7er häuften sich exakt um
|
||
// den Spartacus-Rip (20:32), die 11er beim USB-Aussetzer (20:17).
|
||
// TimeCreated SystemTime ist UTC. Die XML-Ereignisse stehen OHNE
|
||
// Zeilenumbruch hintereinander; ein Regex je <Event>…</Event> reicht.
|
||
// Die Argumente gehen ohne Shell an wevtutil — ein „/q:"-Argument mit
|
||
// Leerzeichen bleibt so EIN Argument (über Git Bash zerlegt es der
|
||
// Pfad-Konverter, über spawn nicht).
|
||
import { spawn } from 'node:child_process'
|
||
import type { LaufwerkBericht } from '../../gemeinsam/nachrichten'
|
||
import { textVon } from '../rip/parser'
|
||
|
||
export interface LaufwerkEreignis {
|
||
/** ISO-Zeit (UTC). */
|
||
zeit: string
|
||
provider: string
|
||
id: number
|
||
geraet: string
|
||
}
|
||
|
||
/** Die Ereignis-IDs des cdrom-/disk-Treibers, wie am PC gemessen. */
|
||
export const EREIGNIS_TEXTE: Record<number, string> = {
|
||
7: 'fehlerhafter Block',
|
||
11: 'Controllerfehler',
|
||
15: 'Gerät nicht bereit',
|
||
51: 'E/A-Fehler beim Lesen',
|
||
153: 'E/A-Vorgang wiederholt',
|
||
}
|
||
|
||
export function xpathFuer(stunden: number): string {
|
||
const ms = Math.max(1, Math.round(stunden)) * 60 * 60 * 1000
|
||
return `*[System[Provider[@Name='cdrom' or @Name='disk'] and TimeCreated[timediff(@SystemTime) <= ${ms}]]]`
|
||
}
|
||
|
||
export function buildWevtutilArgs(stunden: number, hoechstens = 200): string[] {
|
||
return ['qe', 'System', `/q:${xpathFuer(stunden)}`, '/f:xml', `/c:${hoechstens}`, '/rd:true']
|
||
}
|
||
|
||
/** Die XML-Ausgabe von wevtutil → Ereignisse (pur, testbar). */
|
||
export function ereignisseAusXml(xml: string): LaufwerkEreignis[] {
|
||
const liste: LaufwerkEreignis[] = []
|
||
for (const treffer of xml.matchAll(/<Event\b[\s\S]*?<\/Event>/g)) {
|
||
const block = treffer[0]
|
||
const provider = /<Provider Name='([^']*)'/.exec(block)?.[1] ?? ''
|
||
const id = Number(/<EventID[^>]*>(\d+)<\/EventID>/.exec(block)?.[1] ?? '')
|
||
const zeit = /<TimeCreated SystemTime='([^']*)'/.exec(block)?.[1] ?? ''
|
||
const geraet = /<Data>([^<]*)<\/Data>/.exec(block)?.[1] ?? ''
|
||
if (!Number.isInteger(id) || provider.length === 0) continue
|
||
liste.push({ zeit, provider, id, geraet })
|
||
}
|
||
return liste
|
||
}
|
||
|
||
/** Urteil aus den Ereignissen (pur). `seit` grenzt auf einen Zeitraum ein
|
||
* (z. B. den laufenden Rip); ohne `seit` zählt alles, was da ist. */
|
||
export function beurteilen(ereignisse: readonly LaufwerkEreignis[], stunden: number, seit?: Date): LaufwerkBericht {
|
||
const relevant = seit === undefined ? [...ereignisse] : ereignisse.filter((e) => new Date(e.zeit).getTime() >= seit.getTime())
|
||
let fehlerhafteBloecke = 0
|
||
let controllerfehler = 0
|
||
let sonstige = 0
|
||
for (const e of relevant) {
|
||
if (e.id === 7) fehlerhafteBloecke += 1
|
||
else if (e.id === 11 || e.id === 15) controllerfehler += 1
|
||
else sonstige += 1
|
||
}
|
||
const letzte = relevant.slice(0, 8).map((e) => {
|
||
const lokal = e.zeit.length > 0 ? new Date(e.zeit) : null
|
||
const zeit =
|
||
lokal !== null && !Number.isNaN(lokal.getTime())
|
||
? `${lokal.getFullYear()}-${String(lokal.getMonth() + 1).padStart(2, '0')}-${String(lokal.getDate()).padStart(2, '0')} ${String(lokal.getHours()).padStart(2, '0')}:${String(lokal.getMinutes()).padStart(2, '0')}`
|
||
: '?'
|
||
return `${zeit} · ${e.provider} ${e.id} · ${EREIGNIS_TEXTE[e.id] ?? 'Ereignis'} (${e.geraet})`
|
||
})
|
||
const zeitraum = seit === undefined ? `in den letzten ${stunden} Stunden` : `seit ${seit.toISOString().slice(11, 16)} UTC`
|
||
|
||
if (relevant.length === 0) {
|
||
return {
|
||
urteil: 'ruhig',
|
||
text: `Das Windows-Protokoll meldet ${zeitraum} nichts zum Laufwerk — weder Lesefehler noch Hardware-Fehler.`,
|
||
fehlerhafteBloecke,
|
||
controllerfehler,
|
||
sonstige,
|
||
letzte,
|
||
stunden,
|
||
}
|
||
}
|
||
if (controllerfehler > 0) {
|
||
return {
|
||
urteil: 'hardware',
|
||
text:
|
||
`${controllerfehler} Controllerfehler ${zeitraum}` +
|
||
(fehlerhafteBloecke > 0 ? ` und ${fehlerhafteBloecke} fehlerhafte Blöcke` : '') +
|
||
'. Ein Controllerfehler kommt vom Laufwerk oder seiner Verbindung, nicht von der Disc: ' +
|
||
'Bei einem USB-Laufwerk zuerst einen Port direkt am Mainboard probieren, Kabel und Netzteil prüfen.',
|
||
fehlerhafteBloecke,
|
||
controllerfehler,
|
||
sonstige,
|
||
letzte,
|
||
stunden,
|
||
}
|
||
}
|
||
if (fehlerhafteBloecke > 0) {
|
||
return {
|
||
urteil: 'disc',
|
||
text:
|
||
`${fehlerhafteBloecke} fehlerhafte Blöcke ${zeitraum}, keine Controllerfehler. ` +
|
||
'Das spricht für die DISC (Kratzer, Fertigungsfehler), nicht für das Laufwerk: ' +
|
||
'Disc reinigen, den betroffenen Titel überspringen oder eine andere Ausgabe versuchen.',
|
||
fehlerhafteBloecke,
|
||
controllerfehler,
|
||
sonstige,
|
||
letzte,
|
||
stunden,
|
||
}
|
||
}
|
||
return {
|
||
urteil: 'unklar',
|
||
text: `${sonstige} Laufwerks-Ereignisse ${zeitraum}, aber weder fehlerhafte Blöcke noch Controllerfehler — kein klares Bild.`,
|
||
fehlerhafteBloecke,
|
||
controllerfehler,
|
||
sonstige,
|
||
letzte,
|
||
stunden,
|
||
}
|
||
}
|
||
|
||
/** Liest das Protokoll — null heißt „konnte nicht lesen", mit Grund. */
|
||
export function ereignisseLesen(
|
||
stunden: number,
|
||
timeoutMs = 30_000,
|
||
): Promise<{ ereignisse: LaufwerkEreignis[] } | { fehler: string }> {
|
||
return new Promise((fertig) => {
|
||
let kind: ReturnType<typeof spawn>
|
||
try {
|
||
kind = spawn('wevtutil', buildWevtutilArgs(stunden), { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
||
} catch (fehler) {
|
||
fertig({ fehler: `wevtutil nicht startbar: ${String(fehler)}` })
|
||
return
|
||
}
|
||
const stuecke: Buffer[] = []
|
||
const fehlerStuecke: Buffer[] = []
|
||
kind.stdout?.on('data', (s: Buffer) => stuecke.push(s))
|
||
kind.stderr?.on('data', (s: Buffer) => fehlerStuecke.push(s))
|
||
const zeiger = setTimeout(() => kind.kill(), timeoutMs)
|
||
kind.on('error', (fehler) => {
|
||
clearTimeout(zeiger)
|
||
fertig({ fehler: `wevtutil nicht startbar: ${String(fehler)}` })
|
||
})
|
||
kind.on('close', (code) => {
|
||
clearTimeout(zeiger)
|
||
const xml = textVon(Buffer.concat(stuecke))
|
||
// Ohne Treffer endet wevtutil mit 0 und leerer Ausgabe; ein Fehler
|
||
// steht auf stderr mit Code ≠ 0.
|
||
if (code !== 0 && xml.trim().length === 0) {
|
||
fertig({ fehler: `wevtutil endete mit Code ${code}: ${textVon(Buffer.concat(fehlerStuecke)).trim().slice(0, 200)}` })
|
||
return
|
||
}
|
||
fertig({ ereignisse: ereignisseAusXml(xml) })
|
||
})
|
||
})
|
||
}
|