Files
rippy/rippy-windows/src/kern/komprimieren/encoder.ts
T
HitonabiandClaude Fable 5.1 6dd1b0bc7a feat(v5): Ein Smoke-Beweis, der KLICKT — Playwright durch das echte Programm
WAS: `npm run klick` startet das ECHTE Rippy (Electron, Haupt, Kern,
Fenster, Datenbank) mit eigenem Profil (--profil, --klick-smoke: ohne
Tray, Updater, Toasts), einem nachgebauten Laufwerk (RIPPY_FAKE_LAUFWERK)
und nachgebauten Werkzeugen (bau/klick/fake-*.cjs) — und klickt sich
durch: Rippen → Dialog → Sprachen je Titel (jpn statt deu) → Rollen auf
Folge → Staffel 1, ab Folge 3 → Vorschau → Rip → Warteschlange →
Kompression → Ablage als S01E03/S01E04 unter Serien\…\Season 01 →
Einstellungen/Roh-Dateien → Roh loeschen → Ereignisse. Bilder liegen in
beweise/klick/. Die Naht dafuer (kern/werkzeuge/aufruf.ts): Zeigt ein
Werkzeug-Pfad auf ein Node-Skript, laeuft es ueber die eigene Laufzeit
(ELECTRON_RUN_AS_NODE) — fuer EXE-Pfade aendert sich nichts.

WARUM: Beide Fehler vom 01.09.2026 waren Verkabelung; 261 Unit-Tests
sahen keinen. Der Klick-Beweis haette beide gefunden.

NEBENBEFUND: Die CSP blockte eingebettete Schrift-Teile (data:-Fonts der
fontsource-Pakete) — Konsolenfehler im gebauten Fenster, jetzt font-src.
Dazu test/messung.spuren.test.ts: HandBrake-Scan einer echten Roh-Datei
auf Verlangen (RIPPY_MESSUNG_MKV).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-01 22:22:04 +02:00

102 lines
3.8 KiB
TypeScript

// 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 { werkzeugSpawn } from '../werkzeuge/aufruf'
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 = werkzeugSpawn(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) }
}