Ampel / ampel (push) Failing after 1m22s
rip/parser.ts: die reine Textverarbeitung mit allen bezahlten Fallen aus dem Docker-Zweig — quelle() (dev:G: statt dev:\.\G:, der 404-Verwandte), textVon() (UTF-8 streng, sonst cp1252 — nie wieder 'Öffnen'), parseMsg (Regex statt split — Meldungen mit Komma), PRGV (-1 vs. echtes 0 %), TINFO/SINFO (Sprache aus Attr 3/4, NICHT 28/29), laengsterTitel mit Play-All-Schutz, episodenTitel, KRITISCHE_CODES (sprachfeste NUMMERN). rip/makemkv.ts: Kommandobau + Lauf — ZeilenLeser (binaer, halbe Zeilen, je Zeile dekodiert), RipAuswertung (pur), rippen() mit AbortSignal, Lesefehler-Flag AUCH bei Rueckgabewert 0. werkzeuge/katalog.ts: die Suchreihenfolge samt %VAR%-Gross/Klein-Falle und Registry-Rueckfall. ablauf/rippen.ts: ein Rip je Laufwerk, Status-Push, 'erst nachsehen, dann rippen' (unbekannt heisst trotzdem rippen). Fenster: Rippen-Knopf, Fortschrittsbalken, Abbrechen; Auswurf waehrend des Rips abgelehnt. Gemessen am echten System (30.08.2026): Katalog findet makemkvcon64 (Program Files x86), HandBrakeCLI und flac (beide im rc11-Werkzeugordner LOCALAPPDATA\Rippy\tools). NEUER BEFUND: Nach dem abgebrochenen rc11-Rip vom Mittag sieht MakeMKV GAR KEIN Laufwerk mehr (MSG 5042, leere DRV-Liste, mit und ohne --noscan), waehrend die Storage-IOCTLs sauber antworten — MakeMKV spricht SCSI direkt. 5042 ist jetzt ein KRITISCHER_CODE mit Klartext-Abhilfe. Der volle Rip-Beweis (messung.rip.test.ts liegt bereit) braucht deshalb erst eine Hand am Geraet: Disc neu einlegen oder USB ab-/anstecken. 96 Tests gruen, Smoke gruen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
187 lines
6.4 KiB
TypeScript
187 lines
6.4 KiB
TypeScript
// 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)
|
|
}
|
|
})
|
|
})
|