feat(v5): Etappe W-7 — Audio-CD, ISO-Backup, MakeMKV-Schluesselkette
Ampel / ampel (push) Successful in 1m20s
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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c78ff22ab7
commit
2d9cef22b3
@@ -0,0 +1,195 @@
|
||||
// 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.`,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user