Files
rippy/rippy-windows/test/audiocd.test.ts
T
HitonabiandClaude Opus 5 12b9720386 fix(v5): Beta-Key mit @ kommt ganz an; Schlüssel-Stand ehrlich (rot statt „Rips laufen", nicht prüfbar ohne Laufwerk); Zeiten in Ortszeit
WAS: KEY_MUSTER nimmt alles bis Leerraum/HTML-Zeichen (der September-Key
„T-…4BI@N5rPp" hat 68 Zeichen mit @; das alte Muster lieferte 62 und
MakeMKV lief danach ohne Schlüssel). extractGueltigBis versteht „end of
September 2026". Die Kette prüft zuerst den vorhandenen Schlüssel und
lässt ihn stehen, wenn MakeMKV ihn annimmt (kein Überschreiben gekaufter
Keys mehr); reg add wird auf Erfolg geprüft; 5042 ohne Lizenzzeile heißt
„nicht prüfbar" statt „angenommen". Fenster: Ablehnung ist ROT mit dem
Satz, dass Blu-rays dann nur noch in MakeMKVs Gnadenfrist laufen; die
MakeMKV-Karte sagt, was Rippy in MakeMKVs Einstellungen schreibt. Ein
neuer Werkzeug-Pfad wird sofort gesucht, ein neuer eigener Schlüssel
sofort geprüft. datumKurz zeigt Ortszeit statt UTC.
WARUM: Durchsicht 12.09.2026, Funde F1, F5, F12, F13 und A2 — am echten
Forum-HTML und an makemkvcon ohne Laufwerk gemessen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-12 21:17:43 +02:00

179 lines
7.1 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('')
})
// 5.7.0 (Durchsicht 12.09.2026, F1): So steht es WIRKLICH auf der Forum-
// Seite — der September-Key trägt ein @, und das Datum heißt „end of".
// Das alte Muster endete am @ und lieferte 62 von 68 Zeichen.
it('der echte September-2026-Key mit @ kommt ganz an', () => {
const key = 'T-vwvnrmF87JuRPLYYO8HnIrZR_LCAF7JXs9iJEm10moGpjEgkvmPTsf6Ox4BI@N5rPp'
const html = `<div class="content">The current beta key is:<br><code>${key}</code><br>The key is valid until end of September 2026. Please check back for updated key on this page.</div>`
expect(extractKey(html)).toBe(key)
expect(extractKey(html)).toHaveLength(68)
expect(extractGueltigBis(html)).toBe('end of September 2026')
})
it('der Key endet am HTML-Zeichen, nicht mitten im Wort', () => {
const key = 'T-' + 'a'.repeat(66)
expect(extractKey(`<code>${key}</code><p>valid</p>`)).toBe(key)
expect(extractKey(`key: ${key} (valid until end of October 2026)`)).toBe(key)
})
})