Ampel / ampel (push) Successful in 1m20s
rip/audiocd.ts: die Rechnungen PUR (MSF→LBA mit den 150 Vorlauf-Frames, TOC-Zerlegung mit Lead-Out-Pflicht und Daten-Track-Bit, 27-Sektoren- Bloecke unter der 64-KB-Treibergrenze, RAW_READ_INFO mit DiskOffset in 2048ern OBWOHL der Sektor 2352 hat, WAV-Kopf, AC/DC-sichere Namen). metadaten/musicbrainz.ts: DiscID nach musicbrainz.org-Rechnung — gegen die ECHTE AC/DC-Kennung getestet (3KVSIWn_…), Vorlauf wird beim Melden wieder HINZUgerechnet; Tags je Spur mit Bindewort-Kuenstlern. laufwerk/disc.ts: audioToc + audioSpurLesen ueber die LaufwerkApi. rip/iso.ts (§ 6.4): 1:1-Abbild fuer unbekannte Discs — async (ein 33-GB-Abbild darf den Kern nicht blockieren), sektor-ausgerichtet, unvollstaendig heisst FEHLER mit Byte-Zahlen, nie Schoenfaerberei. werkzeuge/schluessel.ts (§ 9.1): Dauerlizenz schlaegt Forum-Beta-Key (t=1053, T-Muster aus makemkv_key.py), Ablage in HKCU ueber reg.exe (kein zweiter koffi-Ort), GEFRAGT statt gerechnet (makemkvcon info disc:9999, MSG 5020/5021/5051), abgelehnter Key wird ZURUECKGENOMMEN (schlimmer als keiner: 0 Titel, kein Laufwerk), app_UpdateEnable nur- wenn-fehlt (der 4K-Schluesselkanal). Kern prueft beim Start und taeglich; Dashboard-Kachel zeigt Stand + letzten Pruefzeitpunkt; Dauerlizenz-Feld in den Einstellungen. Pipeline verzweigt nach Disc-Typ: cd → Sektoren→WAV→FLAC mit MusicBrainz-Tags (Ordner 'Kuenstler - Album', ohne Treffer ehrlich 'Track NN'), unknown → ISO. Bibliothek-Eintraege fuer beide. 181 Tests gruen, Smoke gruen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
171 lines
6.1 KiB
TypeScript
171 lines
6.1 KiB
TypeScript
// 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',
|
|
flac: () => '',
|
|
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',
|
|
flac: () => '',
|
|
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)
|
|
})
|
|
})
|