// 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) /** 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' | '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 } /** Was ein Titel IST — davon hängt ab, WOHIN er abgelegt wird: * hauptfilm → Filme/ (Jahr)/ (Jahr).mkv * extra → Filme/ (Jahr)/extras/… (Jellyfin/Emby/Kodi) * folge → Serien//Season NN/… S01E02.mkv * EINE Definition für Kern und Fenster. */ export type TitelRolle = 'hauptfilm' | 'extra' | 'folge' /** 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 } /** 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 } /** 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 } /** 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 } /** Ein Eintrag der Bibliothek (§ 6.6). */ export interface BibliothekEintrag { fingerprint: string titel: string jahr: number | null typ: string groesseBytes: number ablageort: string datum: string dauerS: number /** TMDb-Posterpfad fürs Bibliotheks-Regal — '' 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 } /** 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 } | { 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 } | { art: 'werkzeuge'; auskunft: WerkzeugAuskunft } | { art: 'bibliothek'; eintraege: BibliothekEintrag[] } | { 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 } } /** 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[] } | { 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' } | { art: 'bibliothek-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' } /** 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 } /** 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 { return typeof wert === 'object' && wert !== null } 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': 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).id === 'string' && typeof (wert.info as Record).titel === 'string' && typeof (wert.info as Record).confidence === 'number' ) case 'disc-titel': return ( istObjekt(wert.stand) && typeof (wert.stand as Record).id === 'string' && typeof (wert.stand as Record).laeuft === 'boolean' && Array.isArray((wert.stand as Record).titel) ) case 'einstellungen': return istObjekt(wert.werte) case 'werkzeuge': return istObjekt(wert.auskunft) && Array.isArray((wert.auskunft as Record).presets) case 'bibliothek': return Array.isArray(wert.eintraege) case 'schluessel-status': return istObjekt(wert.stand) && typeof (wert.stand as Record).text === 'string' case 'beschaffung-status': return ( istObjekt(wert.stand) && typeof (wert.stand as Record).werkzeug === 'string' && typeof (wert.stand as Record).text === 'string' && typeof (wert.stand as Record).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) 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 } 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 === 'auswerfen' || wert.art === 'rip-start' || wert.art === 'rip-abbruch') { return typeof wert.id === 'string' && wert.id.length > 0 } if (wert.art === 'einstellung-setzen') { return typeof wert.schluessel === 'string' && wert.schluessel.length > 0 && typeof wert.wert === 'string' } if (wert.art === 'metadaten-suchen') { return typeof wert.id === 'string' && wert.id.length > 0 && typeof wert.text === 'string' } if (wert.art === 'zuordnung-setzen') { return typeof wert.id === 'string' && wert.id.length > 0 && istMetaVorschlag(wert.vorschlag) } return ( wert.art === 'einstellungen-laden' || wert.art === 'werkzeuge-laden' || wert.art === 'bibliothek-laden' || wert.art === 'makemkv-update' || wert.art === 'schluessel-pruefen' ) } 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 }