// Die Werkzeuge (§ 9): wo makemkvcon, HandBrakeCLI und flac liegen, die // gemessenen HandBrake-Listen (Presets, Encoder), die MakeMKV-Schlüsselkette // (§ 9.1) samt Auswahlregel (5.4.0) und das MakeMKV-Selbst-Update. import type { KernNachricht } from '../../gemeinsam/nachrichten' import type { KernKontext } from '../kontext' import { finden } from '../werkzeuge/katalog' import type { EinstellungsDienst } from './einstellungen' export type SchluesselStand = Extract['stand'] export class WerkzeugDienst { /** Die gemessene Auskunft bleibt liegen — die Pipeline braucht sie für die * Preset-Empfehlung, auch wenn gerade kein Fenster offen ist. */ auskunft: { presets: string[]; backends: string[] } = { presets: [], backends: [] } schluesselStand: SchluesselStand | null = null private beschaffungLaeuft = false private installerAntwort: ((fehler: string | null) => void) | null = null /** 5.7.0 (F16): Die offene Ja/Nein-Frage der Beschaffung — null, wenn keine. */ private frageAntwort: ((ja: boolean) => void) | null = null private letzterBeschaffungsText = '' constructor( private readonly kontext: KernKontext, private readonly einstellungen: EinstellungsDienst, ) {} // 5.7.0 (Durchsicht F22): Ein Fund gilt 60 s. Vorher lief bei fehlendem // MakeMKV bei JEDEM Aufruf die Suchkette bis in die Registry (dreimal // `reg query … /s`, synchron, bis 45 s) — und makemkvcon() wird oft // gerufen: je Pipeline-Start, je Titel-Lauf, im Update-Lauf alle 15 s. private gefunden = new Map() private static readonly FUND_FRISCH_MS = 60_000 private suchen(name: string, schluessel: string): string { const eingestellt = this.einstellungen.lesen(schluessel) const alt = this.gefunden.get(name) if (alt !== undefined && alt.eingestellt === eingestellt && Date.now() - alt.wann < WerkzeugDienst.FUND_FRISCH_MS) return alt.pfad const pfad = finden(name, eingestellt) this.gefunden.set(name, { pfad, eingestellt, wann: Date.now() }) return pfad } /** Nach einer Installation oder einem neuen Pfad: frisch suchen. */ neuSuchen(): void { this.gefunden.clear() } makemkvcon(): string { return this.suchen('makemkv', 'werkzeug.makemkv') } handbrake(): string { return this.suchen('handbrake', 'werkzeug.handbrake') } flac(): string { return this.suchen('flac', 'werkzeug.flac') } /** Werkzeuge suchen, HandBrake messen, ans Fenster. */ async senden(): Promise { this.neuSuchen() const makemkvcon = this.makemkvcon() const handbrake = this.handbrake() const flac = this.flac() const { encoderMessen } = await import('../komprimieren/encoder') const gemessen = await encoderMessen(handbrake) this.auskunft = { presets: gemessen.presets, backends: gemessen.backends } this.kontext.anFenster({ art: 'werkzeuge', auskunft: { makemkvcon, handbrake, flac, presets: gemessen.presets, backends: gemessen.backends }, }) this.kontext.protokoll.schreiben( `Werkzeuge: makemkvcon=${makemkvcon || 'FEHLT'} handbrake=${handbrake || 'FEHLT'} flac=${flac || 'FEHLT'} · ${gemessen.presets.length} Presets · Encoder ${gemessen.backends.join(',') || '—'}`, ) } // ── MakeMKV-Schlüsselkette (§ 9.1): beim Start und täglich ────────────── async schluesselLauf(): Promise { if (process.platform !== 'win32') return try { const { schluesselKette } = await import('../werkzeuge/schluessel') this.schluesselStand = await schluesselKette( this.makemkvcon(), this.einstellungen.lesen('makemkvAppKey'), new Date().toISOString(), ) this.kontext.anHauptUndFenster({ art: 'schluessel-status', stand: this.schluesselStand }) this.kontext.protokoll.schreiben(`Schlüssel: ${this.schluesselStand.text}`) } catch (fehler) { // R4: Der Schlüssel-Lauf ist Komfort — sein Scheitern wird gesagt. this.kontext.fehler(`Schlüsselkette: ${String(fehler)}`) } } /** MakeMKVs Spur-Auswahlregel sicherstellen (5.4.0) — sonst fehlen * Sprachen in der Roh-Datei, die der Dialog anbot. */ async spurauswahlLauf(): Promise { if (process.platform !== 'win32') return try { const { spurauswahlSicherstellen } = await import('../werkzeuge/spurauswahl') const stand = spurauswahlSicherstellen() if (stand.stand === 'eigen' || stand.stand === 'fehler') { this.kontext.fehler(`MakeMKV-Auswahlregel: ${stand.text}`) } else if (stand.stand === 'gesetzt') { this.kontext.hinweis(stand.text) } else { this.kontext.protokoll.schreiben(stand.text) } } catch (fehler) { this.kontext.fehler(`MakeMKV-Auswahlregel: ${String(fehler)}`) } } /** Antwort des Haupts auf 'installer-starten'. */ installerErgebnis(fehler: string | null): void { this.installerAntwort?.(fehler ?? '') this.installerAntwort = null } private beschaffungMelden(text: string, anteil: number | null, laeuft: boolean, fehler = false, frage?: string): void { this.letzterBeschaffungsText = text this.kontext.anFenster({ art: 'beschaffung-status', stand: { werkzeug: 'makemkv', laeuft, text, anteil, fehler, ...(frage !== undefined ? { frage } : {}) } }) this.kontext.protokoll.schreiben(`MakeMKV-Update: ${text}${frage !== undefined ? ` — FRAGE: ${frage}` : ''}`) } /** Antwort des Fensters auf eine offene Frage (beschaffung-antwort). */ beschaffungAntwort(ja: boolean): void { const antwort = this.frageAntwort this.frageAntwort = null if (antwort === null) { this.kontext.hinweis('Es ist keine Frage der MakeMKV-Beschaffung offen.') return } antwort(ja) } /** Eine Ja/Nein-Frage ans Fenster; ohne Antwort in zehn Minuten gilt Nein. */ private frage(text: string): Promise { return new Promise((fertig) => { this.frageAntwort = (ja) => { clearTimeout(zeiger) this.beschaffungMelden(ja ? 'Freigegeben — die Archiv-Kopie wird geladen.' : 'Nicht freigegeben.', null, true) fertig(ja) } const zeiger = setTimeout(() => { if (this.frageAntwort === null) return this.frageAntwort = null this.beschaffungMelden('Keine Antwort — die Archiv-Kopie wird nicht geladen.', null, true) fertig(false) }, 10 * 60_000) zeiger.unref() this.beschaffungMelden(this.letzterBeschaffungsText, null, true, false, text) }) } // ── MakeMKV-Update (§ 9): holen, prüfen, installieren lassen ──────────── // Der Kern lädt und PRÜFT die Datei; STARTEN kann sie nur das Haupt // (shell → UAC-Abfrage; der erhöhte Installer hängt dadurch nicht an der // Prozess-Leine und überlebt ein geschlossenes Rippy). async makemkvUpdateLauf(): Promise { if (this.beschaffungLaeuft) { this.beschaffungMelden('Ein MakeMKV-Update läuft bereits.', null, true) return } this.beschaffungLaeuft = true try { const { istNeuer, makemkvNeuesteVersion, makemkvInstallerHolen, versionsangaben } = await import( '../werkzeuge/beschaffen' ) this.beschaffungMelden('Nach der neuesten MakeMKV-Fassung wird gesucht …', null, true) const neueste = await makemkvNeuesteVersion() if (neueste.length === 0) { this.beschaffungMelden( 'Keine der Quellen konnte sagen, welche Fassung aktuell ist (Hersteller-Seite, Forum, Archiv). ' + 'Ausweg: MakeMKV von Hand von makemkv.com laden und installieren; Rippy findet es danach von allein.', null, false, true, ) this.beschaffungLaeuft = false return } const installiert = this.schluesselStand?.version ?? '' if (installiert.length > 0 && !istNeuer(neueste, installiert)) { // Ehrlich bleiben (31.08.2026 gemessen: 1.18.4 ist installiert UND // die neueste angekündigte — die Beta ist nur abgelaufen): Ein // Download derselben Fassung würde nichts lösen. this.beschaffungMelden( `MakeMKV ${installiert} ist bereits die neueste angekündigte Fassung. ` + 'Sobald der Hersteller eine neuere veröffentlicht, holt dieser Knopf sie — ' + 'bis dahin rippt der Beta-Modus weiter.', null, false, ) this.beschaffungLaeuft = false return } // Merken, woran eine gelungene Installation zu erkennen ist: Die // Versions-Ressource der gefundenen EXE ändert sich (oder es gibt // erstmals eine). const alteExe = this.makemkvcon() const alteVersion = alteExe.length > 0 ? versionsangaben(alteExe).version : '' const ergebnis = await makemkvInstallerHolen( neueste, (text, anteil) => this.beschaffungMelden(text, anteil, true), '', undefined, undefined, undefined, // F16: Das Archiv liefert eine unsignierte EXE, die gleich mit // Adminrechten läuft — das entscheidet der Nutzer, nicht Rippy. (quelle) => this.frage( `makemkv.com antwortet nicht. Soll Rippy den MakeMKV-Installer ${neueste} aus dem ${quelle} laden? Das ist eine Kopie von fremder Hand, geprüft wird nur, dass sie ein MakeMKV-Programm ist — er läuft danach mit Adminrechten. Wer sicher gehen will, lädt später selbst von makemkv.com.`, ), ) this.beschaffungMelden( `Geprüft (Quelle: ${ergebnis.quelle}, ${(ergebnis.bytes / 1024 ** 2).toFixed(1)} MB, SHA-256 ${ergebnis.sha256}) — der Installer wird gestartet …`, null, true, ) const startFehler = await new Promise((fertig) => { this.installerAntwort = fertig process.parentPort.postMessage({ art: 'installer-starten', pfad: ergebnis.pfad }) // Falls das Haupt nie antwortet, bleibt der Lauf nicht ewig hängen. setTimeout(() => fertig('Keine Antwort vom Haupt-Prozess'), 30_000).unref() }) this.installerAntwort = null if (startFehler !== null && startFehler.length > 0) { this.beschaffungMelden(`Der Installer wurde nicht gestartet: ${startFehler}`, null, false, true) this.beschaffungLaeuft = false return } this.beschaffungMelden( `Der MakeMKV-Installer ${ergebnis.version} läuft — bitte die Windows-Nachfrage bestätigen und durchklicken. ` + 'Rippy merkt die fertige Installation von allein und prüft den Schlüssel neu.', null, true, ) // Warten, bis die Installation durch ist: Die Versions-Ressource der // EXE ändert sich. Alle 15 s nachsehen, höchstens 20 Minuten. let runden = 0 const zeiger = setInterval(() => { runden += 1 const exe = this.makemkvcon() const jetztVersion = exe.length > 0 ? versionsangaben(exe).version : '' if (jetztVersion.length > 0 && jetztVersion !== alteVersion) { clearInterval(zeiger) this.beschaffungLaeuft = false this.beschaffungMelden(`MakeMKV ${jetztVersion} ist installiert — der Schlüssel wird neu geprüft.`, null, false) void this.schluesselLauf().then(() => this.senden()) } else if (runden >= 80) { clearInterval(zeiger) this.beschaffungLaeuft = false this.beschaffungMelden( 'Die fertige Installation wurde nicht bemerkt — nach dem Durchklicken hilft „Schlüssel neu prüfen".', null, false, ) } }, 15_000) zeiger.unref() } catch (fehler) { this.beschaffungMelden(fehler instanceof Error ? fehler.message : String(fehler), null, false, true) this.beschaffungLaeuft = false } } }