// Die Kompressions-Warteschlange (5.4.0) — KONZEPT § 4.2 nannte sie // `auftraege.ts`: Warteschlange, Slots. Seit 5.4.0 sind Rip und Kompression // entkoppelt (Commander-Wahl 01.09.2026: „Nächste Disc rippen, während // die vorige noch komprimiert"). Das Laufwerk ist frei, sobald die // Roh-Dateien liegen; die Kompressionen laufen hier der Reihe nach — // EINE zur Zeit, denn zwei HandBrakes teilen sich dieselbe Grafikkarte und // werden zusammen nicht schneller. // // 5.5.0: Pause (Commander: „zum Beispiel beim Spielen") — der laufende // Film wird fertig gerechnet, der nächste wartet, bis es weitergeht. // // PUR: Was ausgeführt wird, kommt injiziert (ablegen.ts) — die Ampel // prüft das Verhalten der Schlange auf Linux ohne HandBrake. import type { KompressionStand } from '../../gemeinsam/nachrichten' import type { KompressionsAuftrag } from './auftrag' import type { AblageErgebnis, AblageMeldung } from './ablegen' export type Ausfuehren = ( auftrag: KompressionsAuftrag, melden: AblageMeldung, abbruch: AbortSignal, ) => Promise interface Eintrag { auftrag: KompressionsAuftrag titel: string abbruch: AbortController } export interface WarteschlangenStand { laeuft: KompressionStand | null wartend: KompressionStand[] } export class KompressionsWarteschlange { private wartend: Eintrag[] = [] private laufend: Eintrag | null = null private laufendStand: KompressionStand | null = null private arbeitet = false private pausiert = false constructor( private readonly ausfuehren: Ausfuehren, /** Jede Änderung — Einreihen, Fortschritt, Ende — geht hier hinaus. */ private readonly beiStand: (stand: WarteschlangenStand) => void, /** Ein Auftrag ist zu Ende (auch abgebrochen/gescheitert). */ private readonly beiErgebnis: (auftrag: KompressionsAuftrag, ergebnis: AblageErgebnis) => void | Promise, ) {} /** Reiht ein und liefert den Platz (1 = läuft jetzt oder als Nächstes). * Ein Vorgang, der schon drin ist, wird nicht doppelt eingereiht. */ einreihen(auftrag: KompressionsAuftrag, titel: string): number { if (this.laufend?.auftrag.vorgangId === auftrag.vorgangId) return 1 const schon = this.wartend.findIndex((e) => e.auftrag.vorgangId === auftrag.vorgangId) if (schon >= 0) return schon + (this.laufend === null ? 1 : 2) this.wartend.push({ auftrag, titel, abbruch: new AbortController() }) const platz = this.wartend.length + (this.laufend === null ? 0 : 1) this.beiStand(this.stand()) void this.schleife() return platz } /** Bricht einen wartenden (entfernt) oder laufenden (Signal) Auftrag ab. */ abbrechen(vorgangId: string): boolean { const i = this.wartend.findIndex((e) => e.auftrag.vorgangId === vorgangId) if (i >= 0) { const [weg] = this.wartend.splice(i, 1) this.beiStand(this.stand()) void this.beiErgebnis(weg.auftrag, { status: 'abgebrochen', zielOrdner: '', dateien: [], text: 'Aus der Warteschlange genommen — die Roh-Dateien bleiben stehen.', komprimiert: false, preset: '', fertigBytes: 0, hinweise: [], folgenVergeben: [], }) return true } if (this.laufend?.auftrag.vorgangId === vorgangId) { this.laufend.abbruch.abort() return true } return false } /** Pause: Der laufende Auftrag wird fertig, der nächste startet erst nach * `pausieren(false)`. */ pausieren(an: boolean): void { this.pausiert = an if (!an) void this.schleife() } istPausiert(): boolean { return this.pausiert } stand(): WarteschlangenStand { return { laeuft: this.laufendStand === null ? null : { ...this.laufendStand }, wartend: this.wartend.map((e) => ({ vorgangId: e.auftrag.vorgangId, titel: e.titel, prozent: -1, text: this.pausiert ? 'wartet (Pause)' : 'wartet', phase: 'wartet', })), } } laeuftEtwas(): boolean { return this.laufend !== null || this.wartend.length > 0 } /** Liegt dieser Vorgang gerade in der Schlange (wartend oder laufend)? */ kennt(vorgangId: string): boolean { return this.laufend?.auftrag.vorgangId === vorgangId || this.wartend.some((e) => e.auftrag.vorgangId === vorgangId) } private async schleife(): Promise { if (this.arbeitet) return this.arbeitet = true try { while (this.wartend.length > 0 && !this.pausiert) { const eintrag = this.wartend.shift()! this.laufend = eintrag this.laufendStand = { vorgangId: eintrag.auftrag.vorgangId, titel: eintrag.titel, prozent: -1, text: 'Beginne …', phase: 'komprimiert', } this.beiStand(this.stand()) let ergebnis: AblageErgebnis try { ergebnis = await this.ausfuehren( eintrag.auftrag, (stand) => { if (this.laufendStand === null) return this.laufendStand = { ...this.laufendStand, ...stand } this.beiStand(this.stand()) }, eintrag.abbruch.signal, ) } catch (fehler) { // R4: Eine Ausnahme mitten im Encode ist ein lauter Fehlschlag, // kein stilles Ende der Schlange. ergebnis = { status: 'fehler', zielOrdner: '', dateien: [], text: `Kompression abgestürzt: ${String(fehler)} — die Roh-Dateien bleiben stehen.`, komprimiert: false, preset: '', fertigBytes: 0, hinweise: [], folgenVergeben: [], } } this.laufend = null this.laufendStand = null try { await this.beiErgebnis(eintrag.auftrag, ergebnis) } catch (fehler) { console.error(`[warteschlange] Ergebnis-Verarbeitung: ${String(fehler)}`) } this.beiStand(this.stand()) } } finally { this.arbeitet = false } } }