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,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 }
|
||||
}
|
||||
Reference in New Issue
Block a user