feat(v5): Etappe W-2 — Rippen: Parser, makemkvcon-Lauf, Werkzeug-Katalog
Ampel / ampel (push) Failing after 1m22s
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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a45069af5e
commit
844bca37ff
@@ -0,0 +1,71 @@
|
||||
// Tests des Werkzeug-Katalogs — Suchreihenfolge und die %VAR%-Falle
|
||||
// (Groß/Klein), beides ohne installierte Programme (Prüf-Funktionen
|
||||
// werden eingespritzt, Muster aus test_katalog.py).
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { entfalten, finden, kandidaten, WERKZEUGE } from '../src/kern/werkzeuge/katalog'
|
||||
|
||||
describe('entfalten — %VAR% ohne Rücksicht auf Schreibweise', () => {
|
||||
it('die gemessene Falle: PROGRAMFILES(X86) trifft %ProgramFiles(x86)%', () => {
|
||||
// Windows liefert Umgebungsnamen GROSS; das Muster schreibt sie
|
||||
// gemischt. Der strikte Vergleich ließ die bekannten Orte auf einer
|
||||
// echten Maschine NIE greifen (katalog.py, 28.08.2026).
|
||||
expect(entfalten('%ProgramFiles(x86)%\\MakeMKV', { 'PROGRAMFILES(X86)': 'C:\\Program Files (x86)' })).toBe(
|
||||
'C:\\Program Files (x86)\\MakeMKV',
|
||||
)
|
||||
})
|
||||
it('bleibt ein % stehen, ist der Kandidat unbrauchbar', () => {
|
||||
expect(entfalten('%GibtEsNicht%\\X', {})).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('kandidaten — Reihenfolge: eigener Ordner vor System', () => {
|
||||
it('eigener Ordner zuerst, dann bekannte Orte, 64 Bit zuerst', () => {
|
||||
const liste = kandidaten('makemkv', 'C:\\R\\tools', {
|
||||
'PROGRAMFILES(X86)': 'C:\\PF86',
|
||||
PROGRAMFILES: 'C:\\PF',
|
||||
})
|
||||
expect(liste[0]).toBe('C:\\R\\tools\\makemkv\\makemkvcon64.exe')
|
||||
expect(liste).toContain('C:\\PF86\\MakeMKV\\makemkvcon64.exe')
|
||||
expect(liste.indexOf('C:\\R\\tools\\makemkvcon64.exe')).toBeLessThan(
|
||||
liste.indexOf('C:\\PF86\\MakeMKV\\makemkvcon64.exe'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('finden', () => {
|
||||
const umgebung = { 'PROGRAMFILES(X86)': 'C:\\PF86', PATH: '' }
|
||||
|
||||
it('ein eingestellter Pfad gewinnt — und ein falscher wird NICHT ersetzt', () => {
|
||||
const existiert = (p: string): boolean => p === 'D:\\eigen\\makemkvcon64.exe'
|
||||
expect(finden('makemkv', 'D:\\eigen\\makemkvcon64.exe', 'C:\\R', umgebung, existiert, () => '')).toBe(
|
||||
'D:\\eigen\\makemkvcon64.exe',
|
||||
)
|
||||
// Tippfehler soll als Fehler sichtbar werden, nicht still anderswo landen.
|
||||
expect(finden('makemkv', 'D:\\tippfehler.exe', 'C:\\R', umgebung, existiert, () => '')).toBe('')
|
||||
})
|
||||
|
||||
it('findet über die bekannten Orte', () => {
|
||||
const existiert = (p: string): boolean => p === 'C:\\PF86\\MakeMKV\\makemkvcon64.exe'
|
||||
expect(finden('makemkv', '', 'C:\\R', umgebung, existiert, () => '')).toBe(
|
||||
'C:\\PF86\\MakeMKV\\makemkvcon64.exe',
|
||||
)
|
||||
})
|
||||
|
||||
it('fällt zuletzt auf die Registry zurück', () => {
|
||||
expect(finden('makemkv', '', 'C:\\R', umgebung, () => false, () => 'C:\\Reg\\makemkvcon64.exe')).toBe(
|
||||
'C:\\Reg\\makemkvcon64.exe',
|
||||
)
|
||||
})
|
||||
|
||||
it('nichts gefunden ist "", kein Raten', () => {
|
||||
expect(finden('makemkv', '', 'C:\\R', umgebung, () => false, () => '')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('WERKZEUGE — die Pflicht-Markierungen', () => {
|
||||
it('nur MakeMKV ist Pflicht; ohne HandBrake/FLAC läuft Rippy eingeschränkt', () => {
|
||||
expect(WERKZEUGE.makemkv.pflicht).toBe(true)
|
||||
expect(WERKZEUGE.handbrake.pflicht).toBe(false)
|
||||
expect(WERKZEUGE.flac.pflicht).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
// MESSUNG an echter Hardware: makemkvcon finden und die eingelegte Disc
|
||||
// befragen (Titel + Sprachen). Läuft nur auf Verlangen:
|
||||
//
|
||||
// RIPPY_MESSUNG=1 npx vitest run test/messung.makemkv.test.ts
|
||||
//
|
||||
// Der VOLLE Rip ist ein eigener Lauf (messung.rip.test.ts) — er dauert
|
||||
// je Disc 30–90 Minuten.
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const messen = process.env.RIPPY_MESSUNG === '1' && process.platform === 'win32'
|
||||
|
||||
describe.skipIf(!messen)('makemkvcon am echten System', () => {
|
||||
it('wird über den Katalog gefunden', async () => {
|
||||
const { finden } = await import('../src/kern/werkzeuge/katalog')
|
||||
const pfad = finden('makemkv')
|
||||
console.log(`MESSUNG makemkvcon: ${pfad.length > 0 ? pfad : 'NICHT GEFUNDEN'}`)
|
||||
const handbrake = finden('handbrake')
|
||||
console.log(`MESSUNG HandBrakeCLI: ${handbrake.length > 0 ? handbrake : 'NICHT GEFUNDEN'}`)
|
||||
const flac = finden('flac')
|
||||
console.log(`MESSUNG flac: ${flac.length > 0 ? flac : 'NICHT GEFUNDEN'}`)
|
||||
expect(pfad.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('liest Titel und Sprachen der eingelegten Disc', { timeout: 360_000 }, async () => {
|
||||
const { finden } = await import('../src/kern/werkzeuge/katalog')
|
||||
const { titelInfoLesen } = await import('../src/kern/rip/makemkv')
|
||||
const { win32Api } = await import('../src/kern/laufwerk/win32')
|
||||
const { listOptischeLaufwerke } = await import('../src/kern/laufwerk/disc')
|
||||
|
||||
const programm = finden('makemkv')
|
||||
expect(programm.length).toBeGreaterThan(0)
|
||||
const geraete = listOptischeLaufwerke(win32Api())
|
||||
expect(geraete.length).toBeGreaterThan(0)
|
||||
|
||||
const info = await titelInfoLesen(programm, geraete[0])
|
||||
console.log(`MESSUNG Titel: ${info.titel.length}`)
|
||||
for (const t of info.titel.slice(0, 12)) {
|
||||
console.log(
|
||||
`MESSUNG #${t.nr}: ${Math.floor(t.dauerS / 60)}min ${t.dauerS % 60}s · ` +
|
||||
`${(t.groesseBytes / 1e9).toFixed(2)} GB · ${t.kapitel} Kapitel`,
|
||||
)
|
||||
}
|
||||
console.log(
|
||||
`MESSUNG Sprachen Ton: ${info.sprachen.audio.map((s) => `${s.lang}(${s.spuren})`).join(', ') || '—'}`,
|
||||
)
|
||||
console.log(
|
||||
`MESSUNG Sprachen Untertitel: ${info.sprachen.untertitel.map((s) => `${s.lang}(${s.spuren})`).join(', ') || '—'}`,
|
||||
)
|
||||
expect(info.titel.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe.skipIf(messen)('Messung übersprungen', () => {
|
||||
it('läuft nur mit RIPPY_MESSUNG=1 unter Windows', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
// DER W-2-BEWEIS: ein voller, verlustfreier Rip der eingelegten Disc am
|
||||
// echten Laufwerk — dauert 30–90 Minuten. Läuft nur auf Verlangen:
|
||||
//
|
||||
// RIPPY_MESSUNG_RIP=1 RIPPY_MESSUNG_ZIEL=<ordner> \
|
||||
// npx vitest run test/messung.rip.test.ts --reporter=verbose
|
||||
//
|
||||
// Fortschritt wird in 5-%-Schritten geloggt; das Ergebnis nennt Dateien
|
||||
// und Größen. Das Protokoll wandert in den SAVEPOINT (beweisen statt
|
||||
// behaupten).
|
||||
import { statSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const messen = process.env.RIPPY_MESSUNG_RIP === '1' && process.platform === 'win32'
|
||||
const zielWurzel = process.env.RIPPY_MESSUNG_ZIEL ?? ''
|
||||
|
||||
describe.skipIf(!messen)('Voller Rip am echten Laufwerk', () => {
|
||||
it('rippt die eingelegte Disc verlustfrei', { timeout: 3 * 60 * 60 * 1000 }, async () => {
|
||||
expect(zielWurzel.length, 'RIPPY_MESSUNG_ZIEL fehlt').toBeGreaterThan(0)
|
||||
const { finden } = await import('../src/kern/werkzeuge/katalog')
|
||||
const { rippen } = await import('../src/kern/rip/makemkv')
|
||||
const { win32Api } = await import('../src/kern/laufwerk/win32')
|
||||
const { listOptischeLaufwerke } = await import('../src/kern/laufwerk/disc')
|
||||
|
||||
const programm = finden('makemkv')
|
||||
expect(programm.length).toBeGreaterThan(0)
|
||||
const geraete = listOptischeLaufwerke(win32Api())
|
||||
expect(geraete.length).toBeGreaterThan(0)
|
||||
|
||||
const start = Date.now()
|
||||
let letzteStufe = -5
|
||||
const ergebnis = await rippen({
|
||||
programm,
|
||||
geraetepfad: geraete[0],
|
||||
zielOrdner: zielWurzel,
|
||||
titel: 'all',
|
||||
aufFortschritt: (prozent) => {
|
||||
if (prozent >= letzteStufe + 5) {
|
||||
letzteStufe = prozent
|
||||
const minuten = ((Date.now() - start) / 60000).toFixed(1)
|
||||
console.log(`RIP-FORTSCHRITT ${prozent}% nach ${minuten}min`)
|
||||
}
|
||||
},
|
||||
aufMeldung: (code, text) => {
|
||||
// Nur die tragenden Meldungen — sonst wird das Protokoll unlesbar.
|
||||
if (code >= 5000 || text.includes('errors') || text.includes('gesichert') || text.includes('saved')) {
|
||||
console.log(`RIP-MSG ${code}: ${text}`)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const minuten = ((Date.now() - start) / 60000).toFixed(1)
|
||||
if (ergebnis.status === 'success') {
|
||||
console.log(`RIP-ERGEBNIS success nach ${minuten}min · lesefehler=${ergebnis.lesefehler}`)
|
||||
for (const datei of ergebnis.dateien) {
|
||||
console.log(`RIP-DATEI ${datei} · ${(statSync(datei).size / 1e9).toFixed(2)} GB`)
|
||||
}
|
||||
} else {
|
||||
console.log(`RIP-ERGEBNIS ${ergebnis.status} nach ${minuten}min: ${'fehler' in ergebnis ? ergebnis.fehler : ''}`)
|
||||
}
|
||||
expect(ergebnis.status).toBe('success')
|
||||
})
|
||||
})
|
||||
|
||||
describe.skipIf(messen)('Rip-Messung übersprungen', () => {
|
||||
it('läuft nur mit RIPPY_MESSUNG_RIP=1 unter Windows', () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
// Tests der Rip-Lauf-Bausteine: Zeilenzerlegung (binär, halbe Zeilen)
|
||||
// und die Auswertung (Fortschritt, Lesefehler, kritische Ursachen) —
|
||||
// alles ohne echtes makemkvcon.
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildInfoArgs, buildRipArgs, RipAuswertung, ZeilenLeser } from '../src/kern/rip/makemkv'
|
||||
|
||||
describe('Kommandobau', () => {
|
||||
it('info: Robot-Mode ohne Geräte-Scan', () => {
|
||||
expect(buildInfoArgs('\\\\.\\G:')).toEqual(['-r', '--noscan', 'info', 'dev:G:'])
|
||||
})
|
||||
it('mkv: Fortschritt in denselben Strom', () => {
|
||||
expect(buildRipArgs('\\\\.\\G:', 'C:\\ziel', 'all')).toEqual([
|
||||
'-r',
|
||||
'--noscan',
|
||||
'--progress=-same',
|
||||
'mkv',
|
||||
'dev:G:',
|
||||
'all',
|
||||
'C:\\ziel',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('ZeilenLeser — binär, halbe Zeilen bleiben liegen', () => {
|
||||
it('setzt Zeilen über Stück-Grenzen zusammen', () => {
|
||||
const leser = new ZeilenLeser()
|
||||
expect(leser.hinzu(Buffer.from('PRGV:1,2,'))).toEqual([])
|
||||
expect(leser.hinzu(Buffer.from('65536\r\nMSG:1,0,0,"a"\nRest'))).toEqual([
|
||||
'PRGV:1,2,65536',
|
||||
'MSG:1,0,0,"a"',
|
||||
])
|
||||
expect(leser.schluss()).toEqual(['Rest'])
|
||||
})
|
||||
it('dekodiert jede Zeile einzeln — auch cp1252 mitten im Strom', () => {
|
||||
const leser = new ZeilenLeser()
|
||||
const zeilen = leser.hinzu(
|
||||
Buffer.concat([Buffer.from('MSG:5010,0,0,"Das '), Buffer.from('Öffnen', 'latin1'), Buffer.from('"\n')]),
|
||||
)
|
||||
expect(zeilen).toEqual(['MSG:5010,0,0,"Das Öffnen"'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('RipAuswertung', () => {
|
||||
it('meldet Fortschritt nur bei Änderung', () => {
|
||||
const a = new RipAuswertung()
|
||||
expect(a.verarbeite('PRGV:0,32768,65536')).toEqual({ prozent: 50 })
|
||||
expect(a.verarbeite('PRGV:0,32769,65536')).toEqual({})
|
||||
expect(a.verarbeite('PRGV:0,45876,65536')).toEqual({ prozent: 70 })
|
||||
})
|
||||
|
||||
it('erkennt Lesefehler an der sprachfesten URL — auch auf Deutsch', () => {
|
||||
const a = new RipAuswertung()
|
||||
a.verarbeite(
|
||||
'MSG:2003,0,3,"Encountered 29 errors of type \'Read Error\' - see http://www.makemkv.com/errors/read/","f"',
|
||||
)
|
||||
expect(a.lesefehler).toBe(true)
|
||||
})
|
||||
|
||||
it('sammelt kritische URSACHEN über die Nummer, nicht den Text', () => {
|
||||
const a = new RipAuswertung()
|
||||
a.verarbeite('MSG:5051,0,0,"Der Testzeitraum ist abgelaufen. Erwerben Sie bitte einen Schlüssel"')
|
||||
a.verarbeite('MSG:5010,0,0,"Das Öffnen der Disk schlug fehl"')
|
||||
const fehler = a.fehlerText(11, false)
|
||||
expect(fehler).toContain('Code 11')
|
||||
expect(fehler).toContain('nicht freigeschaltet')
|
||||
expect(fehler).toContain('keine MKV-Datei entstanden')
|
||||
})
|
||||
|
||||
it('englische Textmuster bleiben als Rückfall', () => {
|
||||
const a = new RipAuswertung()
|
||||
a.verarbeite('MSG:9999,0,0,"AACS volume key is unknown for this disc"')
|
||||
expect(a.kritischeMeldungen).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,186 @@
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user