// Tests des makemkvcon-Parsers — die Testfälle wandern sinngemäß aus dem // Docker-Zweig mit (test_makemkv_aufruf.py, ripping.py-Doku): Ein neu // geschriebener Parser, der dieselben Fälle besteht, kennt dieselben // Fallen (KONZEPT § 10). import { describe, expect, it } from 'vitest' import { episodenTitel, KRITISCHE_CODES, laengsterTitel, parseMsg, parseStreamInfo, parseTitelInfo, progressAusPrgv, quelle, sprachenZusammenfassen, textVon, } from '../src/kern/rip/parser' describe('quelle — wie MakeMKV das Laufwerk nennt', () => { it.each([ ['\\\\.\\G:', 'dev:G:'], ['G:', 'dev:G:'], ['G:\\', 'dev:G:'], ['\\\\?\\E:', 'dev:E:'], ['/dev/sr0', 'dev:/dev/sr0'], ])('%s → %s', (eingabe, erwartet) => { expect(quelle(eingabe)).toBe(erwartet) }) it('ohne Gerät kommt nichts', () => { expect(quelle('')).toBe('') expect(quelle(' ')).toBe('') }) it('ein langer Windows-Pfad wird nicht verstümmelt', () => { expect(quelle('\\\\.\\D:\\Ordner')).toBe('dev:D:\\Ordner') }) }) describe('textVon — Kodierung bestimmen statt behaupten', () => { it('liest UTF-8', () => { expect(textVon(Buffer.from('Das Öffnen', 'utf8'))).toBe('Das Öffnen') }) it('liest AUCH cp1252', () => { expect(textVon(Buffer.from('Das Öffnen', 'latin1'))).toBe('Das Öffnen') }) it('der Fehler des Commanders kommt NICHT zurück (kein Ã)', () => { for (const kodierung of ['utf8', 'latin1'] as const) { const gelesen = textVon(Buffer.from('Das Öffnen der Disk schlug fehl', kodierung)) expect(gelesen).not.toContain('Ã') expect(gelesen).toBe('Das Öffnen der Disk schlug fehl') } }) it('Text bleibt Text, leer bleibt leer, kaputte Bytes werfen nicht', () => { expect(textVon('schon Text')).toBe('schon Text') expect(textVon(Buffer.alloc(0))).toBe('') expect(textVon(Buffer.from([0xff, 0xfe, 0x20]))).toBeTypeOf('string') }) }) describe('parseMsg', () => { it('liest Code und Klartext', () => { expect(parseMsg('MSG:5010,0,1,"Das Öffnen der Disk schlug fehl","%1","x"')).toEqual({ code: 5010, text: 'Das Öffnen der Disk schlug fehl', }) }) it('Meldungen MIT Komma werden nicht abgeschnitten (25.07.-Falle)', () => { const m = parseMsg('MSG:3025,0,1,"Title #1 has length of 12 seconds, which is less than minimum","f"') expect(m?.text).toBe('Title #1 has length of 12 seconds, which is less than minimum') }) it('maskierte Anführungszeichen werden entpackt', () => { expect(parseMsg('MSG:1,0,0,"sagt \\"hallo\\""')?.text).toBe('sagt "hallo"') }) it('Nicht-MSG-Zeilen sind null', () => { expect(parseMsg('PRGV:0,1,65536')).toBeNull() expect(parseMsg('irgendwas')).toBeNull() }) }) describe('progressAusPrgv', () => { it('total/max ist der Gesamtfortschritt', () => { expect(progressAusPrgv('PRGV:100,32768,65536')).toBe(50) expect(progressAusPrgv('PRGV:0,65536,65536')).toBe(100) }) it('ein echtes 0 % ist eine Angabe, keine Zeile ist -1', () => { expect(progressAusPrgv('PRGV:0,0,65536')).toBe(0) expect(progressAusPrgv('MSG:1,0,0,"x"')).toBe(-1) expect(progressAusPrgv('PRGV:1,1,0')).toBe(-1) }) }) // Fixture nach dem dokumentierten Robot-Format; die SINFO-Zeilen sind das // am 26.07.2026 an der Akira-Blu-ray abgenommene Muster. const INFO_AUSGABE = [ 'TINFO:0,8,0,"12"', 'TINFO:0,9,0,"1:57:07"', 'TINFO:0,11,0,"31474929664"', 'TINFO:1,9,0,"0:02:01"', 'TINFO:1,8,0,"2"', 'SINFO:0,0,1,6201,"Video"', 'SINFO:0,1,1,6202,"Audio"', 'SINFO:0,1,3,0,"deu"', 'SINFO:0,1,4,0,"German"', 'SINFO:0,1,6,0,"DTS-HD MA"', 'SINFO:0,1,14,0,"2"', 'SINFO:0,1,30,0,"DTS-HD MA Stereo German"', 'SINFO:0,2,1,6202,"Audio"', 'SINFO:0,2,3,0,"jpn"', 'SINFO:0,2,4,0,"Japanese"', 'SINFO:0,3,1,6203,"Subtitles"', 'SINFO:0,3,3,0,"deu"', 'SINFO:0,3,4,0,"German"', ].join('\n') describe('parseTitelInfo', () => { it('liest Dauer, Größe und Kapitel je Titel', () => { expect(parseTitelInfo(INFO_AUSGABE)).toEqual([ { nr: 0, dauerS: 7027, groesseBytes: 31_474_929_664, kapitel: 12 }, { nr: 1, dauerS: 121, groesseBytes: 0, kapitel: 2 }, ]) }) it('leere Ausgabe ist eine leere Liste', () => { expect(parseTitelInfo('')).toEqual([]) }) }) describe('parseStreamInfo + sprachenZusammenfassen', () => { it('trennt Audio und Untertitel, Sprache aus Attribut 3/4', () => { const streams = parseStreamInfo(INFO_AUSGABE) const titel0 = streams.get(0)! expect(titel0.audio.map((s) => s.lang)).toEqual(['deu', 'jpn']) expect(titel0.audio[0]).toMatchObject({ codec: 'DTS-HD MA', kanaele: '2', sprache: 'German' }) expect(titel0.untertitel.map((s) => s.lang)).toEqual(['deu']) }) it('fasst die Sprachen der ganzen Disc zusammen', () => { const sprachen = sprachenZusammenfassen(parseStreamInfo(INFO_AUSGABE)) expect(sprachen.audio).toEqual([ { lang: 'deu', sprache: 'German', spuren: 1 }, { lang: 'jpn', sprache: 'Japanese', spuren: 1 }, ]) expect(sprachen.untertitel).toEqual([{ lang: 'deu', sprache: 'German', spuren: 1 }]) }) }) describe('laengsterTitel', () => { const dauern = new Map([ [0, 7027], // Hauptfilm ~117 min [1, 7300], // „Play All" wäre länger [2, 121], ]) it('ohne Soll-Laufzeit gewinnt der längste', () => { expect(laengsterTitel(dauern)).toBe(1) }) it('mit Soll-Laufzeit gewinnt der nächstliegende (Play-All-Falle)', () => { expect(laengsterTitel(dauern, 117)).toBe(0) }) it('Soll-Laufzeit weit weg von allem → wieder der längste', () => { expect(laengsterTitel(new Map([[0, 100]]), 117)).toBe(0) expect(laengsterTitel(new Map(), 117)).toBeNull() }) }) describe('episodenTitel', () => { it('findet Titel nahe der mittleren Episoden-Laufzeit (±10 min)', () => { const dauern = new Map([ [1, 42 * 60], [2, 44 * 60], [3, 43 * 60], [9, 170 * 60], // Play All [10, 2 * 60], // Trailer ]) expect(episodenTitel(dauern, [43, 43, 42])).toEqual([1, 2, 3]) }) it('ohne Angaben keine Treffer', () => { expect(episodenTitel(new Map(), [43])).toEqual([]) expect(episodenTitel(new Map([[1, 60]]), [])).toEqual([]) }) }) describe('KRITISCHE_CODES — die gemessenen Ursachen', () => { it('kennt die vier am Commander-Laufwerk gesehenen Nummern + 5010', () => { for (const code of [2024, 5010, 5020, 5021, 5051]) { expect(KRITISCHE_CODES[code], `Code ${code}`).toBeDefined() expect(KRITISCHE_CODES[code].length).toBeGreaterThan(20) } }) })