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>
81 lines
2.8 KiB
TypeScript
81 lines
2.8 KiB
TypeScript
// 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 }
|
|
}
|