feat(v5): Kern 5.4.0 — Vorgaenge, Kompressions-Warteschlange, Roh-Aufraeumen, Sprachen je Titel, Erkennung v2
WAS: Der Rip endet, sobald die Roh-Dateien liegen; die Kompression laeuft danach in einer Warteschlange (eine zur Zeit), das Laufwerk ist frei fuer die naechste Disc. Jeder Rip ist jetzt ein Datensatz (Tabelle vorgaenge, Schema 4): Roh-Ordner, Zielordner, Phase, Auftrag als JSON. Darauf bauen: Roh-Aufraeumen nach Regel (Vorgabe 7 Tage, nur bei gelungener Kompression und nur wenn jede fertige Datei da und nicht leer ist), Wiederholen ab Kompression, Aufraeumen halber Dateien nach Abbruch/Fehler, Wiederaufnahme nach Neustart. Unkomprimierte Rips (4K, kein HandBrake) werden in den Zielordner VERSCHOBEN statt unter roh\ liegen zu bleiben. Sprachen je Titel reisen vom Info-Lauf (SINFO) bis in HandBrake; fremder Ton schaltet den deutschen Untertitel vor (--subtitle-default=1). Vor der Kompression prueft ein HandBrake-Scan die Roh-Datei. MakeMKVs Auswahlregel wird gesetzt, wenn keine steht, damit alle Sprachen in die Roh-Datei kommen. Staffel und erste Folge von Hand schlagen die Laufzeit-Raterei. Durchgehender Fortschritt, nach Groesse gewichtet. Erkennung v2: leere TMDb-Eintraege (Stubs) zaehlen nicht, Laufzeit wird nach dem Info-Lauf gegen TMDb gehalten, Titel-Struktur als Fingerabdruck, bdmt_deu als zweite Titel-Variante. TMDb liefert Logo, Tagline, Bewertung, FSK und Besetzung in derselben Anfrage. Laufwerks-Gesundheit aus dem Windows-Ereignisprotokoll (wevtutil): Disc oder Hardware. WARUM: Uebergabe-Punkte 1-5, 8-12 des SAVEPOINT vom 01.09.2026 — vom Commander gutgeheissen (7 Tage / Untertitel vorgewaehlt / entkoppelte Kompression waren seine Wahl). GEMESSEN (Regel D): HandBrake 1.11 Sprach-Schalter an einer ffmpeg- Testdatei (jpn/deu/deu/eng + deu/eng), HandBrake --scan --json Format, TMDb movie/149 + tv/46296 mit append_to_response, wevtutil /f:xml an diesem PC (338x cdrom 7, 8x cdrom 11 in 30 Tagen), makemkvcon --minlength, MakeMKV-Auswahlregel laut forum.makemkv.com t=4386. 348 Tests gruen (vorher 261), Typpruefung und Bau gruen. Oberflaeche folgt im naechsten Schritt. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
d70a81d194
commit
3f7f962dff
+208
-117
@@ -1,170 +1,261 @@
|
||||
// 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'
|
||||
// Tests der Rip-Phase (5.4.0): Die Pipeline führt den RIP und übergibt
|
||||
// danach einen Kompressions-Auftrag an die Warteschlange — das Laufwerk
|
||||
// ist frei, sobald die Roh-Dateien liegen. Die Kompression selbst prüft
|
||||
// test/ablegen.test.ts.
|
||||
//
|
||||
// makemkvcon ist nachgebildet und SCHREIBT Dateien in den Roh-Ordner —
|
||||
// nur so lässt sich prüfen, dass halbe Dateien nach Abbruch und Fehler
|
||||
// wirklich verschwinden.
|
||||
import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } 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'
|
||||
import type { RipAuftragTitel, RipStatus } from '../src/gemeinsam/nachrichten'
|
||||
import type { KompressionsAuftrag } from '../src/kern/ablauf/auftrag'
|
||||
|
||||
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,
|
||||
}))
|
||||
interface Schritt {
|
||||
status: 'success' | 'error' | 'cancelled'
|
||||
/** Diese Datei entsteht im Roh-Ordner (auch bei Abbruch/Fehler: die halbe). */
|
||||
datei?: string
|
||||
bytes?: number
|
||||
lesefehler?: boolean
|
||||
fehler?: string
|
||||
/** Fortschritts-Ticks, die der Lauf meldet. */
|
||||
ticks?: number[]
|
||||
}
|
||||
|
||||
vi.mock('../src/kern/rip/makemkv', () => ({
|
||||
rippen: vi.fn(async () => ripErgebnis.wert),
|
||||
}))
|
||||
vi.mock('../src/kern/komprimieren/handbrake', () => ({
|
||||
komprimieren: vi.fn(async () => encodeErgebnis.wert),
|
||||
}))
|
||||
const plan = vi.hoisted(() => ({ schritte: [] as Schritt[] }))
|
||||
|
||||
import { Pipeline, type PipelineUmgebung } from '../src/kern/ablauf/pipeline'
|
||||
vi.mock('../src/kern/rip/makemkv', async () => {
|
||||
const fs = await import('node:fs')
|
||||
const pfad = await import('node:path')
|
||||
return {
|
||||
rippen: vi.fn(
|
||||
async (o: {
|
||||
zielOrdner: string
|
||||
aufFortschritt?: (p: number) => void
|
||||
}) => {
|
||||
const schritt = plan.schritte.shift() ?? { status: 'success' as const, datei: 'title_t00.mkv' }
|
||||
for (const p of schritt.ticks ?? []) o.aufFortschritt?.(p)
|
||||
if (schritt.datei !== undefined) {
|
||||
fs.writeFileSync(pfad.join(o.zielOrdner, schritt.datei), 'x'.repeat(schritt.bytes ?? 10))
|
||||
}
|
||||
if (schritt.status === 'cancelled') return { status: 'cancelled' }
|
||||
if (schritt.status === 'error') {
|
||||
return { status: 'error', fehler: schritt.fehler ?? 'MakeMKV endete mit Code 1', lesefehler: schritt.lesefehler ?? false }
|
||||
}
|
||||
const dateien = fs
|
||||
.readdirSync(o.zielOrdner)
|
||||
.filter((n: string) => n.endsWith('.mkv'))
|
||||
.sort()
|
||||
.map((n: string) => pfad.join(o.zielOrdner, n))
|
||||
return { status: 'success', dateien, lesefehler: schritt.lesefehler ?? false }
|
||||
},
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
import { Pipeline, halbeDateienEntfernen, type PipelineUmgebung } from '../src/kern/ablauf/pipeline'
|
||||
|
||||
let wegwerf: string[] = []
|
||||
afterEach(() => {
|
||||
plan.schritte = []
|
||||
for (const ordner of wegwerf.splice(0)) rmSync(ordner, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
const GERAET = {
|
||||
id: 'G',
|
||||
name: 'Laufwerk G:',
|
||||
typ: 'bluray' as const,
|
||||
pfad: '\\\\.\\G:',
|
||||
status: 'ready' as const,
|
||||
modell: '',
|
||||
seriennummer: '',
|
||||
groesseBytes: 33e9,
|
||||
grund: '',
|
||||
}
|
||||
|
||||
function umgebung(überschreiben: Partial<PipelineUmgebung> = {}): {
|
||||
pipeline: Pipeline
|
||||
meldungen: RipStatus[]
|
||||
ablage: string
|
||||
eingereiht: KompressionsAuftrag[]
|
||||
endet: Array<{ id: string; phase: string; text: string }>
|
||||
ausgeworfen: string[]
|
||||
} {
|
||||
const ablage = mkdtempSync(join(tmpdir(), 'rippy-pipe-'))
|
||||
wegwerf.push(ablage)
|
||||
const meldungen: RipStatus[] = []
|
||||
const eingereiht: KompressionsAuftrag[] = []
|
||||
const endet: Array<{ id: string; phase: string; text: string }> = []
|
||||
const ausgeworfen: string[] = []
|
||||
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: '',
|
||||
}),
|
||||
geraet: () => GERAET,
|
||||
ablage: () => ablage,
|
||||
einstellungen: () => ({}),
|
||||
melden: (s) => meldungen.push(s),
|
||||
jetzt: () => new Date('2026-08-30T12:00:00Z'),
|
||||
vorgangEinreihen: (auftrag) => {
|
||||
eingereiht.push(auftrag)
|
||||
return eingereiht.length
|
||||
},
|
||||
vorgangEndet: (id, phase, text) => endet.push({ id, phase, text }),
|
||||
nachRipAuswerfen: (id) => ausgeworfen.push(id),
|
||||
...überschreiben,
|
||||
})
|
||||
return { pipeline, meldungen }
|
||||
return { pipeline, meldungen, ablage, eingereiht, endet, ausgeworfen }
|
||||
}
|
||||
|
||||
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 AUFTRAG: RipAuftragTitel[] = [
|
||||
{ nr: 0, rolle: 'folge', dauerS: 3167, groesseBytes: 3e9, audioSprachen: ['jpn'], untertitelSprachen: ['deu'] },
|
||||
{ nr: 1, rolle: 'folge', dauerS: 3249, groesseBytes: 1e9 },
|
||||
]
|
||||
|
||||
describe('Pipeline — die Rip-Phase (5.4.0)', () => {
|
||||
it('rippt Titel für Titel, übergibt den Auftrag und gibt das Laufwerk frei', async () => {
|
||||
plan.schritte = [
|
||||
{ status: 'success', datei: 'title_t00.mkv', bytes: 300 },
|
||||
{ status: 'success', datei: 'title_t01.mkv', bytes: 100 },
|
||||
]
|
||||
const { pipeline, meldungen, ablage, eingereiht, ausgeworfen } = umgebung()
|
||||
const beginn: string[] = []
|
||||
;(pipeline as unknown as { umgebung: PipelineUmgebung }).umgebung.vorgangBeginnt = (v) => beginn.push(v.rohOrdner)
|
||||
await pipeline.starten('G', AUFTRAG, { staffel: 2, ersteFolge: 3 })
|
||||
|
||||
expect(meldungen[0].phase).toBe('rippt')
|
||||
const letzte = meldungen.at(-1)!
|
||||
expect(letzte.phase).toBe('fertig')
|
||||
expect(letzte.text).toContain('eingereiht')
|
||||
expect(letzte.text).toContain('Laufwerk ist frei')
|
||||
expect(letzte.vorgangId).toBe('bluray-2026-08-30T12-00-00')
|
||||
expect(pipeline.laeuft('G')).toBe(false)
|
||||
expect(ausgeworfen).toEqual(['G'])
|
||||
expect(beginn).toEqual([join(ablage, 'roh', 'bluray-2026-08-30T12-00-00')])
|
||||
|
||||
expect(eingereiht).toHaveLength(1)
|
||||
const auftrag = eingereiht[0]
|
||||
expect(auftrag.vorgangId).toBe('bluray-2026-08-30T12-00-00')
|
||||
expect(auftrag.dateien.map((d) => d.rolle)).toEqual(['folge', 'folge'])
|
||||
expect(auftrag.dateien[0].audioSprachen).toEqual(['jpn'])
|
||||
expect(auftrag.dateien[0].untertitelSprachen).toEqual(['deu'])
|
||||
expect(auftrag.dateien[1].audioSprachen).toEqual([])
|
||||
expect(auftrag.dateien.map((d) => d.groesseBytes)).toEqual([300, 100])
|
||||
expect(auftrag.staffel).toBe(2)
|
||||
expect(auftrag.ersteFolge).toBe(3)
|
||||
expect(auftrag.preset).toBe('')
|
||||
})
|
||||
|
||||
it('der Fortschritt läuft durch — nach Dateigröße gewichtet, ohne Rücksprung', async () => {
|
||||
plan.schritte = [
|
||||
{ status: 'success', datei: 'title_t00.mkv', ticks: [50, 100] },
|
||||
{ status: 'success', datei: 'title_t01.mkv', ticks: [50] },
|
||||
]
|
||||
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'])
|
||||
await pipeline.starten('G', AUFTRAG)
|
||||
const prozente = meldungen.filter((m) => m.phase === 'rippt').map((m) => m.prozent)
|
||||
// Titel 0 wiegt 3 GB, Titel 1 wiegt 1 GB: 50 % vom ersten = 37 %,
|
||||
// erster fertig = 75 %, 50 % vom zweiten = 87 %. Nie zurück auf 0.
|
||||
expect(prozente).toContain(37)
|
||||
expect(prozente).toContain(75)
|
||||
expect(prozente).toContain(87)
|
||||
for (let i = 1; i < prozente.length; i++) expect(prozente[i]).toBeGreaterThanOrEqual(prozente[i - 1])
|
||||
})
|
||||
|
||||
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('Abbruch: die halbe Datei verschwindet, der leere Roh-Ordner auch', async () => {
|
||||
plan.schritte = [{ status: 'cancelled', datei: 'title_t00.mkv' }]
|
||||
const { pipeline, meldungen, ablage, eingereiht, endet } = umgebung()
|
||||
await pipeline.starten('G', AUFTRAG)
|
||||
const letzte = meldungen.at(-1)!
|
||||
expect(letzte.phase).toBe('abgebrochen')
|
||||
expect(letzte.text).toContain('Halbe Datei entfernt: title_t00.mkv')
|
||||
expect(existsSync(join(ablage, 'roh', 'bluray-2026-08-30T12-00-00'))).toBe(false)
|
||||
expect(eingereiht).toHaveLength(0)
|
||||
expect(endet).toEqual([{ id: 'bluray-2026-08-30T12-00-00', phase: 'abgebrochen', text: letzte.text }])
|
||||
})
|
||||
|
||||
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('Abbruch nach einem fertigen Titel: der fertige bleibt, die halbe geht', async () => {
|
||||
plan.schritte = [
|
||||
{ status: 'success', datei: 'title_t00.mkv' },
|
||||
{ status: 'cancelled', datei: 'title_t01.mkv' },
|
||||
]
|
||||
const { pipeline, meldungen, ablage } = umgebung()
|
||||
await pipeline.starten('G', AUFTRAG)
|
||||
expect(meldungen.at(-1)!.phase).toBe('abgebrochen')
|
||||
const roh = join(ablage, 'roh', 'bluray-2026-08-30T12-00-00')
|
||||
expect(readdirSync(roh)).toEqual(['title_t00.mkv'])
|
||||
expect(meldungen.at(-1)!.text).toContain('1 fertige Datei(en) bleiben')
|
||||
})
|
||||
|
||||
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('EIN kaputter Titel beendet den Auftrag nicht — seine halbe Datei wird entfernt', async () => {
|
||||
plan.schritte = [
|
||||
{ status: 'error', datei: 'title_t00.mkv', fehler: 'Lesefehler an Byte 56881152' },
|
||||
{ status: 'success', datei: 'title_t01.mkv' },
|
||||
]
|
||||
const { pipeline, meldungen, eingereiht } = umgebung()
|
||||
await pipeline.starten('G', AUFTRAG)
|
||||
const letzte = meldungen.at(-1)!
|
||||
expect(letzte.phase).toBe('fertig')
|
||||
expect(eingereiht[0].dateien.map((d) => d.pfad.split(/[\\/]/).pop())).toEqual(['title_t01.mkv'])
|
||||
expect(eingereiht[0].hinweise.join(' ')).toContain('Nicht gesichert')
|
||||
expect(eingereiht[0].hinweise.join(' ')).toContain('Halbe Dateien entfernt: title_t00.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()
|
||||
it('kein Titel gesichert: Fehler, kein Auftrag, Roh-Ordner weg', async () => {
|
||||
plan.schritte = [{ status: 'error', fehler: 'kaputt' }, { status: 'error', fehler: 'auch kaputt' }]
|
||||
const { pipeline, meldungen, ablage, eingereiht, endet } = umgebung()
|
||||
await pipeline.starten('G', AUFTRAG)
|
||||
expect(meldungen.at(-1)!.phase).toBe('fehler')
|
||||
expect(meldungen.at(-1)!.text).toContain('kaputt')
|
||||
expect(eingereiht).toHaveLength(0)
|
||||
expect(endet[0].phase).toBe('fehler')
|
||||
expect(existsSync(join(ablage, 'roh', 'bluray-2026-08-30T12-00-00'))).toBe(false)
|
||||
})
|
||||
|
||||
it('Lesefehler reisen bis in den Auftrag und die Fertig-Meldung', async () => {
|
||||
plan.schritte = [{ status: 'success', datei: 'title_t00.mkv', lesefehler: true }]
|
||||
const { pipeline, meldungen, eingereiht } = umgebung()
|
||||
await pipeline.starten('G', [AUFTRAG[0]])
|
||||
expect(meldungen.at(-1)!.lesefehler).toBe(true)
|
||||
expect(meldungen.at(-1)!.text).toContain('nicht lesen')
|
||||
expect(eingereiht[0].lesefehler).toBe(true)
|
||||
})
|
||||
|
||||
it('ohne Auftrag (alles rippen): eine Datei, Rolle Hauptfilm, Staffel von der Disc', async () => {
|
||||
plan.schritte = [{ status: 'success', datei: 'title_t00.mkv' }]
|
||||
const { pipeline, eingereiht } = umgebung({ discAngaben: () => ({ staffel: 3, discNr: 2 }) })
|
||||
await pipeline.starten('G')
|
||||
const letzte = meldungen.at(-1)
|
||||
expect(letzte?.lesefehler).toBe(true)
|
||||
expect(letzte?.text).toContain('nicht lesen')
|
||||
expect(eingereiht[0].dateien).toHaveLength(1)
|
||||
expect(eingereiht[0].dateien[0].rolle).toBe('hauptfilm')
|
||||
expect(eingereiht[0].staffel).toBe(3)
|
||||
expect(eingereiht[0].discNr).toBe(2)
|
||||
})
|
||||
|
||||
it('ohne Empfänger für den Auftrag sagt die Meldung, wo die Roh-Dateien liegen', async () => {
|
||||
plan.schritte = [{ status: 'success', datei: 'title_t00.mkv' }]
|
||||
const { pipeline, meldungen } = umgebung({ vorgangEinreihen: undefined })
|
||||
await pipeline.starten('G')
|
||||
expect(meldungen.at(-1)!.text).toContain('nicht eingereiht')
|
||||
})
|
||||
|
||||
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: '',
|
||||
}),
|
||||
geraet: () => ({ ...GERAET, typ: 'unknown', status: 'empty', groesseBytes: 0 }),
|
||||
})
|
||||
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)
|
||||
it('halbeDateienEntfernen: nur, was NEU dazukam', () => {
|
||||
const ordner = mkdtempSync(join(tmpdir(), 'rippy-halb-'))
|
||||
wegwerf.push(ordner)
|
||||
writeFileSync(join(ordner, 'alt.mkv'), 'a')
|
||||
const vorher = new Set(readdirSync(ordner))
|
||||
writeFileSync(join(ordner, 'neu.mkv'), 'b')
|
||||
expect(halbeDateienEntfernen(ordner, vorher)).toEqual(['neu.mkv'])
|
||||
expect(readdirSync(ordner)).toEqual(['alt.mkv'])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user