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>
133 lines
4.8 KiB
TypeScript
133 lines
4.8 KiB
TypeScript
// Tests der Kompressions-Warteschlange (kern/ablauf/warteschlange.ts,
|
|
// 5.4.0): eine zur Zeit, der Reihe nach; abbrechen wartend und laufend;
|
|
// eine Ausnahme im Encode beendet nicht die Schlange; kein Doppel.
|
|
import { describe, expect, it } from 'vitest'
|
|
import type { KompressionsAuftrag } from '../src/kern/ablauf/auftrag'
|
|
import type { AblageErgebnis } from '../src/kern/ablauf/ablegen'
|
|
import { KompressionsWarteschlange, type WarteschlangenStand } from '../src/kern/ablauf/warteschlange'
|
|
|
|
function auftrag(id: string): KompressionsAuftrag {
|
|
return {
|
|
vorgangId: id,
|
|
laufwerk: 'G',
|
|
discTyp: 'bluray',
|
|
rohOrdner: `R:\\roh\\${id}`,
|
|
dateien: [],
|
|
zuordnung: null,
|
|
staffel: null,
|
|
ersteFolge: null,
|
|
discNr: null,
|
|
fingerprint: null,
|
|
lesefehler: false,
|
|
hinweise: [],
|
|
begonnen: '2026-08-30T12:00:00Z',
|
|
preset: '',
|
|
zielOrdner: '',
|
|
}
|
|
}
|
|
|
|
function fertig(text = 'ok'): AblageErgebnis {
|
|
return { status: 'fertig', zielOrdner: 'Z', dateien: [], text, komprimiert: true, preset: 'p', fertigBytes: 1, hinweise: [] }
|
|
}
|
|
|
|
/** Ein Encode, den der Test von außen beendet. */
|
|
function steuerbar() {
|
|
const freigaben: Array<() => void> = []
|
|
const gestartet: string[] = []
|
|
const ausfuehren = (a: KompressionsAuftrag, melden: (s: { phase: 'komprimiert'; prozent: number; text: string }) => void, abbruch: AbortSignal) =>
|
|
new Promise<AblageErgebnis>((loesen) => {
|
|
gestartet.push(a.vorgangId)
|
|
melden({ phase: 'komprimiert', prozent: 10, text: 'läuft' })
|
|
abbruch.addEventListener('abort', () => loesen({ ...fertig('abgebrochen'), status: 'abgebrochen' }), { once: true })
|
|
freigaben.push(() => loesen(fertig()))
|
|
})
|
|
return { ausfuehren, freigaben, gestartet }
|
|
}
|
|
|
|
const tick = (): Promise<void> => new Promise((r) => setTimeout(r, 0))
|
|
|
|
describe('KompressionsWarteschlange', () => {
|
|
it('arbeitet der Reihe nach, meldet Stand und Ergebnis, vergibt Plätze', async () => {
|
|
const { ausfuehren, freigaben, gestartet } = steuerbar()
|
|
const staende: WarteschlangenStand[] = []
|
|
const ergebnisse: string[] = []
|
|
const q = new KompressionsWarteschlange(ausfuehren, (s) => staende.push(s), (a, e) => {
|
|
ergebnisse.push(`${a.vorgangId}:${e.status}`)
|
|
})
|
|
expect(q.einreihen(auftrag('a'), 'A')).toBe(1)
|
|
expect(q.einreihen(auftrag('b'), 'B')).toBe(2)
|
|
expect(q.einreihen(auftrag('c'), 'C')).toBe(3)
|
|
await tick()
|
|
expect(gestartet).toEqual(['a'])
|
|
expect(q.stand().laeuft?.vorgangId).toBe('a')
|
|
expect(q.stand().laeuft?.prozent).toBe(10)
|
|
expect(q.stand().wartend.map((w) => w.vorgangId)).toEqual(['b', 'c'])
|
|
expect(q.laeuftEtwas()).toBe(true)
|
|
expect(q.kennt('b')).toBe(true)
|
|
|
|
freigaben[0]()
|
|
await tick()
|
|
expect(gestartet).toEqual(['a', 'b'])
|
|
expect(ergebnisse).toEqual(['a:fertig'])
|
|
freigaben[1]()
|
|
await tick()
|
|
freigaben[2]()
|
|
await tick()
|
|
expect(ergebnisse).toEqual(['a:fertig', 'b:fertig', 'c:fertig'])
|
|
expect(q.laeuftEtwas()).toBe(false)
|
|
expect(q.stand().laeuft).toBeNull()
|
|
expect(staende.length).toBeGreaterThan(3)
|
|
})
|
|
|
|
it('kein Doppel: derselbe Vorgang bekommt seinen alten Platz zurück', async () => {
|
|
const { ausfuehren } = steuerbar()
|
|
const q = new KompressionsWarteschlange(ausfuehren, () => {}, () => {})
|
|
q.einreihen(auftrag('a'), 'A')
|
|
q.einreihen(auftrag('b'), 'B')
|
|
await tick()
|
|
expect(q.einreihen(auftrag('a'), 'A')).toBe(1)
|
|
expect(q.einreihen(auftrag('b'), 'B')).toBe(2)
|
|
expect(q.stand().wartend).toHaveLength(1)
|
|
})
|
|
|
|
it('abbrechen: wartend wird entfernt (mit Ergebnis), laufend bekommt das Signal', async () => {
|
|
const { ausfuehren, freigaben } = steuerbar()
|
|
const ergebnisse: string[] = []
|
|
const q = new KompressionsWarteschlange(ausfuehren, () => {}, (a, e) => {
|
|
ergebnisse.push(`${a.vorgangId}:${e.status}`)
|
|
})
|
|
q.einreihen(auftrag('a'), 'A')
|
|
q.einreihen(auftrag('b'), 'B')
|
|
await tick()
|
|
expect(q.abbrechen('b')).toBe(true)
|
|
expect(ergebnisse).toEqual(['b:abgebrochen'])
|
|
expect(q.stand().wartend).toHaveLength(0)
|
|
expect(q.abbrechen('a')).toBe(true)
|
|
await tick()
|
|
expect(ergebnisse).toEqual(['b:abgebrochen', 'a:abgebrochen'])
|
|
expect(q.abbrechen('gibt-es-nicht')).toBe(false)
|
|
expect(freigaben).toHaveLength(1)
|
|
})
|
|
|
|
it('eine Ausnahme im Encode ist ein lautes Fehler-Ergebnis — die Schlange läuft weiter', async () => {
|
|
let n = 0
|
|
const ergebnisse: string[] = []
|
|
const q = new KompressionsWarteschlange(
|
|
async () => {
|
|
n += 1
|
|
if (n === 1) throw new Error('HandBrake explodiert')
|
|
return fertig()
|
|
},
|
|
() => {},
|
|
(a, e) => {
|
|
ergebnisse.push(`${a.vorgangId}:${e.status}:${e.text.includes('explodiert')}`)
|
|
},
|
|
)
|
|
q.einreihen(auftrag('a'), 'A')
|
|
q.einreihen(auftrag('b'), 'B')
|
|
await tick()
|
|
await tick()
|
|
expect(ergebnisse).toEqual(['a:fehler:true', 'b:fertig:false'])
|
|
})
|
|
})
|