// 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/ (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' /** 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[] /** 5.6.0, nur für Extras: WELCHE Untertitel-Sprache beim Abspielen * vorgewählt ist (weiche Spur, nie eingebrannt). null = keine Vorwahl; * fehlend = Rippys Regel (fremder Ton → Wunschsprache). */ untertitelVorwahl?: string | null /** 5.6.0: Dieser Titel gehört zu einem ANDEREN Film als die Disc-Zuordnung * (Double Feature) — er bekommt eigenen Roh-Ordner, Vorgang und Zielordner. */ film?: MetaVorschlag } /** 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 /** 5.7.0 (F16): Rippy wartet auf ein Ja/Nein — z. B. „Archiv-Kopie laden?". * Fehlt: keine Frage offen. */ frage?: string } // ── 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 /** 5.6.0: Restzeit der laufenden Datei in Sekunden, wie HandBrake sie * meldet (ETA); fehlend/-1 = noch keine Angabe. */ restS?: number } /** 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 } | { 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 } /** 5.7.0 (F7): NUR die Warteschlange — jeder Prozent-Tick der Kompression * kommt so, ohne den teuren vollen Vorgangs-Stand (Ordnerläufe, statfs). */ | { art: 'kompression-stand'; kompression: VorgaengeStand['kompression'] } /** 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.7.0 (F16): Antwort auf eine Frage der MakeMKV-Beschaffung. */ | { art: 'beschaffung-antwort'; ja: boolean } // ── 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 { 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).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 '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) case 'vorgaenge': return ( istObjekt(wert.stand) && Array.isArray((wert.stand as Record).vorgaenge) && typeof (wert.stand as Record).rohGesamtBytes === 'number' && istObjekt((wert.stand as Record).kompression) && istObjekt((wert.stand as Record).statistik) ) case 'kompression-stand': return istObjekt(wert.kompression) && Array.isArray((wert.kompression as Record).wartend) 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).urteil === 'string' && typeof (wert.bericht as Record).text === 'string' && Array.isArray((wert.bericht as Record).letzte) ) case 'speicher': return ( istObjekt(wert.stand) && istObjekt((wert.stand as Record).arbeit) && istObjekt((wert.stand as Record).ablage) && typeof (wert.stand as Record).getrennt === 'boolean' ) case 'automatik': return ( istObjekt(wert.stand) && istKennung((wert.stand as Record).id) && typeof (wert.stand as Record).aktiv === 'boolean' && typeof (wert.stand as Record).sekunden === 'number' && typeof (wert.stand as Record).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 === 'beschaffung-antwort') { return typeof wert.ja === '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 }