Ampel / ampel (push) Successful in 1m20s
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>
196 lines
7.5 KiB
TypeScript
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.`,
|
|
}
|
|
}
|