Files
rippy/rippy-windows/src/kern/werkzeuge/schluessel.ts
T
HitonabiandClaude Fable 5 2d9cef22b3
Ampel / ampel (push) Successful in 1m20s
feat(v5): Etappe W-7 — Audio-CD, ISO-Backup, MakeMKV-Schluesselkette
rip/audiocd.ts: die Rechnungen PUR (MSF→LBA mit den 150 Vorlauf-Frames,
TOC-Zerlegung mit Lead-Out-Pflicht und Daten-Track-Bit, 27-Sektoren-
Bloecke unter der 64-KB-Treibergrenze, RAW_READ_INFO mit DiskOffset in
2048ern OBWOHL der Sektor 2352 hat, WAV-Kopf, AC/DC-sichere Namen).
metadaten/musicbrainz.ts: DiscID nach musicbrainz.org-Rechnung — gegen
die ECHTE AC/DC-Kennung getestet (3KVSIWn_…), Vorlauf wird beim Melden
wieder HINZUgerechnet; Tags je Spur mit Bindewort-Kuenstlern.
laufwerk/disc.ts: audioToc + audioSpurLesen ueber die LaufwerkApi.

rip/iso.ts (§ 6.4): 1:1-Abbild fuer unbekannte Discs — async (ein
33-GB-Abbild darf den Kern nicht blockieren), sektor-ausgerichtet,
unvollstaendig heisst FEHLER mit Byte-Zahlen, nie Schoenfaerberei.

werkzeuge/schluessel.ts (§ 9.1): Dauerlizenz schlaegt Forum-Beta-Key
(t=1053, T-Muster aus makemkv_key.py), Ablage in HKCU ueber reg.exe
(kein zweiter koffi-Ort), GEFRAGT statt gerechnet (makemkvcon info
disc:9999, MSG 5020/5021/5051), abgelehnter Key wird ZURUECKGENOMMEN
(schlimmer als keiner: 0 Titel, kein Laufwerk), app_UpdateEnable nur-
wenn-fehlt (der 4K-Schluesselkanal). Kern prueft beim Start und
taeglich; Dashboard-Kachel zeigt Stand + letzten Pruefzeitpunkt;
Dauerlizenz-Feld in den Einstellungen.

Pipeline verzweigt nach Disc-Typ: cd → Sektoren→WAV→FLAC mit
MusicBrainz-Tags (Ordner 'Kuenstler - Album', ohne Treffer ehrlich
'Track NN'), unknown → ISO. Bibliothek-Eintraege fuer beide.

181 Tests gruen, Smoke gruen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 18:56:07 +02:00

196 lines
7.5 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 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 <Datum>" 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<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
}
/** 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<KeyUrteil> {
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<SchluesselStand> {
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.`,
}
}