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,168 @@
|
||||
// Tests der Verkettung Rip → Kompression → Ablage — mit nachgebildeten
|
||||
// Werkzeug-Läufen. Die Regeln: Kompression nur wenn gewollt UND HandBrake
|
||||
// da; ein Kompressions-Fehler lässt den verlustfreien Rip STEHEN und sagt
|
||||
// wo er liegt; Statusmeldungen tragen die Phase.
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { RipStatus } from '../src/gemeinsam/nachrichten'
|
||||
|
||||
const ripErgebnis = vi.hoisted(() => ({
|
||||
wert: { status: 'success', dateien: ['R:\\roh\\a.mkv'], lesefehler: false } as unknown,
|
||||
}))
|
||||
const encodeErgebnis = vi.hoisted(() => ({
|
||||
wert: { status: 'success', ausgabe: 'R:\\fertig\\a.mkv' } as unknown,
|
||||
}))
|
||||
|
||||
vi.mock('../src/kern/rip/makemkv', () => ({
|
||||
rippen: vi.fn(async () => ripErgebnis.wert),
|
||||
}))
|
||||
vi.mock('../src/kern/komprimieren/handbrake', () => ({
|
||||
komprimieren: vi.fn(async () => encodeErgebnis.wert),
|
||||
}))
|
||||
|
||||
import { Pipeline, type PipelineUmgebung } from '../src/kern/ablauf/pipeline'
|
||||
|
||||
let wegwerf: string[] = []
|
||||
afterEach(() => {
|
||||
for (const ordner of wegwerf.splice(0)) rmSync(ordner, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function umgebung(überschreiben: Partial<PipelineUmgebung> = {}): {
|
||||
pipeline: Pipeline
|
||||
meldungen: RipStatus[]
|
||||
} {
|
||||
const ablage = mkdtempSync(join(tmpdir(), 'rippy-pipe-'))
|
||||
wegwerf.push(ablage)
|
||||
const meldungen: RipStatus[] = []
|
||||
const pipeline = new Pipeline({
|
||||
makemkvcon: () => 'C:\\mk\\makemkvcon64.exe',
|
||||
handbrake: () => 'C:\\hb\\HandBrakeCLI.exe',
|
||||
geraet: () => ({
|
||||
id: 'G',
|
||||
name: 'Laufwerk G:',
|
||||
typ: 'bluray',
|
||||
pfad: '\\\\.\\G:',
|
||||
status: 'ready',
|
||||
modell: '',
|
||||
seriennummer: '',
|
||||
groesseBytes: 33e9,
|
||||
grund: '',
|
||||
}),
|
||||
ablage: () => ablage,
|
||||
einstellungen: () => ({}),
|
||||
melden: (s) => meldungen.push(s),
|
||||
jetzt: () => new Date('2026-08-30T12:00:00Z'),
|
||||
...überschreiben,
|
||||
})
|
||||
return { pipeline, meldungen }
|
||||
}
|
||||
|
||||
describe('Pipeline', () => {
|
||||
it('kettet Rip → Kompression → Ablage und meldet die Phasen', async () => {
|
||||
ripErgebnis.wert = { status: 'success', dateien: ['R:\\roh\\a.mkv'], lesefehler: false }
|
||||
encodeErgebnis.wert = { status: 'success', ausgabe: 'R:\\fertig\\a.mkv' }
|
||||
const { pipeline, meldungen } = umgebung()
|
||||
await pipeline.starten('G')
|
||||
const phasen = meldungen.map((m) => m.phase)
|
||||
expect(phasen[0]).toBe('rippt')
|
||||
expect(phasen).toContain('komprimiert')
|
||||
expect(phasen).toContain('legt-ab')
|
||||
expect(phasen.at(-1)).toBe('fertig')
|
||||
expect(meldungen.at(-1)?.dateien).toEqual(['R:\\fertig\\a.mkv'])
|
||||
})
|
||||
|
||||
it('Kompression abgewählt: Roh-Rip ist das Ergebnis, mit Begründung', async () => {
|
||||
ripErgebnis.wert = { status: 'success', dateien: ['R:\\roh\\a.mkv'], lesefehler: false }
|
||||
const { pipeline, meldungen } = umgebung({
|
||||
einstellungen: () => ({ transcodePresetBluray: 'keine' }),
|
||||
})
|
||||
await pipeline.starten('G')
|
||||
const letzte = meldungen.at(-1)
|
||||
expect(letzte?.phase).toBe('fertig')
|
||||
expect(letzte?.text).toContain('abgewählt')
|
||||
expect(letzte?.dateien).toEqual(['R:\\roh\\a.mkv'])
|
||||
})
|
||||
|
||||
it('ohne HandBrake bleibt es beim Roh-Rip — mit klarem Satz', async () => {
|
||||
ripErgebnis.wert = { status: 'success', dateien: ['R:\\roh\\a.mkv'], lesefehler: false }
|
||||
const { pipeline, meldungen } = umgebung({ handbrake: () => '' })
|
||||
await pipeline.starten('G')
|
||||
expect(meldungen.at(-1)?.text).toContain('HandBrakeCLI nicht gefunden')
|
||||
})
|
||||
|
||||
it('Kompressions-Fehler: Roh bleibt stehen und wird GENANNT', async () => {
|
||||
ripErgebnis.wert = { status: 'success', dateien: ['R:\\roh\\a.mkv'], lesefehler: false }
|
||||
encodeErgebnis.wert = { status: 'error', fehler: 'HandBrake endete mit Code 3' }
|
||||
const { pipeline, meldungen } = umgebung()
|
||||
await pipeline.starten('G')
|
||||
const letzte = meldungen.at(-1)
|
||||
expect(letzte?.phase).toBe('fehler')
|
||||
expect(letzte?.text).toContain('bleibt stehen')
|
||||
expect(letzte?.dateien).toEqual(['R:\\roh\\a.mkv'])
|
||||
})
|
||||
|
||||
it('Lesefehler überleben bis in die Fertig-Meldung (Rückgabewert-0-Falle)', async () => {
|
||||
ripErgebnis.wert = { status: 'success', dateien: ['R:\\roh\\a.mkv'], lesefehler: true }
|
||||
encodeErgebnis.wert = { status: 'success', ausgabe: 'R:\\fertig\\a.mkv' }
|
||||
const { pipeline, meldungen } = umgebung()
|
||||
await pipeline.starten('G')
|
||||
const letzte = meldungen.at(-1)
|
||||
expect(letzte?.lesefehler).toBe(true)
|
||||
expect(letzte?.text).toContain('nicht lesen')
|
||||
})
|
||||
|
||||
it('leeres Laufwerk: ein Satz statt makemkvcon (aber unknown rippt!)', async () => {
|
||||
const { pipeline, meldungen } = umgebung({
|
||||
geraet: () => ({
|
||||
id: 'G',
|
||||
name: 'Laufwerk G:',
|
||||
typ: 'unknown',
|
||||
pfad: '\\\\.\\G:',
|
||||
status: 'empty',
|
||||
modell: '',
|
||||
seriennummer: '',
|
||||
groesseBytes: 0,
|
||||
grund: '',
|
||||
}),
|
||||
})
|
||||
await pipeline.starten('G')
|
||||
expect(meldungen[0].phase).toBe('fehler')
|
||||
expect(meldungen[0].text).toContain('Kein Datenträger')
|
||||
})
|
||||
|
||||
it('NFO landet im fertigen Ordner', async () => {
|
||||
ripErgebnis.wert = { status: 'success', dateien: ['R:\\roh\\a.mkv'], lesefehler: false }
|
||||
encodeErgebnis.wert = { status: 'success', ausgabe: 'R:\\fertig\\a.mkv' }
|
||||
const ablage = mkdtempSync(join(tmpdir(), 'rippy-pipe-'))
|
||||
wegwerf.push(ablage)
|
||||
const meldungen: RipStatus[] = []
|
||||
const pipeline = new Pipeline({
|
||||
makemkvcon: () => 'x',
|
||||
handbrake: () => 'y',
|
||||
geraet: () => ({
|
||||
id: 'G',
|
||||
name: 'Laufwerk G:',
|
||||
typ: 'dvd',
|
||||
pfad: '\\\\.\\G:',
|
||||
status: 'ready',
|
||||
modell: '',
|
||||
seriennummer: '',
|
||||
groesseBytes: 4e9,
|
||||
grund: '',
|
||||
}),
|
||||
ablage: () => ablage,
|
||||
einstellungen: () => ({}),
|
||||
melden: (s) => meldungen.push(s),
|
||||
jetzt: () => new Date('2026-08-30T12:00:00Z'),
|
||||
})
|
||||
await pipeline.starten('G')
|
||||
expect(meldungen.at(-1)?.phase).toBe('fertig')
|
||||
const { existsSync, readdirSync } = await import('node:fs')
|
||||
const filme = join(ablage, 'Filme')
|
||||
const ordner = readdirSync(filme)
|
||||
expect(ordner).toHaveLength(1)
|
||||
expect(existsSync(join(filme, ordner[0], 'movie.nfo'))).toBe(true)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user