// Welche Titel schlägt Rippy vor? — und wann sagt es „ich bin unsicher"? // // Anlass (01.09.2026): Bis dahin übergab die Pipeline fest `titel: 'all'` // — Rippy rippte IMMER alles, inklusive Trailer, Werbung und // Alterskennzeichen. Der Commander: „es fehlt generell noch komplett die // auswahl WAS man rippen möchte." import { describe, expect, it } from 'vitest' import { offsetAusMeldung } from '../src/kern/rip/parser' import { HAENGT_AB, RipAuswertung } from '../src/kern/rip/makemkv' import { AEHNLICH, KURZ_S, titelVorauswahl } from '../src/kern/rip/titelwahl' import type { TitelInfo } from '../src/kern/rip/parser' function titel(nr: number, minuten: number, kapitel = 8, gb = 1): TitelInfo { return { nr, dauerS: Math.round(minuten * 60), groesseBytes: Math.round(gb * 1e9), kapitel } } const KEIN_HINWEIS = { istSerie: false, folgen: 0 } describe('titelVorauswahl — Serie', () => { it('die echte Spartacus-Disc: zwei Folgen, sonst nichts', () => { // Gemessen an der Disc: zwei große Ströme (15,76 GB / 14,89 GB) und // Kleinkram. Das bdmt-Inhaltsverzeichnis nennt EP 1 und EP 2. const wahl = titelVorauswahl( [titel(0, 52.2, 8, 15.76), titel(1, 49.6, 8, 14.89), titel(2, 0.5, 1), titel(3, 0.2, 1)], { istSerie: true, folgen: 2 }, ) expect(wahl.titel.filter((t) => t.gewaehlt).map((t) => t.nr)).toEqual([0, 1]) expect(wahl.unsicher).toBe(false) expect(wahl.einordnung).toContain('2 Folgen erwartet, 2 gefunden') expect(wahl.titel[0].grund).toBe('Folge') expect(wahl.titel[3].grund).toContain('zu kurz') }) it('weniger Folgen gefunden als erwartet — Rippy sagt es', () => { const wahl = titelVorauswahl([titel(0, 50), titel(1, 0.4, 1)], { istSerie: true, folgen: 3 }) expect(wahl.unsicher).toBe(true) expect(wahl.einordnung).toContain('nur 1 gefunden') }) it('gefundene Folgen sind unterschiedlich lang — auch das sagt Rippy', () => { // 50 min und 20 min sind keine Staffel; die Auswahl bleibt, aber // Rippy behauptet nicht, sie sei sicher. const wahl = titelVorauswahl([titel(0, 50), titel(1, 20)], { istSerie: true, folgen: 2 }) expect(wahl.titel.filter((t) => t.gewaehlt).map((t) => t.nr)).toEqual([0, 1]) expect(wahl.unsicher).toBe(true) expect(wahl.einordnung).toContain('unterschiedlich lang') }) it('ähnlich lange Folgen bleiben innerhalb der Toleranz sicher', () => { // 45 und 50 Minuten liegen bei 10 % — unter AEHNLICH. expect((50 - 45) / 50).toBeLessThan(AEHNLICH) const wahl = titelVorauswahl([titel(0, 50), titel(1, 45)], { istSerie: true, folgen: 2 }) expect(wahl.unsicher).toBe(false) }) }) describe('titelVorauswahl — Film', () => { it('der längste Titel ist der Hauptinhalt', () => { const wahl = titelVorauswahl([titel(0, 118), titel(1, 4), titel(2, 2)], KEIN_HINWEIS) expect(wahl.titel.filter((t) => t.gewaehlt).map((t) => t.nr)).toEqual([0]) expect(wahl.unsicher).toBe(false) expect(wahl.titel[0].grund).toBe('Hauptinhalt') expect(wahl.titel[1].grund).toBe('Zugabe') }) it('DIE ANGEL-FALLE: mehrere fast gleich lange Fassungen', () => { // Seamless Branching / Kamerawinkel — dieselbe Fassung in Varianten. // Rippy nimmt die mit den meisten Kapiteln UND sagt, dass es unsicher ist. const wahl = titelVorauswahl( [titel(0, 120, 12), titel(1, 119, 24), titel(2, 118, 6)], KEIN_HINWEIS, ) expect(wahl.titel.filter((t) => t.gewaehlt).map((t) => t.nr)).toEqual([1]) expect(wahl.unsicher).toBe(true) expect(wahl.einordnung).toContain('fast gleich lang') }) }) describe('titelVorauswahl — die Sammeltitel-Falle', () => { it('„alle Folgen am Stück" gewinnt NICHT die Film-Auswahl', () => { // 150 min = 50 + 50 + 50. Ohne diese Regel wäre der Sammeltitel der // längste — und Rippy würde alles doppelt sichern. const wahl = titelVorauswahl( [titel(0, 150), titel(1, 50), titel(2, 50), titel(3, 50)], KEIN_HINWEIS, ) expect(wahl.titel[0].gewaehlt).toBe(false) expect(wahl.titel[0].grund).toContain('Sammeltitel') expect(wahl.titel.filter((t) => t.gewaehlt).map((t) => t.nr)).toEqual([1]) }) it('auch bei einer Serie bleibt der Sammeltitel außen vor', () => { const wahl = titelVorauswahl( [titel(0, 100), titel(1, 50), titel(2, 50)], { istSerie: true, folgen: 2 }, ) expect(wahl.titel.filter((t) => t.gewaehlt).map((t) => t.nr)).toEqual([1, 2]) }) it('ein einzelner langer Titel ist KEIN Sammeltitel', () => { // Ein Film mit einem Trailer daneben — 118 min sind nicht die Summe // von irgendetwas, sonst würde jeder Film als Sammeltitel gelten. const wahl = titelVorauswahl([titel(0, 118), titel(1, 4)], KEIN_HINWEIS) expect(wahl.titel[0].gewaehlt).toBe(true) }) }) describe('titelVorauswahl — wenn nichts passt, wird nichts behauptet', () => { it('nur Schnipsel: keine Vorauswahl, aber eine Ansage', () => { const wahl = titelVorauswahl([titel(0, 1, 1), titel(1, 0.5, 1)], KEIN_HINWEIS) expect(wahl.titel.every((t) => !t.gewaehlt)).toBe(true) expect(wahl.unsicher).toBe(true) expect(wahl.einordnung).toContain(`${Math.round(KURZ_S / 60)} Minuten`) }) it('gar keine Titel', () => { const wahl = titelVorauswahl([], KEIN_HINWEIS) expect(wahl.titel).toEqual([]) expect(wahl.unsicher).toBe(true) }) }) describe('titelVorauswahl — die Rolle bestimmt, WOHIN abgelegt wird', () => { it('Serie: gewählte Titel sind Folgen', () => { const wahl = titelVorauswahl([titel(0, 52), titel(1, 50), titel(2, 3)], { istSerie: true, folgen: 2 }) expect(wahl.titel.filter((t) => t.gewaehlt).map((t) => t.rolle)).toEqual(['folge', 'folge']) }) it('Film: der Hauptinhalt ist Hauptfilm, alles andere Extra', () => { // Hakt der Nutzer eine Zugabe zusätzlich an, landet sie damit unter // extras/ statt als zweiter Film daneben. const wahl = titelVorauswahl([titel(0, 118), titel(1, 6), titel(2, 4)], KEIN_HINWEIS) expect(wahl.titel[0].rolle).toBe('hauptfilm') expect(wahl.titel[1].rolle).toBe('extra') expect(wahl.titel[2].rolle).toBe('extra') }) it('ohne Vorauswahl ist nichts Hauptfilm', () => { const wahl = titelVorauswahl([titel(0, 1, 1), titel(1, 0.5, 1)], KEIN_HINWEIS) expect(wahl.titel.every((t) => t.rolle === 'extra')).toBe(true) }) }) // ── Der Defekt-Wächter ────────────────────────────────────────────────── // Gemessen an der Spartacus-Disc (01.09.2026): MakeMKV meldete eine halbe // Stunde lang denselben Offset, makemkvcon verbrauchte dabei 1,7 s CPU // (es wartete also), und Windows protokollierte 41 fehlerhafte Blöcke in // 10 Minuten. Dieselbe Stelle wieder und wieder ist kein Fortschritt. const FEHLER_DE = "Fehler 'SCSI-Fehler - MEDIUM ERROR:L-EC UNCORRECTABLE ERROR' trat auf während des Auslesens von '/BDMV/STREAM/00047.m2ts' bei Offset '56881152'" const FEHLER_EN = "Error 'SCSI error - MEDIUM ERROR:L-EC UNCORRECTABLE ERROR' occurred while reading '/BDMV/STREAM/00047.m2ts' at offset '56881152'" describe('offsetAusMeldung', () => { it('liest die Stelle — unabhängig von MakeMKVs Sprache', () => { expect(offsetAusMeldung(FEHLER_DE)).toBe('56881152') expect(offsetAusMeldung(FEHLER_EN)).toBe('56881152') }) it('ohne Stelle bleibt es leer', () => { expect(offsetAusMeldung('Kopiere 2 Titel')).toBe('') expect(offsetAusMeldung('')).toBe('') // Eine einzelne Ziffer ist eine Titelnummer, kein Offset. expect(offsetAusMeldung("Titel '3'")).toBe('') }) }) describe('RipAuswertung — hängt der Lauf an einer festen Stelle?', () => { const zeile = (text: string): string => `MSG:2003,0,3,"${text}"` it('dieselbe Stelle immer wieder = Defekt', () => { const a = new RipAuswertung() for (let i = 0; i < HAENGT_AB; i++) a.verarbeite(zeile(FEHLER_DE)) expect(a.haengtBei).toBe('56881152') expect(a.haengt()).toBe(true) }) it('ein kurzer Aussetzer ist noch kein Defekt', () => { const a = new RipAuswertung() for (let i = 0; i < HAENGT_AB - 1; i++) a.verarbeite(zeile(FEHLER_DE)) expect(a.haengt()).toBe(false) }) it('Fortschritt räumt den Verdacht ab — es ging ja weiter', () => { const a = new RipAuswertung() for (let i = 0; i < HAENGT_AB; i++) a.verarbeite(zeile(FEHLER_DE)) expect(a.haengt()).toBe(true) a.verarbeite('PRGV:16384,16384,65536') expect(a.haengt()).toBe(false) expect(a.haengtBei).toBe('') }) it('wandernde Stellen sind normale Lesefehler, kein Hängen', () => { const a = new RipAuswertung() for (let i = 0; i < HAENGT_AB * 2; i++) { a.verarbeite(zeile(FEHLER_DE.replace('56881152', String(56881152 + i * 2048)))) } expect(a.haengt()).toBe(false) }) })