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>
164 lines
6.2 KiB
TypeScript
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('')
|
|
})
|
|
})
|