Commander-Wünsche vom 02.09.2026 vor der Freigabe von 5.4.0 (die deshalb nie veröffentlicht wurde): Kern: kern/index.ts ist nur noch Bootstrap und Router; Dienste in kern/dienste/ (Einstellungen, Werkzeuge, Laufwerke, Erkennung, Vorgänge, Speicher, Benachrichtigung, Automatik), Kontext in kern/kontext.ts, Protokoll-Datei in gemeinsam/protokoll.ts, Platz-Rechnung in gemeinsam/platz.ts. Oberfläche: fenster/App.tsx ist nur noch der Rahmen; bausteine.tsx, dialoge/, uebersicht/ (LaufwerkKachel, KompressionsKarte, SpeicherKarte, VerlaufKarte), einstellungen/ (Karten je Funktion, SprachAuswahl), einrichtung/ (ErstEinrichtung, PosterHintergrund). Neu: Bibliothek weg, dafür Verlauf; Speicher-und-Bilanz-Karte (statfs); LED-Fußleiste; MakeMKV-Kopf in die Einstellungen; Sprach-Chips mit Dropdown statt Allgemeinem Preset; Karten Automatik/Benachrichtigung/Allgemein/Update; Durchsuchen bei Werkzeugen; Arbeitsordner lokal (Encode dort, dann in die Ablage); Platz-Prüfung vor dem Rip (Dialog und Kern); Automatik mit Countdown; Serien-Merker über Discs; Rettungs-Abbild mit Nullen nur nach gesehenen Lesefehlern (kern/rip/rettung.ts, Phase rettet, iso:-Quelle); Warteschlange pausieren; Discord/Telegram-Benachrichtigung; Protokoll-Datei je Tag; Fensterlage 80 % mittig, gemerkt; Erst-Einrichtung in acht Schritten vor dem Poster-Grid. Beweise: 413 Tests (neu: speicher, benachrichtigung, automatik, rettung, fensterlage, protokoll, kleinkram-550), drei Typprüfungen, Klick-Beweis mit 29 Schritten inklusive Erst-Einrichtung (zweiter Start mit RIPPY_START_BEREICH=einrichtung), Bilder beweise/klick/01…09. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
737 lines
28 KiB
TypeScript
737 lines
28 KiB
TypeScript
// Das Nachrichten-Schema zwischen den drei Prozessen (KONZEPT-WINDOWS.md § 4.1).
|
||
// NUR Typen und Prüf-Funktionen — keine Logik, keine Abhängigkeiten, denn diese
|
||
// Datei läuft im Fenster UND im Kern.
|
||
//
|
||
// Wege:
|
||
// Kern → Haupt über process.parentPort (KernNachricht)
|
||
// Kern ↔ Fenster über einen direkten MessagePort (KernNachricht/FensterNachricht)
|
||
// Haupt → Fenster über IPC-Kanal 'haupt-status' (HauptStatus)
|
||
// Haupt → Kern über utilityProcess.postMessage (HauptNachricht)
|
||
//
|
||
// Regel seit dem 01.09.2026 (test/waechter.test.ts): JEDE `art:` einer Union
|
||
// steht wörtlich im zugehörigen Prüfer — sonst verwirft der Kern die
|
||
// Nachricht, und niemand erfährt es.
|
||
|
||
/** Zustand der Datenbank, wie der Kern ihn beim Start gemessen hat. */
|
||
export type DbStatus = { ok: true; pfad: string } | { ok: false; fehler: string }
|
||
|
||
// ── Laufwerke (Etappe W-1) ──────────────────────────────────────────────
|
||
export type DiscTyp = 'cd' | 'dvd' | 'bluray' | 'uhd' | 'unknown'
|
||
export type LaufwerkStatus = 'ready' | 'empty' | 'unknown'
|
||
|
||
export interface GeraeteInfo {
|
||
/** Kurze Kennung fürs UI und für Befehle — der Laufwerksbuchstabe. */
|
||
id: string
|
||
name: string
|
||
typ: DiscTyp
|
||
pfad: string
|
||
status: LaufwerkStatus
|
||
modell: string
|
||
seriennummer: string
|
||
/** Größe des eingelegten Mediums in Bytes; 0, wenn unbekannt/leer. */
|
||
groesseBytes: number
|
||
/** Leer, solange alles geht — sonst steht hier, WARUM „unknown" dasteht. */
|
||
grund: string
|
||
}
|
||
|
||
export type WacheEreignis =
|
||
| { art: 'disc-eingelegt'; geraet: GeraeteInfo }
|
||
| { art: 'disc-entnommen'; geraet: GeraeteInfo }
|
||
| { art: 'laufwerk-neu'; geraet: GeraeteInfo }
|
||
| { art: 'laufwerk-weg'; id: string }
|
||
|
||
export type AuswurfGrund = 'ok' | 'kein-zugriff' | 'abgelehnt' | 'bleibt-drin'
|
||
|
||
// ── Rippen (Etappe W-2) ─────────────────────────────────────────────────
|
||
export type RipPhase =
|
||
| 'liest-info'
|
||
/** 5.5.0: Das Rettungs-Abbild wird gelesen — Sektor für Sektor, Nullen für Unlesbares. */
|
||
| 'rettet'
|
||
| 'rippt'
|
||
| 'komprimiert'
|
||
| 'legt-ab'
|
||
| 'fertig'
|
||
| 'fehler'
|
||
| 'abgebrochen'
|
||
|
||
export interface RipStatus {
|
||
/** Laufwerks-Kennung, auf der der Rip läuft. */
|
||
id: string
|
||
phase: RipPhase
|
||
/** 0–100; -1 heißt „noch keine Angabe" (ein echtes 0 % bleibt sichtbar). */
|
||
prozent: number
|
||
text: string
|
||
dateien: string[]
|
||
lesefehler: boolean
|
||
/** Der wievielte gewählte Titel läuft gerade (1-basiert) und wie viele es
|
||
* insgesamt sind — Rippy rippt Titel für Titel. Fehlt bei Audio-CD, ISO
|
||
* und in Fehler-Meldungen; die kennen keine Titel. */
|
||
titelNr?: number
|
||
titelGesamt?: number
|
||
/** Der Lauf klemmt an einer festen Stelle der Disc — ein physischer
|
||
* Defekt, kein Fortschrittsproblem. Fehlt/null heißt: alles in Ordnung. */
|
||
haengt?: { offset: string; wiederholungen: number } | null
|
||
/** Der Vorgang, zu dem dieser Rip gehört (5.4.0) — die Kompression läuft
|
||
* danach unter dieser Kennung in der Warteschlange weiter. */
|
||
vorgangId?: string
|
||
/** 5.5.0: Rippy hat an dieser Disc Lesefehler GESEHEN (Defekt-Wächter,
|
||
* Lesefehler-Meldung, Leseabbruch) — erst dann wird das Rettungs-Abbild
|
||
* angeboten. Eine heile Disc braucht es nicht (Commander 01.09.2026). */
|
||
rettungMoeglich?: boolean
|
||
/** 5.5.0: Stand des Rettungs-Abbilds — unlesbare Sektoren und Stellen. */
|
||
rettung?: { unlesbareSektoren: number; stellen: number }
|
||
}
|
||
|
||
/** Was ein Titel IST — davon hängt ab, WOHIN er abgelegt wird:
|
||
* hauptfilm → Filme/<Titel> (Jahr)/<Titel> (Jahr).mkv
|
||
* extra → Filme/<Titel> (Jahr)/extras/… (Jellyfin/Emby/Kodi)
|
||
* folge → Serien/<Titel>/Season NN/… S01E02.mkv
|
||
* EINE Definition für Kern und Fenster. */
|
||
export type TitelRolle = 'hauptfilm' | 'extra' | 'folge'
|
||
|
||
/** Eine Sprache, wie der Info-Lauf sie an einem Titel gemessen hat
|
||
* (SINFO-Attribute 3/4, parser.ts). `spuren` zählt, wie viele Spuren
|
||
* dieser Sprache es gibt (Stereo + 5.1 = 2). */
|
||
export interface SprachEintrag {
|
||
lang: string
|
||
sprache: string
|
||
spuren: number
|
||
}
|
||
|
||
/** Ein Titel im Rip-Auftrag: welcher, als was — und wie lang.
|
||
* Die Dauer reist mit, weil die Episoden-Zuordnung sie braucht: Rippy
|
||
* gleicht die Laufzeiten gegen die Staffel bei TMDb ab und benennt NUR
|
||
* bei eindeutiger Zuordnung um (§ 4.2). */
|
||
export interface RipAuftragTitel {
|
||
nr: number
|
||
rolle: TitelRolle
|
||
dauerS: number
|
||
/** Größe laut Info-Lauf — gewichtet den durchgehenden Fortschrittsbalken
|
||
* (5.4.0). Fehlt: alle Titel zählen gleich. */
|
||
groesseBytes?: number
|
||
/** Sprachen JE TITEL (5.4.0, Commander-Wunsch: japanischer Ton, deutsche
|
||
* Untertitel). Leer/fehlend heißt: die globale Einstellung gilt. */
|
||
audioSprachen?: string[]
|
||
untertitelSprachen?: string[]
|
||
}
|
||
|
||
/** Was der Nutzer im Auswahl-Dialog über die GANZE Disc sagt (5.4.0):
|
||
* Staffel und erste Folge von Hand — Rippy rät bewusst nicht, aber der
|
||
* Nutzer weiß, dass auf Disc 2 die Folgen 3–4 sind. */
|
||
export interface RipAuftragOptionen {
|
||
/** null/fehlend: was die Disc sagt (bdmt/Label), sonst 1. */
|
||
staffel?: number | null
|
||
/** Fortlaufend ab dieser Folge in Disc-Reihenfolge. null/fehlend:
|
||
* Zuordnung über die TMDb-Laufzeiten wie bisher. */
|
||
ersteFolge?: number | null
|
||
}
|
||
|
||
/** Ein Titel der Disc, wie ihn der Auswahl-Dialog zeigt (§ 6.2). */
|
||
export interface DiscTitelVorschlag {
|
||
nr: number
|
||
dauerS: number
|
||
groesseBytes: number
|
||
kapitel: number
|
||
/** Von Rippy vorausgewählt. */
|
||
gewaehlt: boolean
|
||
/** Als was Rippy ihn einsortieren würde — im Dialog änderbar. */
|
||
rolle: TitelRolle
|
||
/** Warum — Klartext („Folge", „Sammeltitel …", „zu kurz …"). */
|
||
grund: string
|
||
/** Die gemessenen Tonspuren und Untertitel DIESES Titels (5.4.0). */
|
||
sprachen: { audio: SprachEintrag[]; untertitel: SprachEintrag[] }
|
||
}
|
||
|
||
/** Was der Info-Lauf über die Titel der Disc ergab. Er dauert 20–120 s,
|
||
* deshalb trägt der Stand ein eigenes `laeuft` — das Fenster steht in der
|
||
* Zeit nicht stumm da (R2: nichts ohne Auskunft). */
|
||
export interface TitelStand {
|
||
id: string
|
||
laeuft: boolean
|
||
titel: DiscTitelVorschlag[]
|
||
/** Einordnung („Serie: 2 Folgen erwartet, 2 gefunden — passt."). */
|
||
einordnung: string
|
||
/** Rippy ist unsicher — das Fenster fragt dann deutlicher nach. */
|
||
unsicher: boolean
|
||
fehler: string
|
||
}
|
||
|
||
/** Ein Darsteller fürs Kino-Banner (TMDb credits.cast, 5.4.0 gemessen). */
|
||
export interface Darsteller {
|
||
name: string
|
||
rolle: string
|
||
/** TMDb-Profilbild (relativ, z. B. /xCAs….jpg) — '' wenn keins. */
|
||
bildPfad: string
|
||
}
|
||
|
||
/** Was Rippy über die eingelegte Disc herausgefunden hat (§ 5 Schritt 3). */
|
||
export interface DiscInfoStand {
|
||
id: string
|
||
titel: string
|
||
jahr: number | null
|
||
/** 0–1; die Schwelle für „sicher genug" ist einstellbar. */
|
||
confidence: number
|
||
quelle: string
|
||
posterPfad: string
|
||
/** TMDb-Hintergrundbild (Querformat) fürs Kachel-Panorama; '' wenn keins. */
|
||
backdropPfad: string
|
||
/** Kurzbeschreibung (TMDb-Overview) — '' wenn keine. */
|
||
beschreibung: string
|
||
/** Datum des früheren Rips dieser Disc — die Bibliothek erkennt sie am
|
||
* Fingerabdruck wieder (§ 6.6); null wenn neu. */
|
||
schonGerippt: string | null
|
||
/** Nummer der Disc in einer Mehr-Disc-Ausgabe — null, wenn keine dasteht. */
|
||
discNr: number | null
|
||
/** Staffel-Nummer aus Titel oder Label — null, wenn keine dasteht. */
|
||
staffel: number | null
|
||
/** Folgen laut dem Inhaltsverzeichnis der Disc selbst — 0, wenn es keines
|
||
* gibt. Ab zwei gilt „Serie" als bewiesen (FOLGEN_FUER_SERIE). */
|
||
folgen: number
|
||
/** 'movie' | 'tv' | '' — was die Zuordnung sagt. */
|
||
typ: string
|
||
// ── Kino-Banner v2 (5.4.0, alles aus DERSELBEN TMDb-Anfrage gemessen) ──
|
||
/** Freigestellter Schriftzug (TMDb images.logos) — '' wenn keiner. */
|
||
logoPfad: string
|
||
tagline: string
|
||
/** TMDb vote_average (0 = keine). */
|
||
bewertung: number
|
||
/** Deutsche Freigabe („16", „18") — '' wenn unbekannt. */
|
||
fsk: string
|
||
laufzeitMinuten: number
|
||
genres: string[]
|
||
besetzung: Darsteller[]
|
||
/** Was Rippy NACH dem Info-Lauf noch dazugelernt hat („Laufzeit
|
||
* bestätigt: 124 min"), Klartext-Zeilen. */
|
||
hinweise: string[]
|
||
/** 5.5.0 Serien-Merker: Was Rippy von der VORIGEN Disc dieser Serie weiß —
|
||
* Staffel und die Folge, mit der es weitergeht. null, wenn nichts. */
|
||
serienMerker: { staffel: number; naechsteFolge: number; wann: string } | null
|
||
}
|
||
|
||
/** Ein Eintrag der Bibliothek (§ 6.6) — seit 5.5.0 nur noch die
|
||
* Wiedererkennung („schon gerippt am …"), keine eigene Ansicht mehr. */
|
||
export interface BibliothekEintrag {
|
||
fingerprint: string
|
||
titel: string
|
||
jahr: number | null
|
||
typ: string
|
||
groesseBytes: number
|
||
ablageort: string
|
||
datum: string
|
||
dauerS: number
|
||
/** TMDb-Posterpfad — '' wenn keiner bekannt. */
|
||
posterPfad: string
|
||
}
|
||
|
||
/** Ein wählbarer Metadaten-Vorschlag (§ 5 Schritt 4: „nein → fragen"). */
|
||
export interface MetaVorschlag {
|
||
typ: 'movie' | 'tv'
|
||
tmdbId: number
|
||
titel: string
|
||
jahr: number | null
|
||
posterPfad: string
|
||
}
|
||
|
||
/** Auskunft über die Werkzeuge (Katalog + gemessene HandBrake-Listen). */
|
||
export interface WerkzeugAuskunft {
|
||
makemkvcon: string
|
||
handbrake: string
|
||
flac: string
|
||
presets: string[]
|
||
backends: string[]
|
||
}
|
||
|
||
/** Stand einer laufenden Werkzeug-Beschaffung (§ 9: MakeMKV-Update). */
|
||
export interface BeschaffungStand {
|
||
/** Welches Werkzeug — bisher nur 'makemkv'. */
|
||
werkzeug: string
|
||
laeuft: boolean
|
||
text: string
|
||
/** 0–1 während des Downloads; null wenn gerade nicht bezifferbar. */
|
||
anteil: number | null
|
||
fehler: boolean
|
||
}
|
||
|
||
// ── Vorgänge und Kompressions-Warteschlange (5.4.0) ─────────────────────
|
||
/** Der Lebenslauf eines Vorgangs. `wartet` = Roh-Dateien liegen da, die
|
||
* Kompression ist eingereiht; `fertig` = abgelegt (komprimiert ODER
|
||
* verschoben). */
|
||
export type VorgangPhase = 'rippt' | 'wartet' | 'komprimiert' | 'legt-ab' | 'fertig' | 'fehler' | 'abgebrochen'
|
||
|
||
/** Ein Vorgang, wie das Fenster ihn zeigt (Einstellungen → Roh-Dateien,
|
||
* Übersicht → Verlauf). */
|
||
export interface VorgangEintrag {
|
||
id: string
|
||
laufwerk: string
|
||
typ: string
|
||
titel: string
|
||
jahr: number | null
|
||
phase: VorgangPhase
|
||
begonnen: string
|
||
beendet: string | null
|
||
rohOrdner: string
|
||
/** Was der Roh-Ordner JETZT belegt (gemessen, nicht gemerkt). */
|
||
rohBytes: number
|
||
rohGeloescht: boolean
|
||
zielOrdner: string
|
||
fertigBytes: number
|
||
komprimiert: boolean
|
||
preset: string
|
||
fehler: string
|
||
titelZahl: number
|
||
/** Die Roh-Dateien liegen noch da UND der Auftrag ist bekannt. */
|
||
neuKomprimierbar: boolean
|
||
/** Wann die Frist abläuft — '' wenn nie (behalten / nicht fertig). */
|
||
faelligAm: string
|
||
}
|
||
|
||
export interface KompressionStand {
|
||
vorgangId: string
|
||
titel: string
|
||
/** 0–100; -1 = keine Angabe. */
|
||
prozent: number
|
||
text: string
|
||
phase: VorgangPhase
|
||
}
|
||
|
||
/** Ein Ordner unter roh\, den kein Vorgang kennt (Rest aus der Zeit vor
|
||
* 5.4.0 oder von Hand hineingelegt) — nur von Hand löschbar. */
|
||
export interface FremderOrdner {
|
||
ordner: string
|
||
bytes: number
|
||
}
|
||
|
||
/** 5.5.0: Was Rippy insgesamt geschafft hat (aus den Vorgängen). */
|
||
export interface Statistik {
|
||
/** Vorgänge insgesamt (Video-Discs). */
|
||
discs: number
|
||
/** Davon fertig abgelegt. */
|
||
fertige: number
|
||
/** Verlustfrei gesichert, Summe aller Rips (auch wenn Roh inzwischen weg ist). */
|
||
rohBytes: number
|
||
/** Fertig abgelegt, Summe. */
|
||
fertigBytes: number
|
||
/** Summe der Dauer je fertigem Vorgang (Rip-Beginn bis Ablage-Ende) in Sekunden. */
|
||
dauerS: number
|
||
}
|
||
|
||
export interface VorgaengeStand {
|
||
vorgaenge: VorgangEintrag[]
|
||
/** Summe aller Roh-Ordner, die noch da sind. */
|
||
rohGesamtBytes: number
|
||
fremde: FremderOrdner[]
|
||
/** Die Aufbewahrungs-Regel, die gerade gilt (Einstellung rohAufbewahrung). */
|
||
regel: string
|
||
kompression: { laeuft: KompressionStand | null; wartend: KompressionStand[]; pausiert: boolean }
|
||
statistik: Statistik
|
||
}
|
||
|
||
/** Was das Windows-Ereignisprotokoll über ein Laufwerk sagt (5.4.0). */
|
||
export interface LaufwerkBericht {
|
||
/** 'ruhig' | 'disc' | 'hardware' | 'unklar' | 'nicht-lesbar' */
|
||
urteil: string
|
||
text: string
|
||
fehlerhafteBloecke: number
|
||
controllerfehler: number
|
||
sonstige: number
|
||
/** Die jüngsten Zeilen, neueste zuerst („2026-09-01 20:32 · fehlerhafter Block"). */
|
||
letzte: string[]
|
||
/** Betrachteter Zeitraum in Stunden. */
|
||
stunden: number
|
||
}
|
||
|
||
/** 5.5.0: Freier Platz an EINEM Ort — gemessen mit fs.statfs (Node 24). */
|
||
export interface SpeicherPlatz {
|
||
pfad: string
|
||
ok: boolean
|
||
freiBytes: number
|
||
gesamtBytes: number
|
||
/** '' wenn ok — sonst, warum nicht messbar (Pfad fehlt, Netz weg). */
|
||
fehler: string
|
||
}
|
||
|
||
export interface SpeicherStand {
|
||
/** Wo Roh-Dateien und Kompression liegen (Arbeitsordner oder Ablage). */
|
||
arbeit: SpeicherPlatz
|
||
/** Wo Fertiges landet (die Ablage — beim Commander der Server). */
|
||
ablage: SpeicherPlatz
|
||
/** true, wenn ein eigener Arbeitsordner gesetzt ist. */
|
||
getrennt: boolean
|
||
}
|
||
|
||
/** 5.5.0 Automatik: Countdown bis zum Rip ohne Klick (KONZEPT § 5 Zero-Click). */
|
||
export interface AutomatikStand {
|
||
id: string
|
||
aktiv: boolean
|
||
sekunden: number
|
||
text: string
|
||
}
|
||
|
||
/** Nachrichten, die der Kern verschickt. */
|
||
export type KernNachricht =
|
||
| { art: 'kern-bereit'; pid: number; nodeVersion: string; datenbank: DbStatus }
|
||
| { art: 'pong'; zeit: number }
|
||
| { art: 'kern-fehler'; text: string }
|
||
/** Eine Zeile fürs Ereignis-Protokoll OHNE Warnzeichen (5.4.0): „Roh-
|
||
* Ordner von X gelöscht", „Kompression eingereiht" — Auskunft, kein Alarm. */
|
||
| { art: 'kern-hinweis'; text: string }
|
||
| { art: 'laufwerke'; liste: GeraeteInfo[]; ereignisse: WacheEreignis[] }
|
||
| { art: 'auswurf-ergebnis'; id: string; grund: AuswurfGrund; fehler: string | null }
|
||
| { art: 'rip-status'; status: RipStatus }
|
||
| { art: 'disc-info'; info: DiscInfoStand }
|
||
| { art: 'disc-titel'; stand: TitelStand }
|
||
| { art: 'metadaten-vorschlaege'; id: string; vorschlaege: MetaVorschlag[] }
|
||
| { art: 'einstellungen'; werte: Record<string, string> }
|
||
| { art: 'werkzeuge'; auskunft: WerkzeugAuskunft }
|
||
| { art: 'beschaffung-status'; stand: BeschaffungStand }
|
||
/** Nur fürs Haupt: Der Kern hat einen geprüften Installer abgelegt —
|
||
* starten kann ihn nur das Haupt (shell/UAC, § 9). */
|
||
| { art: 'installer-starten'; pfad: string }
|
||
| {
|
||
art: 'schluessel-status'
|
||
stand: {
|
||
quelle: string
|
||
angenommen: boolean | null
|
||
/** Rips laufen trotzdem (Beta-Modus) — Warnung, kein Ausfall. */
|
||
weiterBetrieb: boolean
|
||
gueltigBis: string
|
||
geholtAm: string
|
||
text: string
|
||
/** Die installierte MakeMKV-Fassung (aus MSG 1005) — '' wenn unbekannt. */
|
||
version: string
|
||
}
|
||
}
|
||
/** Alle Vorgänge, die Roh-Belegung und die Kompressions-Warteschlange
|
||
* (5.4.0) — geht auch ans Haupt (Taskleiste, Updates warten). */
|
||
| { art: 'vorgaenge'; stand: VorgaengeStand }
|
||
/** Eine Kompression ist zu Ende — fürs Toast und die Ereignis-Zeile. */
|
||
| { art: 'kompression-ergebnis'; vorgangId: string; titel: string; ok: boolean; text: string }
|
||
| { art: 'laufwerk-gesundheit'; id: string; bericht: LaufwerkBericht }
|
||
/** 5.5.0: freier Platz an Arbeitsordner und Ablage. */
|
||
| { art: 'speicher'; stand: SpeicherStand }
|
||
/** 5.5.0: Automatik-Countdown je Laufwerk. */
|
||
| { art: 'automatik'; stand: AutomatikStand }
|
||
/** 5.5.0: Poster beliebter Filme für den Hintergrund der Erst-Einrichtung. */
|
||
| { art: 'poster-vorschau'; pfade: string[] }
|
||
|
||
/** Nachrichten, die das Fenster über den MessagePort an den Kern schickt. */
|
||
export type FensterNachricht =
|
||
| { art: 'ping'; zeit: number }
|
||
| { art: 'auswerfen'; id: string }
|
||
/** Titel-Lauf anstoßen (§ 6.2): dauert 20–120 s, deshalb erst auf Zuruf
|
||
* und nicht schon beim Einlegen (der 30.08.-Fund „das Erkennen dauert
|
||
* sehr sehr lange"). */
|
||
| { art: 'titel-lesen'; id: string }
|
||
/** `auftrag` leer heißt: alles rippen — wie vor 5.2. */
|
||
| { art: 'rip-start'; id: string; auftrag?: RipAuftragTitel[]; optionen?: RipAuftragOptionen }
|
||
| { art: 'rip-abbruch'; id: string }
|
||
/** Den gerade laufenden Titel aufgeben und mit dem nächsten weitermachen
|
||
* — der Ausweg, wenn die Disc an einer Stelle einen Defekt hat. */
|
||
| { art: 'rip-ueberspringen'; id: string }
|
||
| { art: 'einstellung-setzen'; schluessel: string; wert: string }
|
||
| { art: 'metadaten-suchen'; id: string; text: string }
|
||
| { art: 'zuordnung-setzen'; id: string; vorschlag: MetaVorschlag }
|
||
| { art: 'einstellungen-laden' }
|
||
| { art: 'werkzeuge-laden' }
|
||
/** § 9: MakeMKV über die Quellenkette holen und installieren lassen. */
|
||
| { art: 'makemkv-update' }
|
||
/** Schlüsselkette sofort neu laufen lassen (z. B. nach einer Installation). */
|
||
| { art: 'schluessel-pruefen' }
|
||
// ── 5.4.0: Vorgänge, Roh-Dateien, Warteschlange, Laufwerks-Gesundheit ──
|
||
| { art: 'vorgaenge-laden' }
|
||
/** Den Roh-Ordner EINES Vorgangs jetzt löschen (von Hand). */
|
||
| { art: 'vorgang-roh-loeschen'; vorgangId: string }
|
||
/** „Wiederholen ab Kompression": Roh-Dateien liegen da, ein Preset-Wechsel
|
||
* kostet kein Neulesen mehr. `preset` leer = Einstellung gilt. */
|
||
| { art: 'vorgang-neu-komprimieren'; vorgangId: string; preset?: string }
|
||
/** Einen wartenden oder laufenden Kompressions-Auftrag abbrechen. */
|
||
| { art: 'kompression-abbrechen'; vorgangId: string }
|
||
/** Die Aufräum-Regel sofort anwenden (statt auf den Takt zu warten). */
|
||
| { art: 'roh-aufraeumen' }
|
||
/** Einen Ordner unter roh\, den kein Vorgang kennt, löschen. */
|
||
| { art: 'fremden-ordner-loeschen'; ordner: string }
|
||
/** Das Windows-Ereignisprotokoll zu diesem Laufwerk lesen. */
|
||
| { art: 'laufwerk-pruefen'; id: string }
|
||
// ── 5.5.0 ──
|
||
/** Die Kompressions-Warteschlange anhalten (an) oder weiterlaufen lassen. */
|
||
| { art: 'kompression-pause'; an: boolean }
|
||
/** Den Automatik-Countdown dieses Laufwerks stoppen bzw. sofort rippen. */
|
||
| { art: 'automatik-stopp'; id: string }
|
||
| { art: 'automatik-jetzt'; id: string }
|
||
/** Testnachricht über die eingerichteten Kanäle (Discord, Telegram). */
|
||
| { art: 'benachrichtigung-testen' }
|
||
/** Poster beliebter Filme laden (Erst-Einrichtung). */
|
||
| { art: 'poster-laden' }
|
||
/** Rettungs-Abbild für eine Disc mit GESEHENEN Lesefehlern. */
|
||
| { art: 'rettung-start'; id: string }
|
||
/** Freien Platz neu messen. */
|
||
| { art: 'speicher-laden' }
|
||
|
||
/** Nachrichten, die das Haupt an den Kern schickt. */
|
||
export type HauptNachricht =
|
||
| { art: 'fenster-port' }
|
||
/** Antwort auf 'installer-starten': '' bzw. null heißt gestartet. */
|
||
| { art: 'installer-ergebnis'; fehler: string | null }
|
||
|
||
/** Zustand des Haupt-Prozesses fürs Fenster (Kanal 'haupt-status'). */
|
||
export interface HauptStatus {
|
||
version: string
|
||
/** Prozess-Leine (Job Object, § 3.3): gesetzt oder mit Fehlertext. */
|
||
leine: { ok: boolean; fehler?: string }
|
||
kern: { laeuft: boolean; pid?: number; neustarts: number }
|
||
autostart: boolean
|
||
/** Selbst-Update (§ 6.8). */
|
||
update: UpdateStand
|
||
/** Für die eigene Titelleiste (§ 4.2, seit 5.1.3): Ist das Fenster
|
||
* gerade maximiert? Entscheidet nur, welches Symbol der Knopf zeigt. */
|
||
fensterMaximiert: boolean
|
||
/** 5.5.0: Wo die Protokoll-Dateien liegen — für „Protokoll öffnen". */
|
||
protokollOrdner: string
|
||
}
|
||
|
||
/** Der Stand des Selbst-Updates (§ 6.8) — EINE Definition, die sich Haupt
|
||
* und Fenster teilen. Seit 01.09.2026 trägt sie auch die gefundene
|
||
* Version, ihre Änderungsnotizen und den Ladefortschritt: Der Kanal
|
||
* arbeitete vorher korrekt, aber unsichtbar (Commander-Fund). */
|
||
export interface UpdateStand {
|
||
/** Wann die letzte Prüfung GELANG — '' wenn noch nie. */
|
||
letzteErfolgreichePruefung: string
|
||
text: string
|
||
/** Die gefundene neue Version — '' wenn keine bereitsteht. */
|
||
neueVersion: string
|
||
/** Was in ihr neu ist (aus latest.yml) — '' wenn nichts hinterlegt ist. */
|
||
neuerungen: string
|
||
/** Geladen und wartet auf den Neustart. */
|
||
bereit: boolean
|
||
/** Eine Prüfung oder ein Download läuft gerade. */
|
||
laeuft: boolean
|
||
/** Ladefortschritt in Prozent; -1 heißt: kein Download unterwegs. */
|
||
fortschritt: number
|
||
}
|
||
|
||
function istObjekt(wert: unknown): wert is Record<string, unknown> {
|
||
return typeof wert === 'object' && wert !== null
|
||
}
|
||
|
||
function istText(wert: unknown): wert is string {
|
||
return typeof wert === 'string'
|
||
}
|
||
|
||
function istKennung(wert: unknown): wert is string {
|
||
return typeof wert === 'string' && wert.length > 0
|
||
}
|
||
|
||
export function istKernNachricht(wert: unknown): wert is KernNachricht {
|
||
if (!istObjekt(wert)) return false
|
||
switch (wert.art) {
|
||
case 'kern-bereit':
|
||
return (
|
||
typeof wert.pid === 'number' &&
|
||
typeof wert.nodeVersion === 'string' &&
|
||
istDbStatus(wert.datenbank)
|
||
)
|
||
case 'pong':
|
||
return typeof wert.zeit === 'number'
|
||
case 'kern-fehler':
|
||
case 'kern-hinweis':
|
||
return typeof wert.text === 'string'
|
||
case 'laufwerke':
|
||
return Array.isArray(wert.liste) && Array.isArray(wert.ereignisse)
|
||
case 'auswurf-ergebnis':
|
||
return (
|
||
typeof wert.id === 'string' &&
|
||
typeof wert.grund === 'string' &&
|
||
(wert.fehler === null || typeof wert.fehler === 'string')
|
||
)
|
||
case 'rip-status':
|
||
return istRipStatus(wert.status)
|
||
case 'disc-info':
|
||
return (
|
||
istObjekt(wert.info) &&
|
||
typeof (wert.info as Record<string, unknown>).id === 'string' &&
|
||
typeof (wert.info as Record<string, unknown>).titel === 'string' &&
|
||
typeof (wert.info as Record<string, unknown>).confidence === 'number'
|
||
)
|
||
case 'disc-titel':
|
||
return (
|
||
istObjekt(wert.stand) &&
|
||
typeof (wert.stand as Record<string, unknown>).id === 'string' &&
|
||
typeof (wert.stand as Record<string, unknown>).laeuft === 'boolean' &&
|
||
Array.isArray((wert.stand as Record<string, unknown>).titel)
|
||
)
|
||
case 'einstellungen':
|
||
return istObjekt(wert.werte)
|
||
case 'werkzeuge':
|
||
return istObjekt(wert.auskunft) && Array.isArray((wert.auskunft as Record<string, unknown>).presets)
|
||
case 'schluessel-status':
|
||
return istObjekt(wert.stand) && typeof (wert.stand as Record<string, unknown>).text === 'string'
|
||
case 'beschaffung-status':
|
||
return (
|
||
istObjekt(wert.stand) &&
|
||
typeof (wert.stand as Record<string, unknown>).werkzeug === 'string' &&
|
||
typeof (wert.stand as Record<string, unknown>).text === 'string' &&
|
||
typeof (wert.stand as Record<string, unknown>).laeuft === 'boolean'
|
||
)
|
||
case 'installer-starten':
|
||
return typeof wert.pfad === 'string' && wert.pfad.length > 0
|
||
case 'metadaten-vorschlaege':
|
||
return typeof wert.id === 'string' && Array.isArray(wert.vorschlaege)
|
||
case 'vorgaenge':
|
||
return (
|
||
istObjekt(wert.stand) &&
|
||
Array.isArray((wert.stand as Record<string, unknown>).vorgaenge) &&
|
||
typeof (wert.stand as Record<string, unknown>).rohGesamtBytes === 'number' &&
|
||
istObjekt((wert.stand as Record<string, unknown>).kompression) &&
|
||
istObjekt((wert.stand as Record<string, unknown>).statistik)
|
||
)
|
||
case 'kompression-ergebnis':
|
||
return (
|
||
istKennung(wert.vorgangId) &&
|
||
typeof wert.titel === 'string' &&
|
||
typeof wert.ok === 'boolean' &&
|
||
typeof wert.text === 'string'
|
||
)
|
||
case 'laufwerk-gesundheit':
|
||
return (
|
||
istKennung(wert.id) &&
|
||
istObjekt(wert.bericht) &&
|
||
typeof (wert.bericht as Record<string, unknown>).urteil === 'string' &&
|
||
typeof (wert.bericht as Record<string, unknown>).text === 'string' &&
|
||
Array.isArray((wert.bericht as Record<string, unknown>).letzte)
|
||
)
|
||
case 'speicher':
|
||
return (
|
||
istObjekt(wert.stand) &&
|
||
istObjekt((wert.stand as Record<string, unknown>).arbeit) &&
|
||
istObjekt((wert.stand as Record<string, unknown>).ablage) &&
|
||
typeof (wert.stand as Record<string, unknown>).getrennt === 'boolean'
|
||
)
|
||
case 'automatik':
|
||
return (
|
||
istObjekt(wert.stand) &&
|
||
istKennung((wert.stand as Record<string, unknown>).id) &&
|
||
typeof (wert.stand as Record<string, unknown>).aktiv === 'boolean' &&
|
||
typeof (wert.stand as Record<string, unknown>).sekunden === 'number' &&
|
||
typeof (wert.stand as Record<string, unknown>).text === 'string'
|
||
)
|
||
case 'poster-vorschau':
|
||
return Array.isArray(wert.pfade) && wert.pfade.every(istText)
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
|
||
export function istMetaVorschlag(wert: unknown): wert is MetaVorschlag {
|
||
if (!istObjekt(wert)) return false
|
||
return (
|
||
(wert.typ === 'movie' || wert.typ === 'tv') &&
|
||
typeof wert.tmdbId === 'number' &&
|
||
typeof wert.titel === 'string' &&
|
||
(wert.jahr === null || typeof wert.jahr === 'number') &&
|
||
typeof wert.posterPfad === 'string'
|
||
)
|
||
}
|
||
|
||
export function istRipStatus(wert: unknown): wert is RipStatus {
|
||
if (!istObjekt(wert)) return false
|
||
return (
|
||
typeof wert.id === 'string' &&
|
||
typeof wert.phase === 'string' &&
|
||
typeof wert.prozent === 'number' &&
|
||
typeof wert.text === 'string' &&
|
||
Array.isArray(wert.dateien) &&
|
||
typeof wert.lesefehler === 'boolean'
|
||
)
|
||
}
|
||
|
||
export function istDbStatus(wert: unknown): wert is DbStatus {
|
||
if (!istObjekt(wert)) return false
|
||
if (wert.ok === true) return typeof wert.pfad === 'string'
|
||
if (wert.ok === false) return typeof wert.fehler === 'string'
|
||
return false
|
||
}
|
||
|
||
/** Ein Rip-Auftragstitel, wie das Fenster ihn schickt — die Sprachlisten
|
||
* sind optional, aber wenn sie da sind, müssen es Textlisten sein. */
|
||
export function istRipAuftragTitel(wert: unknown): wert is RipAuftragTitel {
|
||
if (!istObjekt(wert)) return false
|
||
if (typeof wert.nr !== 'number' || typeof wert.dauerS !== 'number') return false
|
||
if (wert.rolle !== 'hauptfilm' && wert.rolle !== 'extra' && wert.rolle !== 'folge') return false
|
||
if (wert.groesseBytes !== undefined && typeof wert.groesseBytes !== 'number') return false
|
||
for (const feld of ['audioSprachen', 'untertitelSprachen']) {
|
||
const liste = wert[feld]
|
||
if (liste === undefined) continue
|
||
if (!Array.isArray(liste) || !liste.every(istText)) return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
export function istFensterNachricht(wert: unknown): wert is FensterNachricht {
|
||
if (!istObjekt(wert)) return false
|
||
if (wert.art === 'ping') return typeof wert.zeit === 'number'
|
||
if (wert.art === 'rip-start') {
|
||
if (!istKennung(wert.id)) return false
|
||
if (wert.auftrag !== undefined && (!Array.isArray(wert.auftrag) || !wert.auftrag.every(istRipAuftragTitel))) {
|
||
return false
|
||
}
|
||
if (wert.optionen !== undefined) {
|
||
if (!istObjekt(wert.optionen)) return false
|
||
const o = wert.optionen
|
||
const zahlOderNull = (v: unknown): boolean => v === undefined || v === null || typeof v === 'number'
|
||
if (!zahlOderNull(o.staffel) || !zahlOderNull(o.ersteFolge)) return false
|
||
}
|
||
return true
|
||
}
|
||
if (
|
||
wert.art === 'auswerfen' ||
|
||
wert.art === 'titel-lesen' ||
|
||
wert.art === 'rip-abbruch' ||
|
||
wert.art === 'rip-ueberspringen' ||
|
||
wert.art === 'laufwerk-pruefen' ||
|
||
wert.art === 'automatik-stopp' ||
|
||
wert.art === 'automatik-jetzt' ||
|
||
wert.art === 'rettung-start'
|
||
) {
|
||
return istKennung(wert.id)
|
||
}
|
||
if (wert.art === 'kompression-pause') {
|
||
return typeof wert.an === 'boolean'
|
||
}
|
||
if (wert.art === 'einstellung-setzen') {
|
||
return istKennung(wert.schluessel) && typeof wert.wert === 'string'
|
||
}
|
||
if (wert.art === 'metadaten-suchen') {
|
||
return istKennung(wert.id) && typeof wert.text === 'string'
|
||
}
|
||
if (wert.art === 'zuordnung-setzen') {
|
||
return istKennung(wert.id) && istMetaVorschlag(wert.vorschlag)
|
||
}
|
||
if (wert.art === 'vorgang-roh-loeschen' || wert.art === 'kompression-abbrechen') {
|
||
return istKennung(wert.vorgangId)
|
||
}
|
||
if (wert.art === 'vorgang-neu-komprimieren') {
|
||
return istKennung(wert.vorgangId) && (wert.preset === undefined || typeof wert.preset === 'string')
|
||
}
|
||
if (wert.art === 'fremden-ordner-loeschen') {
|
||
return istKennung(wert.ordner)
|
||
}
|
||
return (
|
||
wert.art === 'einstellungen-laden' ||
|
||
wert.art === 'werkzeuge-laden' ||
|
||
wert.art === 'makemkv-update' ||
|
||
wert.art === 'schluessel-pruefen' ||
|
||
wert.art === 'vorgaenge-laden' ||
|
||
wert.art === 'roh-aufraeumen' ||
|
||
wert.art === 'benachrichtigung-testen' ||
|
||
wert.art === 'poster-laden' ||
|
||
wert.art === 'speicher-laden'
|
||
)
|
||
}
|
||
|
||
export function istHauptNachricht(wert: unknown): wert is HauptNachricht {
|
||
if (!istObjekt(wert)) return false
|
||
if (wert.art === 'fenster-port') return true
|
||
if (wert.art === 'installer-ergebnis') return wert.fehler === null || typeof wert.fehler === 'string'
|
||
return false
|
||
}
|