feat(v5): Kern 5.4.0 — Vorgaenge, Kompressions-Warteschlange, Roh-Aufraeumen, Sprachen je Titel, Erkennung v2
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>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
d70a81d194
commit
3f7f962dff
@@ -0,0 +1,172 @@
|
||||
// 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) })
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user