// Tests der Ablage — sichere Namen, Zielstruktur, Episoden-Zuordnung, NFO. // Testfälle sinngemäß aus test_medien.py (Docker-Zweig). import { join } from 'node:path' import { describe, expect, it } from 'vitest' import { baueNfo } from '../src/kern/ablage/nfo' import { episodenUmbenennen, matcheEpisoden, serienOrdner, sichererName, zielordner, } from '../src/kern/ablage/struktur' import { parseEncoderListe, leiteBackendsAb, parsePresetListe } from '../src/kern/komprimieren/encoder' describe('sichererName', () => { it('entfernt Verbotenes, behält Leerzeichen, hängt das Jahr an', () => { expect(sichererName('Der Herr der Ringe: Die Gefährten', 2001)).toBe( 'Der Herr der Ringe Die Gefährten (2001)', ) }) it('kein doppeltes Jahr (der OMDb-Fund)', () => { expect(sichererName('X (2009)', 2009)).toBe('X (2009)') }) it('leerer Titel bleibt leer, Punkte am Ende fallen', () => { expect(sichererName('...')).toBe('') expect(sichererName('Film.', null)).toBe('Film') }) }) describe('zielordner', () => { it('sprechender Name; bei Kollision Job-Kennung statt Überschreiben', () => { // Erwartungen über join() — die Funktionen arbeiten mit den // Pfadregeln der LAUFZEIT-Plattform (Linux-CI testet mit POSIX). expect(zielordner('Filme', 'Akira', 1988, 'job-1234abcd', () => false)).toBe( join('Filme', 'Akira (1988)'), ) expect(zielordner('Filme', 'Akira', 1988, 'job-1234abcd', () => true)).toBe( join('Filme', 'Akira (1988) [job-1234]'), ) expect(zielordner('Filme', '', null, 'job-1', () => false)).toBe(join('Filme', 'job-1')) }) }) describe('serienOrdner', () => { it('Jellyfin-Schema Serie/Season NN', () => { expect(serienOrdner('Serien', 'Dr. Who?', 2)).toBe(join('Serien', 'Dr. Who', 'Season 02')) }) }) describe('matcheEpisoden', () => { it('Stufe 1: komplette Staffel in Reihenfolge — auch bei uniformen Zeiten', () => { expect( matcheEpisoden( [1420, 1425, 1418], [ [1, 1420], [2, 1420], [3, 1420], ], ), ).toEqual([1, 2, 3]) }) it('Stufe 2: distinkte Laufzeiten, ordnungserhaltend', () => { expect( matcheEpisoden( [1200, 2400], [ [1, 1210], [2, 2390], [3, 3000], ], ), ).toEqual([1, 2]) }) it('mehrdeutig heißt null — lieber gar nicht als falsch', () => { expect( matcheEpisoden( [1420, 1421], [ [1, 1420], [2, 1420], [3, 1420], ], ), ).toBeNull() }) it('keine Daten heißt null', () => { expect(matcheEpisoden([], [[1, 100]])).toBeNull() expect(matcheEpisoden([100], [])).toBeNull() }) }) describe('episodenUmbenennen', () => { it('benennt in Disc-Reihenfolge um, überschreibt nie', () => { const umbenannt: Array<[string, string]> = [] const meldungen = episodenUmbenennen('S', 'Akte X', 1, [3, 4], { listen: () => ['title_t01.mkv', 'title_t00.mkv'], existiert: () => false, umbenennen: (von, nach) => umbenannt.push([von, nach]), }) expect(umbenannt).toEqual([ [join('S', 'title_t00.mkv'), join('S', 'Akte X S01E03.mkv')], [join('S', 'title_t01.mkv'), join('S', 'Akte X S01E04.mkv')], ]) expect(meldungen).toHaveLength(2) }) it('Zahl passt nicht → gar nichts anfassen', () => { const meldungen = episodenUmbenennen('E:\\S', 'X', 1, [1], { listen: () => ['a.mkv', 'b.mkv'], existiert: () => false, umbenennen: () => { throw new Error('darf nicht') }, }) expect(meldungen[0]).toContain('übersprungen') }) }) describe('baueNfo', () => { it('movie.nfo mit XML-Escaping', () => { const nfo = baueNfo( { beschreibung: 'Roboter & Menschen', genres: ['Sci-Fi'], laufzeitMinuten: 124 }, 'Akira ', 1988, ) expect(nfo).toContain('') expect(nfo).toContain('Akira <Director\'s Cut>') expect(nfo).toContain('Roboter & Menschen') expect(nfo).toContain('1988') expect(nfo).toContain('124') expect(nfo).toContain('Source: Rippy') }) it('Serie bekommt tvshow als Wurzel', () => { expect(baueNfo({ typ: 'serie' }, 'Akte X')).toContain('') }) }) // caps.py-Fixtures: das --help- und --preset-list-Format, am echten // HandBrake gemessen (25./26.07.2026). describe('Encoder- und Preset-Listen', () => { it('parseEncoderListe liest den -e-Block bis zur nächsten Option', () => { const hilfe = [ ' -e, --encoder Select video encoder:', ' svt_av1', ' x264', ' x265', ' nvenc_h264', ' --encoder-preset ', ' ignored', ].join('\n') expect(parseEncoderListe(hilfe)).toEqual(['svt_av1', 'x264', 'x265', 'nvenc_h264']) }) it('leiteBackendsAb: Familien statt Sammelbegriff, AV1 sichtbar', () => { expect(leiteBackendsAb(['x264', 'x265', 'svt_av1', 'vce_h265', 'vce_av1'])).toEqual([ 'cpu-x264', 'cpu-x265', 'cpu-av1', 'vce', 'vce-av1', ]) expect(leiteBackendsAb([])).toEqual([]) }) it('parsePresetListe: genau vier Leerzeichen sind ein Name', () => { const liste = [ 'General/', ' Very Fast 2160p60 4K AV1', ' AV1 video (up to 2160p60) and AAC stereo audio.', 'Hardware/', ' H.265 VCN 2160p 4K', '[10:26:05] qsv: not available on this system', ].join('\n') expect(parsePresetListe(liste)).toEqual(['Very Fast 2160p60 4K AV1', 'H.265 VCN 2160p 4K']) }) })