feat(v5): Etappe W-3 — Komprimieren + Ablegen, mit Vollbeweis
Ampel / ampel (push) Failing after 1m18s
Ampel / ampel (push) Failing after 1m18s
komprimieren/: handbrake.ts traegt ALLE bezahlten Fallen aus dem Docker-Zweig — --aencoder statt --audio-codec (mit Anti-Regressions- Wächter im Test: der alte Test hatte den Fehler FESTGESCHRIEBEN), --format aus der Endung (Container kommt sonst aus dem Preset), Fortschritt NUR aus der Encoding-Zeile (Scan-99%-Falle), letzte 12 Zeilen fuer den Fehlerfall, unknown-option-Erkennung VOR allem anderen (kommt mit Rueckgabewert 0!), Datei-daneben-Suche, CR-Zeilentrennung mit 0x81-Kodierungsfalle. encoder.ts misst statt behauptet (--help, --preset-list, Familien statt Sammelbegriff). presets.ts: je Disc-Typ, PRESET_KEINE, Sprachlisten. ablage/: struktur.ts (sichere Namen, '<Titel> (Jahr)', Season NN, Episoden-Zuordnung NUR bei Eindeutigkeit), nfo.ts (Kodi-Schema), medienserver.ts (Jellyfin/Emby/Kodi-Refresh, wirft nie). ablauf/pipeline.ts ERSETZT rippen.ts: Rip → Kompression → Ablage als EINE Kette; Kompressions-Fehler laesst den verlustfreien Rip STEHEN und nennt den Ort; Lesefehler ueberleben bis in die Fertig-Meldung. Roh wird in dieser Etappe grundsaetzlich behalten (Loeschen erst mit W-5-UI). GEMESSEN am echten System (30.08.2026): - Encoder: 22 Stueck, Backends cpu-x264/x265/av1 + vce + vce-av1 (RX 9070 XT als VCE-Familie — nicht unter Intel-Namen), 107 Presets. - Probe-Encode 5 s Material: success in 45,5 s. - VOLLBEWEIS: Spartacus-Rohschnitt (17,71 GB — die Datei, die in rc11 am falschen Schalter starb) mit Preset 'H.265 VCN 1080p' in 2,1 min auf 1,17 GB komprimiert und nach Jellyfin-Schema abgelegt: E:\Rippy-v5-Beweis\Filme\Spartacus - Gods of the Arena - Disc 2 (2011)\ mit movie.nfo. - Nebenbefund: Der Evangelion-Roh-Rip vom 29.08. traegt 'This file was not properly finalized' im Kopf — 23,5 GB, die wie fertig aussehen und es nicht sind (der abgebrochene rc11-Rip). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
ca338ad467
commit
3dc296f071
@@ -0,0 +1,101 @@
|
||||
// Encoder-Fähigkeiten — GEMESSEN, nicht behauptet (KONZEPT § 4.2,
|
||||
// Regeln aus caps.py, dort am echten HandBrake abgenommen):
|
||||
//
|
||||
// - `--help`, Abschnitt `-e, --encoder`: NUR was DIESE Maschine wirklich
|
||||
// benutzen kann (HandBrake probiert Hardware beim Start selbst an).
|
||||
// - `--preset-list`: ALLE mitgelieferten Preset-NAMEN (Struktur ist die
|
||||
// Einrückung — Namen stehen mit genau vier Leerzeichen).
|
||||
// - Hardware nach FAMILIE unterscheiden (nvenc/qsv/vce/vaapi), sonst
|
||||
// läuft AMD unter einem Intel-Namen (Commander-PC: RX 9070 XT).
|
||||
import { spawn } from 'node:child_process'
|
||||
import { textVon } from '../rip/parser'
|
||||
|
||||
const HB_ENCODER_KOPF = /^-e,\s*--encoder\b/
|
||||
|
||||
/** Video-Encoder aus `--help` — der Block endet an der nächsten Option. */
|
||||
export function parseEncoderListe(hilfeText: string): string[] {
|
||||
const encoder: string[] = []
|
||||
let imBlock = false
|
||||
for (const zeile of (hilfeText ?? '').split(/\r?\n/)) {
|
||||
const inhalt = zeile.trim()
|
||||
if (!imBlock) {
|
||||
if (HB_ENCODER_KOPF.test(inhalt)) imBlock = true
|
||||
continue
|
||||
}
|
||||
if (inhalt.length === 0) continue
|
||||
if (inhalt.startsWith('-')) break
|
||||
encoder.push(inhalt.split(/\s+/)[0])
|
||||
}
|
||||
return encoder
|
||||
}
|
||||
|
||||
// Reihenfolge = Anzeige-Reihenfolge; AV1 in Hardware sichtbar machen.
|
||||
const HW_FAMILIEN: ReadonlyArray<readonly [string, string]> = [
|
||||
['nvenc', 'nvenc'],
|
||||
['qsv', 'qsv'],
|
||||
['vce', 'vce'],
|
||||
['vaapi', 'vaapi'],
|
||||
]
|
||||
|
||||
/** HandBrake-Encoder-Namen → Backend-Kennungen fürs UI. */
|
||||
export function leiteBackendsAb(hbEncoder: readonly string[]): string[] {
|
||||
const namen = new Set(hbEncoder.map((e) => e.toLowerCase()))
|
||||
const gefunden: string[] = []
|
||||
if ([...namen].some((n) => n.startsWith('x264'))) gefunden.push('cpu-x264')
|
||||
if ([...namen].some((n) => n.startsWith('x265'))) gefunden.push('cpu-x265')
|
||||
if ([...namen].some((n) => n.startsWith('svt_av1'))) gefunden.push('cpu-av1')
|
||||
for (const [kennzeichen, backend] of HW_FAMILIEN) {
|
||||
const treffer = [...namen].filter((n) => n.includes(kennzeichen))
|
||||
if (treffer.length === 0) continue
|
||||
gefunden.push(backend)
|
||||
if (treffer.some((n) => n.includes('av1'))) gefunden.push(`${backend}-av1`)
|
||||
}
|
||||
return gefunden
|
||||
}
|
||||
|
||||
/** Preset-Namen aus `--preset-list`: genau vier Leerzeichen Einrückung.
|
||||
* Kategorien (0), Beschreibungen (8) und Logzeilen fallen so weg. */
|
||||
export function parsePresetListe(text: string): string[] {
|
||||
const presets: string[] = []
|
||||
for (const zeile of (text ?? '').split(/\r?\n/)) {
|
||||
if (zeile.trim().length === 0) continue
|
||||
const einrueckung = zeile.length - zeile.replace(/^ +/, '').length
|
||||
if (einrueckung === 4) presets.push(zeile.trim())
|
||||
}
|
||||
return presets
|
||||
}
|
||||
|
||||
function lauf(programm: string, args: string[], timeoutMs: number): Promise<string> {
|
||||
return new Promise((fertig) => {
|
||||
const kind = spawn(programm, args, { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
||||
const stuecke: Buffer[] = []
|
||||
kind.stdout.on('data', (s: Buffer) => stuecke.push(s))
|
||||
kind.stderr.on('data', (s: Buffer) => stuecke.push(s))
|
||||
const zeiger = setTimeout(() => kind.kill(), timeoutMs)
|
||||
kind.on('error', () => {
|
||||
clearTimeout(zeiger)
|
||||
fertig('')
|
||||
})
|
||||
kind.on('close', () => {
|
||||
clearTimeout(zeiger)
|
||||
fertig(textVon(Buffer.concat(stuecke)))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export interface EncoderAuskunft {
|
||||
encoder: string[]
|
||||
backends: string[]
|
||||
presets: string[]
|
||||
}
|
||||
|
||||
/** Fragt HandBrake selbst — leer, wenn es nicht da ist (kein Raten). */
|
||||
export async function encoderMessen(programm: string): Promise<EncoderAuskunft> {
|
||||
if (programm.length === 0) return { encoder: [], backends: [], presets: [] }
|
||||
const [hilfe, presetListe] = await Promise.all([
|
||||
lauf(programm, ['--help'], 30_000),
|
||||
lauf(programm, ['--preset-list'], 30_000),
|
||||
])
|
||||
const encoder = parseEncoderListe(hilfe)
|
||||
return { encoder, backends: leiteBackendsAb(encoder), presets: parsePresetListe(presetListe) }
|
||||
}
|
||||
Reference in New Issue
Block a user