// Die MakeMKV-Schlüsselkette (KONZEPT § 9.1) — Vorlagen makemkv_key.py // (Forum-Abruf) und makemkv_aufruf.py (Registry-Ablage + Prüfung, beides // am Commander-PC gemessen): // // 1. Ein eingetragener Dauerlizenz-Schlüssel schlägt alles. // 2. Sonst der Beta-Key aus dem Forum-Thread des Herstellers (t=1053). // 3. Abgelegt wird in HKCU\Software\MakeMKV\app_Key — die settings.conf // ist der Linux-Weg; unter Windows liest MakeMKV die Registry. // 4. GEFRAGT statt gerechnet: makemkvcon selbst sagt per MSG 5020/5021/ // 5051, ob der Key taugt. Ein ABGELEHNTER Key ist schlimmer als // keiner (0 Titel, kein Laufwerk) — er wird dann ZURÜCKGENOMMEN. // 5. app_UpdateEnable=1 nur-wenn-fehlt: Über diesen Kanal holt MakeMKV // die 4K-UHD-Disc-Schlüssel nach (MSG 3338; unter Windows gemessen). import { spawn, spawnSync } from 'node:child_process' import { textVon } from '../rip/parser' import { KEY_ABGELEHNT, KRITISCHE_CODES } from '../rip/parser' export const FORUM_URL = 'https://forum.makemkv.com/forum/viewtopic.php?t=1053' export const KEY_MUSTER = /T-[A-Za-z0-9_]{50,}/ const REGISTRY_PFAD = 'HKCU\\Software\\MakeMKV' const KEY_NAME = 'app_Key' /** Den Beta-Key aus dem Forum-HTML ziehen (pur). */ export function extractKey(html: string): string { const treffer = KEY_MUSTER.exec(html ?? '') return treffer === null ? '' : treffer[0] } /** „valid until/through " aus dem Thread — best effort, Rohtext. * § 9.1 will das Ablaufdatum sichtbar; die verlässliche Instanz bleibt * aber makemkvcon selbst (MSG 5051 = abgelaufen). */ export function extractGueltigBis(html: string): string { const treffer = /valid\s+(?:until|through)\s+([A-Za-zäöüÄÖÜ]+\s+\d{1,2}(?:st|nd|rd|th)?,?\s+\d{4})/i.exec( html ?? '', ) return treffer === null ? '' : treffer[1].replace(/(\d)(st|nd|rd|th)/, '$1') } export interface ForumKey { key: string gueltigBis: string } /** Der aktuelle Beta-Key aus dem Forum — 8-s-Zeitgrenze je Versuch: Das * Forum fiel bei der Messung in zwei von drei Abrufen aus (§ 9). */ export async function betaKeyHolen(holen: typeof fetch = fetch): Promise { for (let versuch = 0; versuch < 3; versuch++) { try { const antwort = await holen(FORUM_URL, { signal: AbortSignal.timeout(8_000), headers: { 'User-Agent': 'Rippy/5.0 (https://github.com/Hitonabi/rippy)' }, }) if (antwort.status >= 300) continue const html = await antwort.text() const key = extractKey(html) if (key.length > 0) return { key, gueltigBis: extractGueltigBis(html) } } catch { // nächster Versuch — der Fehlschlag wird am Ende als null sichtbar } } return null } // ── Registry-Ablage über reg.exe (kein zweiter koffi-Ort, R1) ─────────── // In dieser Registry steckt die Arbeit des Commanders (Laufwerksprofile, // Fensterlage). Geschrieben wird GENAU app_Key und app_UpdateEnable — // gelöscht wird nur app_Key, und nur nach Ablehnung. function reg(args: string[]): { ok: boolean; ausgabe: string } { const ergebnis = spawnSync('reg', args, { windowsHide: true, timeout: 15_000, encoding: 'utf8' }) return { ok: ergebnis.status === 0, ausgabe: (ergebnis.stdout ?? '') + (ergebnis.stderr ?? '') } } export function keyAblegen(key: string): boolean { if (key.trim().length === 0) return false return reg(['add', REGISTRY_PFAD, '/v', KEY_NAME, '/t', 'REG_SZ', '/d', key.trim(), '/f']).ok } export function keyZuruecknehmen(): boolean { return reg(['delete', REGISTRY_PFAD, '/v', KEY_NAME, '/f']).ok } /** app_UpdateEnable=1 setzen, NUR wenn es fehlt — wer den Web-Kontakt * bewusst abgeschaltet hat, behält seine Entscheidung. */ export function updateKanalSicherstellen(): boolean { const vorhanden = reg(['query', REGISTRY_PFAD, '/v', 'app_UpdateEnable']) if (vorhanden.ok) return false return reg(['add', REGISTRY_PFAD, '/v', 'app_UpdateEnable', '/t', 'REG_DWORD', '/d', '1', '/f']).ok } export interface KeyUrteil { ok: boolean code: number | null grund: string } /** Nimmt MakeMKV den hinterlegten Schlüssel an? — es selbst gefragt: * `info disc:9999` braucht keinen Datenträger, die Freischaltungs-Meldung * kommt trotzdem (makemkv_aufruf.py, 29.08.2026). */ export function keyPruefen(programm: string, timeoutMs = 120_000): Promise { return new Promise((fertig) => { if (programm.length === 0) { fertig({ ok: false, code: null, grund: 'MakeMKV nicht gefunden' }) return } const kind = spawn(programm, ['-r', '--noscan', 'info', 'disc:9999'], { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'], }) const stuecke: Buffer[] = [] kind.stdout.on('data', (s: Buffer) => stuecke.push(s)) kind.stderr.on('data', (s: Buffer) => stuecke.push(s)) const zeiger = setTimeout(() => kind.kill(), timeoutMs) kind.on('error', (fehler) => { clearTimeout(zeiger) fertig({ ok: false, code: null, grund: `MakeMKV nicht befragbar (${String(fehler)})` }) }) kind.on('close', () => { clearTimeout(zeiger) for (const zeile of textVon(Buffer.concat(stuecke)).split(/\r?\n/)) { if (!zeile.startsWith('MSG:')) continue const code = Number(zeile.slice(4).split(',', 1)[0]) if (!Number.isInteger(code)) continue if (KEY_ABGELEHNT.includes(code) || code === 5051) { fertig({ ok: false, code, grund: KRITISCHE_CODES[code] ?? `MSG ${code}` }) return } } fertig({ ok: true, code: null, grund: '' }) }) }) } export interface SchluesselStand { /** 'dauerlizenz' | 'forum' | 'keiner' */ quelle: string angenommen: boolean | null gueltigBis: string geholtAm: string text: string } /** Die ganze Kette: Dauerlizenz → Forum-Key → ablegen → PRÜFEN → * bei Ablehnung zurücknehmen. Gibt den Stand fürs Dashboard zurück. */ export async function schluesselKette( makemkvcon: string, dauerlizenz: string, jetztIso: string, holen: typeof fetch = fetch, ): Promise { updateKanalSicherstellen() if (dauerlizenz.trim().length > 0) { keyAblegen(dauerlizenz) const urteil = await keyPruefen(makemkvcon) return { quelle: 'dauerlizenz', angenommen: urteil.ok, gueltigBis: '', geholtAm: jetztIso, text: urteil.ok ? 'Eigener Schlüssel hinterlegt und angenommen.' : `Eigener Schlüssel abgelehnt: ${urteil.grund}`, } } const forum = await betaKeyHolen(holen) if (forum === null) { // Kein neuer Key erreichbar — der VORHANDENE bleibt stehen (R2) und // wird geprüft, damit der Stand ehrlich ist. const urteil = await keyPruefen(makemkvcon) return { quelle: 'keiner', angenommen: urteil.ok, gueltigBis: '', geholtAm: jetztIso, text: urteil.ok ? 'Forum nicht erreichbar — der hinterlegte Schlüssel funktioniert weiter.' : `Forum nicht erreichbar und der hinterlegte Schlüssel taugt nicht: ${urteil.grund}`, } } keyAblegen(forum.key) const urteil = await keyPruefen(makemkvcon) if (!urteil.ok) { // Ein abgelehnter Schlüssel ist schlimmer als keiner — zurücknehmen. keyZuruecknehmen() } return { quelle: 'forum', angenommen: urteil.ok, gueltigBis: forum.gueltigBis, geholtAm: jetztIso, text: urteil.ok ? `Beta-Key aus dem Forum hinterlegt${forum.gueltigBis.length > 0 ? ` — gültig bis ${forum.gueltigBis}` : ''}.` : `Beta-Key abgelehnt (${urteil.grund}) — wieder entfernt. Vermutlich ist MakeMKV zu alt.`, } }