Files
rippy/rippy-windows/test/audiocd.test.ts
T
HitonabiandClaude Fable 5 2d9cef22b3
Ampel / ampel (push) Successful in 1m20s
feat(v5): Etappe W-7 — Audio-CD, ISO-Backup, MakeMKV-Schluesselkette
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>
2026-08-30 18:56:07 +02:00

164 lines
6.2 KiB
TypeScript

// Tests der Audio-CD-Rechnungen und der MusicBrainz-Kennung — die
// Referenzzahlen sind die ECHTE AC/DC-„Back in Black"-Antwort vom
// 29.08.2026 (test_musicbrainz_cd.py: „abgefragt, nicht erinnert").
import { describe, expect, it } from 'vitest'
import {
bloecke,
dateiname,
dauerText,
flacBefehl,
msfZuLba,
rawReadInfo,
sauberName,
tocZerlegen,
wavKopf,
OFFSET_EINHEIT,
SEKTOR_BYTES,
VORLAUF_FRAMES,
} from '../src/kern/rip/audiocd'
import { discId, kennungAusSpuren, tagsJeSpur, PLAETZE } from '../src/kern/metadaten/musicbrainz'
import { extractGueltigBis, extractKey } from '../src/kern/werkzeuge/schluessel'
const ACDC_VERSAETZE = [182, 23637, 47507, 63692, 79615, 98742, 117937, 133712, 151660, 170112]
const ACDC_LEAD_OUT = 189281
const ACDC_KENNUNG = '3KVSIWn_ewv9z0cCizmOJzDWtsQ-'
describe('MusicBrainz-DiscID', () => {
it('DIE Referenz: stimmt mit MusicBrainz überein', () => {
expect(discId(ACDC_VERSAETZE, ACDC_LEAD_OUT, 1, 10)).toBe(ACDC_KENNUNG)
})
it('der Vorlauf wird wieder HINZUgerechnet (LBA→MSF-Falle)', () => {
const spuren = ACDC_VERSAETZE.map((versatz, i) => ({
nr: i + 1,
start: versatz - VORLAUF_FRAMES,
sektoren: (i + 1 < ACDC_VERSAETZE.length ? ACDC_VERSAETZE[i + 1] : ACDC_LEAD_OUT) - versatz,
audio: true,
dauer: '',
}))
expect(kennungAusSpuren(spuren)).toBe(ACDC_KENNUNG)
})
it('MusicBrainz-Alphabet: kein + / =', () => {
const kennung = discId(ACDC_VERSAETZE, ACDC_LEAD_OUT, 1, 10)
expect(/[+/=]/.test(kennung)).toBe(false)
})
it('immer 99 Plätze — und jede Zahl zählt', () => {
expect(PLAETZE).toBe(99)
expect(discId([150, 20000], 40000, 1, 2)).not.toBe(discId([150, 20001], 40000, 1, 2))
expect(discId([], 1000)).toBe('')
expect(kennungAusSpuren([])).toBe('')
})
})
describe('MSF/LBA und TOC', () => {
it('msfZuLba zieht die 150 Vorlauf-Frames ab', () => {
expect(msfZuLba(0, 2, 0)).toBe(0)
expect(msfZuLba(0, 2, 32)).toBe(32)
expect(msfZuLba(1, 0, 0)).toBe(75 * 60 - 150)
expect(dauerText(4500)).toBe('1:00')
expect(dauerText(75 * 61)).toBe('1:01')
})
it('tocZerlegen liest Spuren, erkennt Daten-Tracks, braucht den Lead-Out', () => {
// CDROM_TOC: 4 Byte Kopf + Einträge à 8 Byte (…, Adr/Control, Nr, …, 0,M,S,F)
const toc = Buffer.alloc(4 + 3 * 8)
const eintrag = (stelle: number, control: number, nr: number, m: number, s: number, f: number): void => {
toc[4 + stelle * 8 + 1] = control
toc[4 + stelle * 8 + 2] = nr
toc[4 + stelle * 8 + 5] = m
toc[4 + stelle * 8 + 6] = s
toc[4 + stelle * 8 + 7] = f
}
eintrag(0, 0x00, 1, 0, 2, 0) // Audio ab LBA 0
eintrag(1, 0x04, 2, 1, 2, 0) // DATEN-Track ab LBA 4500
eintrag(2, 0x00, 0xaa, 2, 2, 0) // Lead-Out bei LBA 9000
const spuren = tocZerlegen(toc)
expect(spuren).toHaveLength(2)
expect(spuren[0]).toMatchObject({ nr: 1, start: 0, sektoren: 4500, audio: true, dauer: '1:00' })
expect(spuren[1]).toMatchObject({ nr: 2, audio: false })
})
it('ohne Endmarke fehlt der letzten Spur die Länge — sie fällt weg', () => {
const toc = Buffer.alloc(4 + 8)
toc[4 + 2] = 1
toc[4 + 6] = 2
expect(tocZerlegen(toc)).toEqual([])
expect(tocZerlegen(Buffer.alloc(4))).toEqual([])
})
})
describe('Lesen und Kodieren', () => {
it('bloecke bleibt unter der 64-KB-Treibergrenze', () => {
const teile = bloecke(1000, 60)
expect(teile[0]).toEqual([1000, 27])
expect(teile[1]).toEqual([1027, 27])
expect(teile[2]).toEqual([1000 + 54, 6])
expect(27 * SEKTOR_BYTES).toBeLessThan(64 * 1024)
})
it('rawReadInfo: DiskOffset in 2048ern, obwohl der Sektor 2352 hat', () => {
const info = rawReadInfo(100, 27)
expect(info.readBigInt64LE(0)).toBe(100n * BigInt(OFFSET_EINHEIT))
expect(info.readUInt32LE(8)).toBe(27)
expect(info.readUInt32LE(12)).toBe(2) // CDDA
})
it('wavKopf: 44 Byte, RIFF-Größenfelder stimmen', () => {
const kopf = wavKopf(1000)
expect(kopf.length).toBe(44)
expect(kopf.toString('ascii', 0, 4)).toBe('RIFF')
expect(kopf.readUInt32LE(4)).toBe(1036)
expect(kopf.readUInt32LE(40)).toBe(1000)
expect(kopf.readUInt32LE(24)).toBe(44100)
})
it('sauberName: AC/DC macht keinen Unterordner auf', () => {
expect(sauberName('AC/DC - Back in Black')).toBe('ACDC - Back in Black')
expect(sauberName('Ende. ')).toBe('Ende')
})
it('dateiname mit und ohne Titel', () => {
expect(dateiname(1, 'Hells Bells')).toBe('01 - Hells Bells')
expect(dateiname(2)).toBe('Track 02')
})
it('flacBefehl trägt Tags einzeln', () => {
const befehl = flacBefehl('a.wav', 'b.flac', { TITLE: 'X', LEER: '' })
expect(befehl).toContain('--tag')
expect(befehl[befehl.indexOf('--tag') + 1]).toBe('TITLE=X')
expect(befehl.join(' ')).not.toContain('LEER=')
expect(befehl.at(-1)).toBe('a.wav')
})
})
describe('tagsJeSpur', () => {
it('setzt Künstler mit Bindewörtern zusammen, nimmt nur das Jahr', () => {
const tags = tagsJeSpur({
releases: [
{
title: 'Back in Black',
date: '1980-07-25',
'artist-credit': [{ name: 'AC/DC' }],
media: [
{
tracks: [
{ position: 1, title: 'Hells Bells', 'artist-credit': [{ name: 'Simon', joinphrase: ' & ' }, { name: 'Garfunkel' }] },
{ position: '2', title: 'Shoot to Thrill' },
],
},
],
},
],
})
expect(tags.get(1)?.['ARTIST']).toBe('Simon & Garfunkel')
expect(tags.get(2)?.['ARTIST']).toBe('AC/DC')
expect(tags.get(1)?.['DATE']).toBe('1980')
expect(tags.get(2)?.['TRACKNUMBER']).toBe('2')
expect(tagsJeSpur(null).size).toBe(0)
})
})
describe('Schlüsselkette (§ 9.1)', () => {
it('extractKey findet den Beta-Key im Forum-HTML', () => {
const html = '<code>T-' + 'x'.repeat(60) + '</code>'
expect(extractKey(html)).toBe('T-' + 'x'.repeat(60))
expect(extractKey('kein Key hier T-kurz')).toBe('')
})
it('extractGueltigBis liest das Ablaufdatum (best effort)', () => {
expect(extractGueltigBis('The key is valid until September 30th, 2026 or so')).toBe('September 30, 2026')
expect(extractGueltigBis('nichts')).toBe('')
})
})