WAS: KEY_MUSTER nimmt alles bis Leerraum/HTML-Zeichen (der September-Key „T-…4BI@N5rPp" hat 68 Zeichen mit @; das alte Muster lieferte 62 und MakeMKV lief danach ohne Schlüssel). extractGueltigBis versteht „end of September 2026". Die Kette prüft zuerst den vorhandenen Schlüssel und lässt ihn stehen, wenn MakeMKV ihn annimmt (kein Überschreiben gekaufter Keys mehr); reg add wird auf Erfolg geprüft; 5042 ohne Lizenzzeile heißt „nicht prüfbar" statt „angenommen". Fenster: Ablehnung ist ROT mit dem Satz, dass Blu-rays dann nur noch in MakeMKVs Gnadenfrist laufen; die MakeMKV-Karte sagt, was Rippy in MakeMKVs Einstellungen schreibt. Ein neuer Werkzeug-Pfad wird sofort gesucht, ein neuer eigener Schlüssel sofort geprüft. datumKurz zeigt Ortszeit statt UTC. WARUM: Durchsicht 12.09.2026, Funde F1, F5, F12, F13 und A2 — am echten Forum-HTML und an makemkvcon ohne Laufwerk gemessen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
347 lines
15 KiB
TypeScript
347 lines
15 KiB
TypeScript
// 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 <Datum>" 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<ForumKey | null> {
|
|
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<KeyUrteil> {
|
|
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<SchluesselStand> {
|
|
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,
|
|
}
|
|
}
|