Files
rippy/rippy-windows/test/ablage.test.ts
T
HitonabiandClaude Fable 5 e840b7f327
Ampel / ampel (push) Successful in 1m21s
fix(v5): Laufzeit-Pfade plattformneutral — pipeline/struktur auf node:path
Der NFO-Ablage-Test brach in der Linux-CI: pipeline/struktur bauten mit
path.win32 auf POSIX-Testordnern (mkdir legte 'x\Filme' als EINEN Namen
an, readdir fand nichts). Regel jetzt dokumentiert: Module ueber
LAUFZEIT-Wurzeln (Ablage) nutzen plattform-path — zur echten Laufzeit
immer Windows, in der CI physisch korrekt auf POSIX; Module mit FESTER
Windows-Semantik (katalog, dateiDaneben) bleiben bei path.win32.
Testerwartungen entsprechend ueber join() statt Literale.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 18:29:46 +02:00

175 lines
5.7 KiB
TypeScript

// 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 <Director\'s Cut>',
1988,
)
expect(nfo).toContain('<movie>')
expect(nfo).toContain('<title>Akira &lt;Director\'s Cut&gt;</title>')
expect(nfo).toContain('<plot>Roboter &amp; Menschen</plot>')
expect(nfo).toContain('<year>1988</year>')
expect(nfo).toContain('<runtime>124</runtime>')
expect(nfo).toContain('Source: Rippy')
})
it('Serie bekommt tvshow als Wurzel', () => {
expect(baueNfo({ typ: 'serie' }, 'Akte X')).toContain('<tvshow>')
})
})
// 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 <string> Select video encoder:',
' svt_av1',
' x264',
' x265',
' nvenc_h264',
' --encoder-preset <string>',
' 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'])
})
})