feat(v5): Etappe W-7 — Audio-CD, ISO-Backup, MakeMKV-Schluesselkette
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:
Hitonabi
2026-08-30 18:56:07 +02:00
co-authored by Claude Fable 5
parent c78ff22ab7
commit 2d9cef22b3
14 changed files with 1045 additions and 2 deletions
+56 -1
View File
@@ -16,7 +16,14 @@ import {
} from '../gemeinsam/nachrichten'
import { Pipeline } from './ablauf/pipeline'
import type { LaufwerkApi } from './laufwerk/codes'
import { auswerfenMitGrund, geraeteInfo, kennung, listOptischeLaufwerke } from './laufwerk/disc'
import {
audioSpurLesen,
audioToc,
auswerfenMitGrund,
geraeteInfo,
kennung,
listOptischeLaufwerke,
} from './laufwerk/disc'
import { DiscWache } from './laufwerk/wache'
import { discFingerprint, discTitel } from './metadaten/discmerkmale'
import { OmdbClient } from './metadaten/omdb'
@@ -196,9 +203,25 @@ function einstellungenLaden(): Record<string, string> {
return werte
}
function geraetVon(id: string): string | null {
if (laufwerkApi === null) return null
return listOptischeLaufwerke(laufwerkApi).find((g) => kennung(g) === id) ?? null
}
const ripVerwaltung = new Pipeline({
makemkvcon: () => finden('makemkv', db?.einstellungLesen('werkzeug.makemkv') ?? ''),
handbrake: () => finden('handbrake', db?.einstellungLesen('werkzeug.handbrake') ?? ''),
flac: () => finden('flac', db?.einstellungLesen('werkzeug.flac') ?? ''),
audioToc: (id) => {
const geraet = geraetVon(id)
if (laufwerkApi === null || geraet === null) throw new Error(`Laufwerk ${id}: gibt es nicht`)
return audioToc(laufwerkApi, geraet)
},
audioSpurLesen: (id, start, sektoren, schreiben, fortschritt) => {
const geraet = geraetVon(id)
if (laufwerkApi === null || geraet === null) throw new Error(`Laufwerk ${id}: gibt es nicht`)
return audioSpurLesen(laufwerkApi, geraet, start, sektoren, schreiben, fortschritt)
},
geraet: (id) => {
if (laufwerkApi === null) return null
const geraet = listOptischeLaufwerke(laufwerkApi).find((g) => kennung(g) === id)
@@ -237,6 +260,7 @@ const ERLAUBTE_EINSTELLUNGEN = new Set([
'mediaServerApiKey',
'tmdbApiKey',
'omdbApiKey',
'makemkvAppKey',
'autoAuswurf',
'werkzeug.makemkv',
'werkzeug.handbrake',
@@ -252,6 +276,29 @@ function einstellungenSenden(): void {
anFenster({ art: 'einstellungen', werte })
}
// ── MakeMKV-Schlüsselkette (§ 9.1): beim Start und täglich ──────────────
let schluesselStand: Extract<KernNachricht, { art: 'schluessel-status' }>['stand'] | null = null
async function schluesselLauf(): Promise<void> {
if (process.platform !== 'win32') return
try {
const { schluesselKette } = await import('./werkzeuge/schluessel')
const makemkvcon = finden('makemkv', db?.einstellungLesen('werkzeug.makemkv') ?? '')
schluesselStand = await schluesselKette(
makemkvcon,
db?.einstellungLesen('makemkvAppKey') ?? '',
new Date().toISOString(),
)
anHauptUndFenster({ art: 'schluessel-status', stand: schluesselStand })
} catch (fehler) {
// R4: Der Schlüssel-Lauf ist Komfort — sein Scheitern wird gesagt.
console.error(`[kern] Schlüsselkette: ${String(fehler)}`)
anFenster({ art: 'kern-fehler', text: `Schlüsselkette: ${String(fehler)}` })
}
}
// Täglich neu prüfen (§ 9.1) — ohne den Prozess-Exit aufzuhalten.
setInterval(() => void schluesselLauf(), 24 * 60 * 60 * 1000).unref()
async function werkzeugeSenden(): Promise<void> {
const makemkvcon = finden('makemkv', db?.einstellungLesen('werkzeug.makemkv') ?? '')
const handbrake = finden('handbrake', db?.einstellungLesen('werkzeug.handbrake') ?? '')
@@ -336,6 +383,9 @@ process.parentPort.on('message', (ereignis) => {
anFenster({ art: 'kern-fehler', text: `Laufwerke: ${laufwerkFehler}` })
}
anFenster({ art: 'laufwerke', liste: wache?.aktuellerStand() ?? [], ereignisse: [] })
if (schluesselStand !== null) {
anFenster({ art: 'schluessel-status', stand: schluesselStand })
}
})
}
})
@@ -350,3 +400,8 @@ function bereitNachricht(): KernNachricht {
}
process.parentPort.postMessage(bereitNachricht())
// Nicht im Smoke: Der Schlüssel-Lauf spricht Forum + makemkvcon an —
// der Gerüst-Beweis soll ohne Netz und ohne MakeMKV grün sein.
if (!process.argv.includes('--smoke-kern') && process.env['RIPPY_OHNE_SCHLUESSEL'] !== '1') {
void schluesselLauf()
}