fix(v5): Serien über mehrere Discs — nur die eigenen Folgen benennen, nie überschreiben; HandBrake schreibt auf Zwischennamen

WAS:
- episodenUmbenennen bekommt die Dateien DIESER Disc (nurDiese) statt
  den ganzen Season-Ordner zu zählen. Vorher hieß es bei Disc 2 „über-
  sprungen: 4 Dateien, 2 Zuordnungen", sobald Disc 1 dort schon S01E01/
  S01E02 abgelegt hatte — Serien-Merker und „ab Folge" liefen ins Leere.
- freierName: eine vorhandene Datei gleichen Namens im Ziel wird nie
  ersetzt („title_t00 (2).mkv") — außer beim Wiederholen, wo die alte
  Fassung ausdrücklich weicht (auftrag.ersetzt).
- komprimieren() lässt HandBrake auf „<name>.teil.mkv" schreiben und
  benennt erst die fertige Datei um; nach Abbruch oder Fehler werden die
  Reste (auch eine Datei daneben) entfernt. Vorher blieb eine halbe Datei
  unter dem endgültigen Namen liegen — ohne Arbeitsordner direkt in der
  Ablage, wo Jellyfin sie als kaputten Film zeigte.
WARUM: Durchsicht 12.09.2026, Funde F3 und F9 — mit Tests (Season-Ordner
mit Bestand, Kollision, Zwischenname, Reste).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-12 21:31:38 +02:00
co-authored by Claude Opus 5
parent 8c4456d005
commit 904e4b2ef6
8 changed files with 231 additions and 21 deletions
+25
View File
@@ -172,3 +172,28 @@ describe('Encoder- und Preset-Listen', () => {
expect(parsePresetListe(liste)).toEqual(['Very Fast 2160p60 4K AV1', 'H.265 VCN 2160p 4K'])
})
})
// 5.7.0 (Durchsicht F3): Nur die Dateien DIESER Disc werden benannt — der
// Season-Ordner darf voll sein.
describe('episodenUmbenennen mit nurDiese (5.7.0)', () => {
it('Bestand im Ordner stört nicht', () => {
const umbenannt: Array<[string, string]> = []
const meldungen = episodenUmbenennen(
'S',
'Akte X',
1,
[3, 4],
{
listen: () => ['Akte X S01E01.mkv', 'Akte X S01E02.mkv', 'title_t00.mkv', 'title_t01.mkv'],
existiert: () => false,
umbenennen: (von, nach) => umbenannt.push([von, nach]),
},
['title_t00.mkv', 'title_t01.mkv'],
)
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)
})
})
+48
View File
@@ -347,3 +347,51 @@ describe('Wiederholen ersetzt die alte Fassung', () => {
expect(readFileSync(alt, 'utf8')).toBe('alt')
})
})
// ── 5.7.0: Funde der Durchsicht vom 12.09.2026 ─────────────────────────
describe('komprimierenUndAblegen — Durchsicht 5.7.0', () => {
// F3: Disc 2 derselben Staffel landet im selben Season-Ordner wie Disc 1.
// Vorher zählte die Umbenennung ALLE MKVs des Ordners („4 Dateien, 2
// Zuordnungen") und tat nichts — der Serien-Merker lief ins Leere.
it('F3: zweite Disc in denselben Season-Ordner — nur ihre Folgen werden benannt', async () => {
const { ablage, roh, dateien } = aufbau(['title_t00.mkv', 'title_t01.mkv'], ['folge', 'folge'])
const season = join(ablage, 'Serien', 'Spartacus', 'Season 01')
mkdirSync(season, { recursive: true })
writeFileSync(join(season, 'Spartacus S01E01.mkv'), 'disc1')
writeFileSync(join(season, 'Spartacus S01E02.mkv'), 'disc1')
const ergebnis = await komprimierenUndAblegen(
auftrag({ rohOrdner: roh, dateien, zuordnung: zuordnung('tv', 'Spartacus', 2010), staffel: 1, ersteFolge: 3 }),
umgebung(ablage),
() => {},
KEIN_ABBRUCH,
)
expect(ergebnis.status).toBe('fertig')
expect(readdirSync(season).filter((n) => n.endsWith('.mkv')).sort()).toEqual([
'Spartacus S01E01.mkv',
'Spartacus S01E02.mkv',
'Spartacus S01E03.mkv',
'Spartacus S01E04.mkv',
])
expect(readFileSync(join(season, 'Spartacus S01E01.mkv'), 'utf8')).toBe('disc1')
expect(ergebnis.folgenVergeben).toEqual([3, 4])
})
// F3: Liegt im Season-Ordner noch ein title_t00.mkv der ersten Disc
// (deren Umbenennung ausfiel), darf die zweite es NICHT überschreiben.
it('F3: gleichnamige Datei im Ziel wird nie überschrieben — „(2)" statt Verlust', async () => {
const { ablage, roh, dateien } = aufbau(['title_t00.mkv'], ['folge'])
const season = join(ablage, 'Serien', 'Spartacus', 'Season 01')
mkdirSync(season, { recursive: true })
writeFileSync(join(season, 'title_t00.mkv'), 'disc1-unbenannt')
const ergebnis = await komprimierenUndAblegen(
auftrag({ rohOrdner: roh, dateien, zuordnung: zuordnung('tv', 'Spartacus', 2010), staffel: 1 }),
umgebung(ablage),
() => {},
KEIN_ABBRUCH,
)
expect(ergebnis.status).toBe('fertig')
expect(readFileSync(join(season, 'title_t00.mkv'), 'utf8')).toBe('disc1-unbenannt')
expect(existsSync(join(season, 'title_t00 (2).mkv'))).toBe(true)
expect(ergebnis.dateien.map((d) => d.split(/[\\/]/).pop())).toEqual(['title_t00 (2).mkv'])
})
})
+14
View File
@@ -40,3 +40,17 @@ describe('rollenNummern', () => {
])
})
})
// 5.7.0 (Durchsicht F3): nie überschreiben — ein freier Name, wo einer
// nötig ist.
describe('freierName', () => {
it('frei bleibt frei, belegt bekommt (2), (3) …', async () => {
const { freierName } = await import('../src/kern/ablage/dateinamen')
const belegt = new Set(['E:\\S\\x.mkv', 'E:\\S\\x (2).mkv'])
expect(freierName('E:\\S\\y.mkv', (p) => belegt.has(p))).toBe('E:\\S\\y.mkv')
expect(freierName('E:\\S\\x.mkv', (p) => belegt.has(p))).toBe('E:\\S\\x (3).mkv')
expect(freierName('/a/b/x.mkv', () => true).startsWith('/a/b/x (')).toBe(true)
// Ein Punkt im Ordnernamen ist keine Endung.
expect(freierName('E:\\Akira (1988)\\ohne-endung', (p) => p === 'E:\\Akira (1988)\\ohne-endung')).toBe('E:\\Akira (1988)\\ohne-endung (2)')
})
})
+19
View File
@@ -220,3 +220,22 @@ describe('etaAusZeile — Restzeit aus der Encoding-Zeile (5.6.0, Format aus dem
expect(a.verarbeite('Encoding: task 1 of 1, 11.00 %')).toEqual({ prozent: 11 })
})
})
// 5.7.0 (Durchsicht F9): HandBrake schreibt auf einen Zwischennamen; eine
// halbe Datei trägt nie den echten Namen.
describe('Zwischenname (5.7.0)', () => {
it('zwischenname und endname gehören zusammen', async () => {
const { zwischenname, endname } = await import('../src/kern/komprimieren/handbrake')
expect(zwischenname('E:\\Filme\\Akira (1988)\\Akira (1988).mkv')).toBe('E:\\Filme\\Akira (1988)\\Akira (1988).teil.mkv')
expect(endname('E:\\Filme\\Akira (1988)\\Akira (1988).teil.mkv')).toBe('E:\\Filme\\Akira (1988)\\Akira (1988).mkv')
expect(endname('E:\\x\\title_t00.teil.mp4')).toBe('E:\\x\\title_t00.mp4')
expect(zwischenname('ohne-endung')).toBe('ohne-endung.teil')
})
it('zwischenReste räumt die Zwischen-Datei und ihre Datei daneben', async () => {
const { zwischenReste } = await import('../src/kern/komprimieren/handbrake')
const da = new Set(['E:\\x\\a.teil.mkv', 'E:\\x\\a.teil.mp4'])
const weg: string[] = []
expect(zwischenReste('E:\\x\\a.teil.mkv', (p) => weg.push(p), (p) => da.has(p))).toEqual(['E:\\x\\a.teil.mkv', 'E:\\x\\a.teil.mp4'])
expect(zwischenReste('E:\\x\\b.teil.mkv', (p) => weg.push(p), (p) => da.has(p))).toEqual([])
})
})