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
+80
View File
@@ -0,0 +1,80 @@
// ISO-Backup für Datendiscs (KONZEPT § 6.4) — die einzige echte Lücke
// gegenüber ARM: Wird eine Disc weder als Film noch als Musik erkannt,
// wird sie 1:1 gesichert statt abgelehnt. Sequenziell aus \\.\G: lesen;
// die Länge kommt aus IOCTL_DISK_GET_LENGTH_INFO (§ 3.2 gemessen: exakte
// Bytes). Kein Fremdwerkzeug nötig.
//
// Async gelesen: Ein 33-GB-Abbild darf den Kern nicht blockieren — die
// Disc-Wache und der Ping laufen währenddessen weiter. Windows verlangt
// auf Geräte-Handles SEKTOR-AUSGERICHTETE Zugriffe — der Blockpuffer ist
// ein Vielfaches von 2048 (1 MiB = 512 Sektoren), gelesen wird strikt
// sequenziell ab 0.
import { open } from 'node:fs/promises'
const BLOCK_BYTES = 1024 * 1024
export type IsoErgebnis =
| { status: 'success'; bytes: number }
| { status: 'cancelled' }
| { status: 'error'; fehler: string; bytes: number }
export async function isoSichern(
geraetepfad: string,
zielDatei: string,
erwarteteBytes: number,
aufFortschritt?: (prozent: number) => void,
abbruch?: AbortSignal,
): Promise<IsoErgebnis> {
if (erwarteteBytes <= 0) {
return { status: 'error', fehler: 'Die Größe der Disc ist unbekannt — ohne sie gibt es kein 1:1-Abbild.', bytes: 0 }
}
let quelle
try {
quelle = await open(geraetepfad, 'r')
} catch (fehler) {
return { status: 'error', fehler: `Gerät nicht lesbar: ${String(fehler)}`, bytes: 0 }
}
let ziel
try {
ziel = await open(zielDatei, 'w')
} catch (fehler) {
await quelle.close()
return { status: 'error', fehler: `Zieldatei nicht anlegbar: ${String(fehler)}`, bytes: 0 }
}
const puffer = Buffer.alloc(BLOCK_BYTES)
let gelesen = 0
let letzterProzent = -1
try {
while (gelesen < erwarteteBytes) {
if (abbruch?.aborted === true) return { status: 'cancelled' }
const rest = erwarteteBytes - gelesen
const anfordern = rest >= BLOCK_BYTES ? BLOCK_BYTES : rest
const { bytesRead } = await quelle.read(puffer, 0, anfordern, gelesen)
if (bytesRead <= 0) break
await ziel.write(puffer, 0, bytesRead)
gelesen += bytesRead
const prozent = Math.floor((gelesen * 100) / erwarteteBytes)
if (prozent !== letzterProzent) {
letzterProzent = prozent
aufFortschritt?.(prozent)
}
}
} catch (fehler) {
return { status: 'error', fehler: `Lesen brach bei ${gelesen} Bytes ab: ${String(fehler)}`, bytes: gelesen }
} finally {
await quelle.close()
await ziel.close()
}
if (gelesen !== erwarteteBytes) {
// R4/„Rückgabewert ist kein Beweis": Das Abbild ist NUR vollständig,
// wenn die Bytes stimmen — alles andere wird gesagt, nicht beschönigt.
return {
status: 'error',
fehler: `Abbild unvollständig: ${gelesen} von ${erwarteteBytes} Bytes gelesen.`,
bytes: gelesen,
}
}
return { status: 'success', bytes: gelesen }
}