// 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: Ein Schlüssel, der schon im Register steht und angenommen // wird, bleibt (5.7.0 — vorher überschrieb Rippy jeden vorhandenen // Schlüssel mit dem Forum-Key, auch einen gekauften aus dem GUI). // 3. Sonst der Beta-Key aus dem Forum-Thread des Herstellers (t=1053). // 4. Abgelegt wird in HKCU\Software\MakeMKV\app_Key — die settings.conf // ist der Linux-Weg; unter Windows liest MakeMKV die Registry. // 5. 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. // Ohne angeschlossenes Laufwerk sagt makemkvcon NICHTS zur Lizenz // (12.09.2026 gemessen: nur 5042/5010) — dann heißt das Urteil // „nicht prüfbar", nicht „angenommen". // 6. 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' // 5.7.0 (Durchsicht 12.09.2026, F1): Der September-Key lautet // „T-vwvn…6Ox4BI@N5rPp" — 68 Zeichen MIT einem @. Das alte Muster // /T-[A-Za-z0-9_]{50,}/ endete am @ und lieferte 62 Zeichen; MakeMKV lehnte // den Stumpf ab (5020), Rippy nahm ihn zurück, und MakeMKV lief ohne // Schlüssel. Jetzt zählt alles bis zum nächsten Leerraum oder HTML-Zeichen — // Rippy rät nicht, welche Zeichen der Hersteller in seine Schlüssel setzt. export const KEY_MUSTER = /T-[^\s<>"'&;]{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). Der Thread schreibt * mal „September 30th, 2026", mal „end of September 2026" (so am * 12.09.2026 gemessen) — beide Formen kommen an. */ export function extractGueltigBis(html: string): string { const treffer = /valid\s+(?:until|through)\s+((?:(?:the\s+)?end\s+of\s+|(?:the\s+)?beginning\s+of\s+|mid-?\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').replace(/\s+/g, ' ') } 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 } /** Den Wert aus einer `reg query`-Ausgabe lesen (pur, testbar): * „ app_Key REG_SZ T-…" — '' wenn keiner dasteht. */ export function keyAusQuery(ausgabe: string): string { const treffer = new RegExp(`${KEY_NAME}\\s+REG_SZ\\s+(\\S+)`).exec(ausgabe ?? '') return treffer === null ? '' : treffer[1].trim() } /** Der Schlüssel, der gerade im Register steht — '' wenn keiner. */ export function keyLesen(): string { const vorhanden = reg(['query', REGISTRY_PFAD, '/v', KEY_NAME]) return vorhanden.ok ? keyAusQuery(vorhanden.ausgabe) : '' } /** 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 /** false: makemkvcon hat die Lizenz gar nicht angesehen — es sah kein * Laufwerk (5042). Dann ist `ok` keine Aussage über den Schlüssel. */ pruefbar: boolean code: number | null grund: string /** Die MakeMKV-Version aus MSG 1005 („MakeMKV v1.18.4 …") — '' wenn * nicht erkennbar. */ version: 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) — ABER nur mit einem * angeschlossenen Laufwerk (12.09.2026 ohne Laufwerk gemessen: 5042 und * 5010, keine Lizenzzeile). * * ⚠ ALLE Codes einsammeln, dann urteilen: Beim Commander kamen 5020 UND * 5021 zusammen (30.08.2026 live gemessen) — 5020 allein klingt nach * „Schlüssel kaputt", die URSACHE steht aber in 5021: MakeMKV ist zu alt * für den aktuellen Beta-Key. Wer beim ersten Code abbricht, beschuldigt * den falschen. */ export function keyPruefen(programm: string, timeoutMs = 120_000): Promise { return new Promise((fertig) => { if (programm.length === 0) { fertig({ ok: false, pruefbar: false, code: null, grund: 'MakeMKV nicht gefunden', version: '' }) 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, pruefbar: false, code: null, grund: `MakeMKV nicht befragbar (${String(fehler)})`, version: '' }) }) kind.on('close', () => { clearTimeout(zeiger) fertig(urteilAusAusgabe(textVon(Buffer.concat(stuecke)))) }) }) } /** MakeMKV sah kein Laufwerk — ohne eines prüft es die Lizenz nicht. */ export const KEIN_LAUFWERK = 5042 /** Die Ausgabe eines info-Laufs → Urteil (pur, testbar). */ export function urteilAusAusgabe(ausgabe: string): KeyUrteil { const codes: number[] = [] let version = '' let keinLaufwerk = false for (const zeile of ausgabe.split(/\r?\n/)) { if (!zeile.startsWith('MSG:')) continue const code = Number(zeile.slice(4).split(',', 1)[0]) if (!Number.isInteger(code)) continue if (code === 1005) { const treffer = /MakeMKV v([0-9][0-9.]*)/.exec(zeile) if (treffer !== null) version = treffer[1] } if (code === KEIN_LAUFWERK) keinLaufwerk = true if (KEY_ABGELEHNT.includes(code) || code === 5051) codes.push(code) } if (codes.length === 0) { // Keine Lizenzzeile: entweder angenommen — oder gar nicht angesehen. return keinLaufwerk ? { ok: true, pruefbar: false, code: KEIN_LAUFWERK, grund: 'MakeMKV sah kein Laufwerk und hat die Lizenz nicht geprüft', version } : { ok: true, pruefbar: true, code: null, grund: '', version } } // 5021 schlägt alles: Es benennt die URSACHE (Version zu alt) — 5020 // ist dann nur die Folge und würde den Schlüssel zu Unrecht beschuldigen. const code = codes.includes(5021) ? 5021 : codes[0] return { ok: false, pruefbar: true, code, grund: KRITISCHE_CODES[code] ?? `MSG ${code}`, version } } export interface SchluesselStand { /** 'dauerlizenz' | 'vorhanden' | 'forum' | 'keiner' */ quelle: string /** true angenommen · false abgelehnt/fehlt · null nicht prüfbar (kein Laufwerk). */ angenommen: boolean | null /** Laufen Rips trotzdem? Ohne angenommenen Schlüssel: DVDs ja (laut * Hersteller dauerhaft frei), Blu-rays nur in MakeMKVs Gnadenfrist — * das ist ein AUSFALL für Blu-ray, keine Warnung (5.7.0). */ weiterBetrieb: boolean gueltigBis: string geholtAm: string text: string /** Die installierte MakeMKV-Fassung (aus MSG 1005) — fürs Update-Urteil. */ version: string } /** Was ein abgelehnter oder fehlender Schlüssel bedeutet — EIN Satz für * alle Wege, damit das Fenster nie „Rips laufen" sagt, wo Blu-rays nicht * laufen. */ export const OHNE_SCHLUESSEL_FOLGE = 'Ohne angenommenen Schlüssel liest MakeMKV DVDs weiter (dauerhaft frei), Blu-rays und 4K aber nur in seiner Gnadenfrist nach dem Testzeitraum — danach verweigert es sie.' /** Die ganze Kette: Dauerlizenz → vorhandener Schlüssel → 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, registerLesen: () => string = keyLesen, ): Promise { updateKanalSicherstellen() const versionsText = (urteil: KeyUrteil): string => (urteil.version.length > 0 ? ` ${urteil.version}` : '') const nichtPruefbar = (quelle: string, gueltigBis: string, urteil: KeyUrteil, wer: string): SchluesselStand => ({ quelle, angenommen: null, weiterBetrieb: true, gueltigBis, geholtAm: jetztIso, text: `${wer} hinterlegt — ob MakeMKV${versionsText(urteil)} ihn annimmt, lässt sich erst mit angeschlossenem Laufwerk prüfen (${urteil.grund}).`, version: urteil.version, }) if (dauerlizenz.trim().length > 0) { // 5.7.0: Das Ergebnis von reg add zählt — „hinterlegt" stand vorher // auch da, wenn nichts geschrieben wurde. if (!keyAblegen(dauerlizenz)) { return { quelle: 'dauerlizenz', angenommen: false, weiterBetrieb: false, gueltigBis: '', geholtAm: jetztIso, text: 'Eigener Schlüssel konnte nicht ins Register geschrieben werden (reg add scheiterte). ' + OHNE_SCHLUESSEL_FOLGE, version: '', } } const urteil = await keyPruefen(makemkvcon) if (!urteil.pruefbar) return nichtPruefbar('dauerlizenz', '', urteil, 'Eigener Schlüssel') return { quelle: 'dauerlizenz', angenommen: urteil.ok, weiterBetrieb: urteil.ok, gueltigBis: '', geholtAm: jetztIso, text: urteil.ok ? `Eigener Schlüssel hinterlegt und von MakeMKV${versionsText(urteil)} angenommen.` : `Eigener Schlüssel abgelehnt: ${urteil.grund}. ${OHNE_SCHLUESSEL_FOLGE}`, version: urteil.version, } } // 5.7.0: Ein Schlüssel, der schon steht und angenommen wird, bleibt — // egal ob gekauft (im GUI eingetragen) oder ein älterer Forum-Key. const vorhanden = registerLesen() if (vorhanden.length > 0) { const urteil = await keyPruefen(makemkvcon) if (!urteil.pruefbar) return nichtPruefbar('vorhanden', '', urteil, 'Ein Schlüssel ist') if (urteil.ok) { return { quelle: 'vorhanden', angenommen: true, weiterBetrieb: true, gueltigBis: '', geholtAm: jetztIso, text: `Der hinterlegte Schlüssel wird von MakeMKV${versionsText(urteil)} angenommen — Rippy lässt ihn stehen.`, version: urteil.version, } } // Abgelehnt: dann darf der Forum-Key ihn ersetzen (unten). } 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) if (!urteil.pruefbar) return nichtPruefbar('keiner', '', urteil, 'Forum nicht erreichbar; der vorhandene Stand ist') return { quelle: 'keiner', angenommen: urteil.ok, weiterBetrieb: 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}. ${OHNE_SCHLUESSEL_FOLGE}`, version: urteil.version, } } if (!keyAblegen(forum.key)) { return { quelle: 'forum', angenommen: false, weiterBetrieb: false, gueltigBis: forum.gueltigBis, geholtAm: jetztIso, text: 'Beta-Key geholt, aber nicht ins Register schreibbar (reg add scheiterte). ' + OHNE_SCHLUESSEL_FOLGE, version: '', } } const urteil = await keyPruefen(makemkvcon) if (!urteil.pruefbar) return nichtPruefbar('forum', forum.gueltigBis, urteil, 'Beta-Key aus dem Forum') if (!urteil.ok) { // Ein abgelehnter Schlüssel ist schlimmer als keiner — zurücknehmen. // (Live gemessen 30.08.2026: MIT abgelehntem Key verweigert MakeMKV // alles; OHNE läuft es im Beta-Modus weiter und rippt — DVDs immer, // Blu-rays nur bis zum Ende des Testzeitraums.) keyZuruecknehmen() } return { quelle: 'forum', angenommen: urteil.ok, weiterBetrieb: urteil.ok, gueltigBis: forum.gueltigBis, geholtAm: jetztIso, text: urteil.ok ? `Beta-Key aus dem Forum hinterlegt und von MakeMKV${versionsText(urteil)} angenommen${forum.gueltigBis.length > 0 ? ` — gültig bis ${forum.gueltigBis}` : ''}.` : urteil.code === 5021 ? `Der aktuelle Beta-Key ist in Ordnung — aber MakeMKV${versionsText(urteil)} ist ZU ALT für ihn. ` + 'Rippy hat ihn wieder entfernt. ' + OHNE_SCHLUESSEL_FOLGE + ' Abhilfe: MakeMKV von makemkv.com aktualisieren — danach hinterlegt Rippy den Key automatisch neu.' : `Beta-Key abgelehnt (${urteil.grund}) — wieder entfernt. ${OHNE_SCHLUESSEL_FOLGE}`, version: urteil.version, } }