feat(v5): Etappe W-3 — Komprimieren + Ablegen, mit Vollbeweis
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:
Hitonabi
2026-08-30 18:26:00 +02:00
co-authored by Claude Fable 5
parent ca338ad467
commit 3dc296f071
14 changed files with 1300 additions and 155 deletions
@@ -0,0 +1,63 @@
// MESSUNG am echten HandBrakeCLI: Encoder-Auskunft und ein kurzer
// Probe-Encode an einer echten Roh-MKV. Läuft nur auf Verlangen:
//
// RIPPY_MESSUNG=1 [RIPPY_MESSUNG_MKV=<roh.mkv>] \
// npx vitest run test/messung.handbrake.test.ts --reporter=verbose
import { existsSync, mkdtempSync, statSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
const messen = process.env.RIPPY_MESSUNG === '1' && process.platform === 'win32'
const rohMkv = process.env.RIPPY_MESSUNG_MKV ?? ''
describe.skipIf(!messen)('HandBrake am echten System', () => {
it('meldet Encoder, Backends und Presets', { timeout: 120_000 }, async () => {
const { finden } = await import('../src/kern/werkzeuge/katalog')
const { encoderMessen } = await import('../src/kern/komprimieren/encoder')
const programm = finden('handbrake')
expect(programm.length).toBeGreaterThan(0)
const auskunft = await encoderMessen(programm)
console.log(`MESSUNG Encoder (${auskunft.encoder.length}): ${auskunft.encoder.join(', ')}`)
console.log(`MESSUNG Backends: ${auskunft.backends.join(', ')}`)
console.log(`MESSUNG Presets: ${auskunft.presets.length} Stück, z. B. ${auskunft.presets.slice(0, 4).join(' | ')}`)
expect(auskunft.encoder.length).toBeGreaterThan(0)
expect(auskunft.presets.length).toBeGreaterThan(0)
})
it.skipIf(rohMkv.length === 0 || !existsSync(rohMkv))(
'kurzer Probe-Encode an der echten Roh-MKV (5 s Material)',
{ timeout: 15 * 60_000 },
async () => {
const { finden } = await import('../src/kern/werkzeuge/katalog')
const { komprimieren } = await import('../src/kern/komprimieren/handbrake')
const programm = finden('handbrake')
const ziel = join(mkdtempSync(join(tmpdir(), 'rippy-hb-')), 'probe.mkv')
const start = Date.now()
const ergebnis = await komprimieren({
programm,
eingabe: rohMkv,
ausgabe: ziel,
preset: 'H.265 MKV 1080p30',
audioSprachen: ['deu', 'jpn', 'eng'],
extraArgs: ['--start-at', 'duration:60', '--stop-at', 'duration:5'],
})
const sekunden = ((Date.now() - start) / 1000).toFixed(1)
if (ergebnis.status === 'success') {
console.log(
`MESSUNG Probe-Encode: success in ${sekunden}s → ${ergebnis.ausgabe} (${(statSync(ergebnis.ausgabe).size / 1e6).toFixed(1)} MB)` +
(ergebnis.hinweis !== undefined ? ` · Hinweis: ${ergebnis.hinweis}` : ''),
)
} else {
console.log(`MESSUNG Probe-Encode: ${ergebnis.status} in ${sekunden}s ${'fehler' in ergebnis ? ergebnis.fehler : ''}`)
}
expect(ergebnis.status).toBe('success')
},
)
})
describe.skipIf(messen)('Messung übersprungen', () => {
it('läuft nur mit RIPPY_MESSUNG=1 unter Windows', () => {
expect(true).toBe(true)
})
})