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
@@ -0,0 +1,132 @@
|
||||
// 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'])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user