real-debrid-downloader/tests/auto-rename.test.ts
Sucukdeluxe 3ed3877ac9 chore: remove all source code comments and internal artifacts
Strip every comment from the source (parsed with the TypeScript compiler so
strings, template literals, regex literals and JSX are never touched), and drop
internal/working artifacts that do not belong in the public repository
(design mockups, internal analysis docs, a stray backup file and an old log).
No functional change: build is green, the full test suite passes.
2026-06-06 04:53:54 +02:00

1090 lines
43 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
extractEpisodeToken,
applyEpisodeTokenToFolderName,
sourceHasRpToken,
ensureRepackToken,
buildAutoRenameBaseName,
buildAutoRenameBaseNameFromFolders,
buildAutoRenameBaseNameFromFoldersWithOptions,
hasMeaningfulSeriesPrefix,
looksLikeObfuscatedSceneFileName,
decideAutoRenameBaseName,
isBonusContent
} from "../src/main/download-manager";
describe("decideAutoRenameBaseName (shared naming decision — used by auto-rename AND mkv-collect)", () => {
it("derives the clean name for a Herzflimmern episode from the per-episode folder (S07E12 — the reported failure)", () => {
const source = "tvarchiv.herzflimmern.die.klinik.am.see.s07e12-720.mkv";
const folders = [
"Herzflimmern.Die.Klinik.am.See.S07E12.German.720p.Webrip.x264-TVARCHiV",
"Herzflimmern.Die.Klinik.am.See.S07.German.720p.Webrip.x264-TVARCHiV"
];
const decision = decideAutoRenameBaseName(
folders,
source,
"tvarchiv.herzflimmern.die.klinik.am.see.s07e12-720",
folders[0],
folders[1]
);
expect(decision.kind).toBe("rename");
expect(decision.kind === "rename" && decision.baseName).toBe("Herzflimmern.Die.Klinik.am.See.S07E12.German.720p.Webrip.x264-TVARCHiV");
});
it("derives the clean name from a SEASON-only folder by injecting the source episode token (Herzflimmern S03E14)", () => {
const source = "tvarchiv.herzflimmern.die.klinik.am.see.s03e14-720.mkv";
const seasonFolder = "Herzflimmern.die.Klinik.am.See.S03.German.720p.Webrip.x264-TVARCHiV";
const decision = decideAutoRenameBaseName(
[seasonFolder],
source,
"tvarchiv.herzflimmern.die.klinik.am.see.s03e14-720",
seasonFolder,
seasonFolder
);
expect(decision.kind).toBe("rename");
expect(decision.kind === "rename" && decision.baseName).toBe("Herzflimmern.die.Klinik.am.See.S03E14.German.720p.Webrip.x264-TVARCHiV");
});
it("derives the clean name for the Fritzie S04 files that sat raw in Downloader Unfertig (4sf- scene group, season folder)", () => {
const source = "4sf-fritzie.himmel.muss.warten.web.7p-s04e01.mkv";
const seasonFolder = "Fritzie.-.Der.Himmel.muss.warten.S04.GERMAN.720p.WEB.AVC-4SF";
const decision = decideAutoRenameBaseName(
[seasonFolder],
source,
"4sf-fritzie.himmel.muss.warten.web.7p-s04e01",
seasonFolder,
seasonFolder
);
expect(decision.kind).toBe("rename");
expect(decision.kind === "rename" && decision.baseName).toBe("Fritzie.-.Der.Himmel.muss.warten.S04E01.GERMAN.720p.WEB.AVC-4SF");
});
it("is idempotent: an already-clean file in its clean folder derives to the same name (no worse-than-now)", () => {
const clean = "Herzflimmern.Die.Klinik.am.See.S07E02.German.720p.Webrip.x264-TVARCHiV";
const decision = decideAutoRenameBaseName(
[clean, "Herzflimmern.Die.Klinik.am.See.S07.German.720p.Webrip.x264-TVARCHiV"],
`${clean}.mkv`,
clean,
clean,
"Herzflimmern.Die.Klinik.am.See.S07.German.720p.Webrip.x264-TVARCHiV"
);
expect(decision.kind).toBe("rename");
expect(decision.kind === "rename" && decision.baseName).toBe(clean);
});
it("GUARD: lets the parent folder token override an OBFUSCATED source filename (anti-piracy scramble)", () => {
const decision = decideAutoRenameBaseName(
["Die.Thundermans.S02E01.Der.Thunder.Van.GERMAN.x264-aWake"],
"awa-diethundermans02e16hd.mkv",
"awa-diethundermans02e16hd",
"Die.Thundermans.S02E01.Der.Thunder.Van.GERMAN.x264-aWake",
"Die.Thundermans.S02.GERMAN.x264-aWake"
);
expect(decision.kind).toBe("rename");
expect(decision.kind === "rename" && decision.baseName).toContain("S02E01");
});
it("GUARD: a CLEAN scene source is NEVER overridden by a mismatching folder token (folder is wrong, not the file)", () => {
const decision = decideAutoRenameBaseName(
["The.Royals.2015.S01E08.German.DL.720p.BluRay.x264-iNTENTiON"],
"the.royals.2015.s01e09.german.dl.720p.bluray.x264-j4f.mkv",
"the.royals.2015.s01e09.german.dl.720p.bluray.x264-j4f",
"The.Royals.2015.S01E08.German.DL.720p.BluRay.x264-iNTENTiON",
"The.Royals.2015.S01.German.DL.720p.BluRay.x264-iNTENTiON"
);
expect(decision.kind).toBe("skip");
expect(decision.kind === "skip" && decision.reason).toBe("token-mismatch");
});
it("skips (no-target) when no folder candidate yields a usable scene name", () => {
const decision = decideAutoRenameBaseName(
["random user folder", "another plain dir"],
"some.file.mkv",
"some.file",
"random user folder",
"another plain dir"
);
expect(decision.kind).toBe("skip");
});
it("uses the CLEAN per-episode folder (scene group WITH underscore, e.g. -idTV_iNT) — not the obfuscated package folder", () => {
const epFolder = "Castle.S08E02.GERMAN.DL.720p.WEB.H264-idTV_iNT";
const decision = decideAutoRenameBaseName(
[epFolder, "scn2-cstl7"],
"castle.s08e02.german.dl.720p.web.h264-idtv_int.mkv",
"castle.s08e02.german.dl.720p.web.h264-idtv_int",
epFolder,
"scn2-cstl7"
);
expect(decision.kind).toBe("rename");
expect(decision.kind === "rename" && decision.baseName).toBe(epFolder);
});
it("uses the complete per-episode folder when the SOURCE has no SxxExx token (bare 'Folge 01' format)", () => {
const folders = [
"Kreuzfahrt.ins.Glueck.01.Hochzeitsreise.nach.Burma.2007.German.720p.HDTV.x264-BET",
"kig.hdtv.7p-001",
"Kreuzfahrt ins Glück S01"
];
const decision = decideAutoRenameBaseName(folders, "bet_kig_01_hdt.mkv", "bet_kig_01_hdt", folders[0], folders[2]);
expect(decision.kind).toBe("rename");
expect(decision.kind === "rename" && decision.baseName).toBe(folders[0]);
});
it("complete-folder fallback fires on CODEC alone (no resolution token — DVDRip/XviD class)", () => {
const folders = [
"Kreuzfahrt.ins.Glueck.01.Hochzeitsreise.nach.Burma.2007.German.DVDRip.x264-BET",
"Kreuzfahrt ins Glück S01"
];
const decision = decideAutoRenameBaseName(folders, "bet_kig_01.mkv", "bet_kig_01", folders[0], folders[1]);
expect(decision.kind).toBe("rename");
expect(decision.kind === "rename" && decision.baseName).toBe(folders[0]);
});
it("complete-folder fallback does NOT fire when the source HAS an episode token (generic pack stays no-target)", () => {
const decision = decideAutoRenameBaseName(
["Mega-Direct-Pack"],
"Direct.Show.S01E01.German.1080p.WEB.x264-DIRECT.mkv",
"Direct.Show.S01E01.German.1080p.WEB.x264-DIRECT",
"Mega-Direct-Pack",
"Mega-Direct-Pack"
);
expect(decision.kind).toBe("skip");
});
});
describe("hasMeaningfulSeriesPrefix", () => {
it("recognizes a real series name before the season token", () => {
expect(hasMeaningfulSeriesPrefix("Desperate.Housewives.S01.Synced.DL.720p.WEB-DL.AC3.h264")).toBe(true);
expect(hasMeaningfulSeriesPrefix("Die.Thundermans.S02E06.Tickets.und.Shreddy.GERMAN.WS.720p.HDTV.x264-aWake")).toBe(true);
expect(hasMeaningfulSeriesPrefix("Mistresses.2013.S02.GERMAN.DL.720p.WEB.x264-TSCC")).toBe(true);
expect(hasMeaningfulSeriesPrefix("show.name.s01e01.720p")).toBe(true);
});
it("rejects generic season-label folders without a series name", () => {
expect(hasMeaningfulSeriesPrefix("S01 Complete")).toBe(false);
expect(hasMeaningfulSeriesPrefix("S02")).toBe(false);
expect(hasMeaningfulSeriesPrefix("S01E01 Complete")).toBe(false);
expect(hasMeaningfulSeriesPrefix(".S01.bla")).toBe(false);
});
it("returns false when there is no season token at all", () => {
expect(hasMeaningfulSeriesPrefix("Some Random Folder")).toBe(false);
expect(hasMeaningfulSeriesPrefix("")).toBe(false);
});
});
describe("looksLikeObfuscatedSceneFileName", () => {
it("flags hoster-obfuscated names with no scene markers as obfuscated", () => {
expect(looksLikeObfuscatedSceneFileName("awa-diethundermans02e16hd.mkv")).toBe(true);
expect(looksLikeObfuscatedSceneFileName("scn-dthund7-S02E06.mkv")).toBe(true);
expect(looksLikeObfuscatedSceneFileName("4sj-blue-bloods-s08e21-720p.mkv")).toBe(true);
});
it("treats clean scene releases with multiple markers as NOT obfuscated", () => {
expect(looksLikeObfuscatedSceneFileName("the.royals.2015.s01e09.german.dl.720p.bluray.x264-j4f.mkv")).toBe(false);
expect(looksLikeObfuscatedSceneFileName("Die.Thundermans.S02E06.Tickets.und.Shreddy.GERMAN.WS.720p.HDTV.x264-aWake.mkv")).toBe(false);
expect(looksLikeObfuscatedSceneFileName("Desperate.Housewives.S01E01.German.Synced.DL.720p.WEB-DL.AC3.h264.mkv")).toBe(false);
});
it("handles edge cases (empty, very short)", () => {
expect(looksLikeObfuscatedSceneFileName("")).toBe(true);
expect(looksLikeObfuscatedSceneFileName("a.mkv")).toBe(true);
});
it("treats long dotted names as scene-style even with few markers", () => {
expect(looksLikeObfuscatedSceneFileName("Some.Show.With.Many.Tokens.S01E01.mkv")).toBe(false);
});
});
describe("extractEpisodeToken (extended formats)", () => {
it("recognizes the older xX format (capped at 2 episode digits)", () => {
expect(extractEpisodeToken("show.1x01.720p.mkv")).toBe("S01E01");
expect(extractEpisodeToken("show-2x05-hdtv.mkv")).toBe("S02E05");
expect(extractEpisodeToken("Show.Name.10x99.mkv")).toBe("S10E99");
expect(extractEpisodeToken("Show.Name.10x100.mkv")).toBeNull();
expect(extractEpisodeToken("Show.Name.S10E100.mkv")).toBe("S10E100");
});
it("does not falsely match resolution tokens like 1080x720", () => {
expect(extractEpisodeToken("show.1080p.mkv")).toBeNull();
expect(extractEpisodeToken("show.S01E01.1080p.mkv")).toBe("S01E01");
});
it("does not falsely match codec tokens like x264 / x265 (caps episode digits)", () => {
expect(extractEpisodeToken("Movie.x264-GROUP.mkv")).toBeNull();
expect(extractEpisodeToken("Movie.5x265.x265.mkv")).toBeNull();
expect(extractEpisodeToken("Show.S01E01.x265-GROUP.mkv")).toBe("S01E01");
});
it("does not falsely match common aspect ratios like 1920x1080", () => {
expect(extractEpisodeToken("Movie.1920x1080.mkv")).toBeNull();
});
});
describe("extractEpisodeToken", () => {
it("extracts S01E01 from standard scene format", () => {
expect(extractEpisodeToken("show.name.s01e01.720p")).toBe("S01E01");
});
it("extracts episode with dot separators", () => {
expect(extractEpisodeToken("Show.S02E15.1080p")).toBe("S02E15");
});
it("extracts episode with dash separators", () => {
expect(extractEpisodeToken("show-s3e5-720p")).toBe("S03E05");
});
it("extracts episode with underscore separators", () => {
expect(extractEpisodeToken("show_s10e100_hdtv")).toBe("S10E100");
});
it("extracts episode with space separators", () => {
expect(extractEpisodeToken("Show Name s1e2 720p")).toBe("S01E02");
});
it("pads single-digit season and episode to 2 digits", () => {
expect(extractEpisodeToken("show.s1e3.720p")).toBe("S01E03");
});
it("handles 3-digit episode numbers", () => {
expect(extractEpisodeToken("show.s01e123")).toBe("S01E123");
});
it("returns null for no episode token", () => {
expect(extractEpisodeToken("some.random.file.720p")).toBeNull();
});
it("returns null for season-only pattern (no episode)", () => {
expect(extractEpisodeToken("show.s01.720p")).toBeNull();
});
it("returns null for empty string", () => {
expect(extractEpisodeToken("")).toBeNull();
});
it("is case-insensitive", () => {
expect(extractEpisodeToken("Show.S05E10.1080p")).toBe("S05E10");
expect(extractEpisodeToken("show.s05e10.1080p")).toBe("S05E10");
});
it("extracts from episode token at start of string", () => {
expect(extractEpisodeToken("s01e01.720p")).toBe("S01E01");
});
it("extracts from episode token at end of string", () => {
expect(extractEpisodeToken("show.s02e03")).toBe("S02E03");
});
it("extracts double episode token s01e01e02", () => {
expect(extractEpisodeToken("tvr-mammon-s01e01e02-720p")).toBe("S01E01E02");
});
it("extracts double episode with dot separators", () => {
expect(extractEpisodeToken("Show.S01E03E04.720p")).toBe("S01E03E04");
});
it("extracts double episode at end of string", () => {
expect(extractEpisodeToken("show.s02e05e06")).toBe("S02E05E06");
});
it("extracts double episode with single-digit numbers", () => {
expect(extractEpisodeToken("show-s1e1e2-720p")).toBe("S01E01E02");
});
it("extracts episode when title and season token are joined", () => {
expect(extractEpisodeToken("mdgp-carters02e01-720p")).toBe("S02E01");
});
});
describe("applyEpisodeTokenToFolderName", () => {
it("replaces existing episode token in folder name", () => {
expect(applyEpisodeTokenToFolderName("Show.S01E01.720p-4sf", "S02E05")).toBe("Show.S02E05.720p-4sf");
});
it("replaces season-only token when no episode in folder", () => {
expect(applyEpisodeTokenToFolderName("Show.S01.720p-4sf", "S01E03")).toBe("Show.S01E03.720p-4sf");
});
it("inserts before -4sf suffix when no season/episode in folder", () => {
expect(applyEpisodeTokenToFolderName("Show.720p-4sf", "S01E05")).toBe("Show.720p.S01E05-4sf");
});
it("inserts before -4sj suffix", () => {
expect(applyEpisodeTokenToFolderName("Show.720p-4sj", "S01E05")).toBe("Show.720p.S01E05-4sj");
});
it("appends episode token when no recognized pattern", () => {
expect(applyEpisodeTokenToFolderName("SomeFolder", "S01E01")).toBe("SomeFolder.S01E01");
});
it("returns episode token when folder name is empty", () => {
expect(applyEpisodeTokenToFolderName("", "S01E01")).toBe("S01E01");
});
it("handles folder with existing multi-digit episode", () => {
expect(applyEpisodeTokenToFolderName("Show.S01E99.720p-4sf", "S01E05")).toBe("Show.S01E05.720p-4sf");
});
it("is case-insensitive for -4SF/-4SJ suffix", () => {
expect(applyEpisodeTokenToFolderName("Show.720p-4SF", "S01E01")).toBe("Show.720p.S01E01-4SF");
});
it("applies double episode token to season-only folder", () => {
expect(applyEpisodeTokenToFolderName("Mammon.S01.German.1080P.Bluray.x264-SMAHD", "S01E01E02"))
.toBe("Mammon.S01E01E02.German.1080P.Bluray.x264-SMAHD");
});
it("replaces existing double episode in folder with new token", () => {
expect(applyEpisodeTokenToFolderName("Show.S01E01E02.720p-4sf", "S01E03E04"))
.toBe("Show.S01E03E04.720p-4sf");
});
it("replaces existing single episode in folder with double episode token", () => {
expect(applyEpisodeTokenToFolderName("Show.S01E01.720p-4sf", "S01E01E02"))
.toBe("Show.S01E01E02.720p-4sf");
});
});
describe("sourceHasRpToken", () => {
it("detects .rp. in filename", () => {
expect(sourceHasRpToken("show.s01e01.rp.720p")).toBe(true);
});
it("detects -rp- in filename", () => {
expect(sourceHasRpToken("show-s01e01-rp-720p")).toBe(true);
});
it("detects _rp_ in filename", () => {
expect(sourceHasRpToken("show_s01e01_rp_720p")).toBe(true);
});
it("detects rp at end of string", () => {
expect(sourceHasRpToken("show.s01e01.rp")).toBe(true);
});
it("does not match rp inside a word", () => {
expect(sourceHasRpToken("enterprise.s01e01")).toBe(false);
});
it("returns false for empty string", () => {
expect(sourceHasRpToken("")).toBe(false);
});
it("is case-insensitive", () => {
expect(sourceHasRpToken("show.RP.720p")).toBe(true);
});
});
describe("ensureRepackToken", () => {
it("inserts REPACK before quality token", () => {
expect(ensureRepackToken("Show.S01E01.1080p-4sf")).toBe("Show.S01E01.REPACK.1080p-4sf");
});
it("inserts REPACK before 720p", () => {
expect(ensureRepackToken("Show.S01E01.720p-4sf")).toBe("Show.S01E01.REPACK.720p-4sf");
});
it("inserts REPACK before 2160p", () => {
expect(ensureRepackToken("Show.S01E01.2160p-4sf")).toBe("Show.S01E01.REPACK.2160p-4sf");
});
it("inserts REPACK before -4sf when no quality token", () => {
expect(ensureRepackToken("Show.S01E01-4sf")).toBe("Show.S01E01.REPACK-4sf");
});
it("inserts REPACK before -4sj when no quality token", () => {
expect(ensureRepackToken("Show.S01E01-4sj")).toBe("Show.S01E01.REPACK-4sj");
});
it("appends REPACK when no recognized insertion point", () => {
expect(ensureRepackToken("Show.S01E01")).toBe("Show.S01E01.REPACK");
});
it("does not double-add REPACK if already present", () => {
expect(ensureRepackToken("Show.S01E01.REPACK.1080p-4sf")).toBe("Show.S01E01.REPACK.1080p-4sf");
});
it("does not double-add repack (case-insensitive)", () => {
expect(ensureRepackToken("Show.s01e01.repack.720p-4sf")).toBe("Show.s01e01.repack.720p-4sf");
});
});
describe("buildAutoRenameBaseName", () => {
it("renames with episode token from source file", () => {
const result = buildAutoRenameBaseName("Show.S01.720p-4sf", "show.s01e05.720p.mkv");
expect(result).toBe("Show.S01E05.720p-4sf");
});
it("works with -4sj suffix", () => {
const result = buildAutoRenameBaseName("Show.S01.720p-4sj", "show.s01e03.720p.mkv");
expect(result).toBe("Show.S01E03.720p-4sj");
});
it("renames generic scene folder with group suffix", () => {
const result = buildAutoRenameBaseName("Show.S01.720p-GROUP", "show.s01e05.720p.mkv");
expect(result).toBe("Show.S01.720p-GROUP");
});
it("returns null when source has no episode token", () => {
const result = buildAutoRenameBaseName("Show.S01.720p-4sf", "random.file.720p.mkv");
expect(result).toBeNull();
});
it("adds REPACK when source has rp token", () => {
const result = buildAutoRenameBaseName("Show.S01.720p-4sf", "show.s01e05.rp.720p.mkv");
expect(result).toBe("Show.S01E05.REPACK.720p-4sf");
});
it("handles folder with existing episode that gets replaced", () => {
const result = buildAutoRenameBaseName("Show.S01E01.720p-4sf", "show.s01e10.720p.mkv");
expect(result).toBe("Show.S01E10.720p-4sf");
});
it("inserts episode before -4sf when folder has no season/episode", () => {
const result = buildAutoRenameBaseName("Show.720p-4sf", "show.s01e05.720p.mkv");
expect(result).toBe("Show.720p.S01E05-4sf");
});
it("handles case-insensitive 4SF suffix", () => {
const result = buildAutoRenameBaseName("Show.S01.720p-4SF", "show.s01e02.720p.mkv");
expect(result).toBe("Show.S01E02.720p-4SF");
});
it("handles rp + no quality token in folder", () => {
const result = buildAutoRenameBaseName("Show.S01-4sf", "show.s01e05.rp.mkv");
expect(result).toBe("Show.S01E05.REPACK-4sf");
});
it("returns null for empty folder name", () => {
const result = buildAutoRenameBaseName("", "show.s01e01.mkv");
expect(result).toBeNull();
});
it("returns null for empty source file name", () => {
const result = buildAutoRenameBaseName("Show.S01-4sf", "");
expect(result).toBeNull();
});
it("handles 2160p quality token", () => {
const result = buildAutoRenameBaseName("Show.S01.2160p-4sf", "show.s01e01.rp.2160p.mkv");
expect(result).toBe("Show.S01E01.REPACK.2160p-4sf");
});
it("handles 480p quality token", () => {
const result = buildAutoRenameBaseName("Show.S01.480p-4sf", "show.s01e07.480p.mkv");
expect(result).toBe("Show.S01E07.480p-4sf");
});
it("does not trigger on folders ending with similar but wrong suffix", () => {
expect(buildAutoRenameBaseName("Show.S01-4sfx", "show.s01e01.mkv")).toBeNull();
expect(buildAutoRenameBaseName("Show.S01-x4sf", "show.s01e01.mkv")).toBeNull();
});
it("handles high season and episode numbers", () => {
const result = buildAutoRenameBaseName("Show.S99.720p-4sf", "show.s99e999.720p.mkv");
expect(result).not.toBeNull();
expect(result!).toContain("S99E999");
});
it("real-world: German series with dots", () => {
const result = buildAutoRenameBaseName(
"Der.Bergdoktor.S18.German.720p.WEB.x264-4SJ",
"der.bergdoktor.s18e01.german.720p.web.x264"
);
expect(result).toBe("Der.Bergdoktor.S18E01.German.720p.WEB.x264-4SJ");
});
it("real-world: English series with rp token", () => {
const result = buildAutoRenameBaseName(
"The.Last.of.Us.S02.1080p.WEB-4SF",
"the.last.of.us.s02e03.rp.1080p.web"
);
expect(result).toBe("The.Last.of.Us.S02E03.REPACK.1080p.WEB-4SF");
});
it("real-world: multiple dots in name", () => {
const result = buildAutoRenameBaseName(
"Grey.s.Anatomy.S21.German.DL.720p.WEB.x264-4SJ",
"grey.s.anatomy.s21e08.german.dl.720p.web.x264"
);
expect(result).toBe("Grey.s.Anatomy.S21E08.German.DL.720p.WEB.x264-4SJ");
});
it("real-world: 4K content", () => {
const result = buildAutoRenameBaseName(
"Severance.S02.2160p.ATVP.WEB-DL.DDP5.1.DV.H.265-4SF",
"severance.s02e07.2160p.atvp.web-dl.ddp5.1.dv.h.265"
);
expect(result).toBe("Severance.S02E07.2160p.ATVP.WEB-DL.DDP5.1.DV.H.265-4SF");
});
it("real-world: Britannia release keeps folder base name", () => {
const result = buildAutoRenameBaseName(
"Britannia.S02.GERMAN.720p.WEBRiP.x264-LAW",
"law-britannia.s02e01.720p.webrip"
);
expect(result).toBe("Britannia.S02.GERMAN.720p.WEBRiP.x264-LAW");
});
it("real-world: Britannia repack injects REPACK", () => {
const result = buildAutoRenameBaseName(
"Britannia.S02.GERMAN.720p.WEBRiP.x264-LAW",
"law-britannia.s02e09.720p.webrip.repack"
);
expect(result).toBe("Britannia.S02.GERMAN.REPACK.720p.WEBRiP.x264-LAW");
});
it("adds REPACK when folder name carries RP hint", () => {
const result = buildAutoRenameBaseName(
"Banshee.S02E01.German.RP.720p.BluRay.x264-RIPLEY",
"r-banshee.s02e01-720p"
);
expect(result).toBe("Banshee.S02E01.German.REPACK.720p.BluRay.x264-RIPLEY");
});
it("real-world: folder already has wrong episode", () => {
const result = buildAutoRenameBaseName(
"Cobra.Kai.S06E01.720p.NF.WEB-DL.DDP5.1.x264-4SF",
"cobra.kai.s06e14.720p.nf.web-dl.ddp5.1.x264"
);
expect(result).toBe("Cobra.Kai.S06E14.720p.NF.WEB-DL.DDP5.1.x264-4SF");
});
it("source filename extension is not included in episode detection", () => {
const result = buildAutoRenameBaseName("Show.S01-4sf", "show.s01e01.mkv");
expect(result).not.toBeNull();
expect(result!).toContain("S01E01");
});
it("does not match episode-like patterns in codec strings", () => {
const token = extractEpisodeToken("show.s01e01.h.265");
expect(token).toBe("S01E01");
});
it("handles folder with dash separators throughout", () => {
const result = buildAutoRenameBaseName(
"Show-Name-S01-720p-4sf",
"show-name-s01e05-720p"
);
expect(result).toBe("Show-Name-S01E05-720p-4sf");
});
it("does not duplicate episode when folder already has the same episode", () => {
const result = buildAutoRenameBaseName(
"Show.S01E05.720p-4sf",
"show.s01e05.720p"
);
expect(result).toBe("Show.S01E05.720p-4sf");
});
it("handles folder with only -4sf suffix (edge case)", () => {
const result = buildAutoRenameBaseName("-4sf", "show.s01e01.mkv");
expect(result).not.toBeNull();
expect(result!).toContain("S01E01");
expect(result!).toContain("-4sf");
expect(result!).not.toContain(".S01E01.S01E01");
});
it("sanitizes special characters from result", () => {
const result = buildAutoRenameBaseName("Show:Name.S01-4sf", "show.s01e01.mkv");
expect(result).not.toBeNull();
expect(result!).not.toContain(":");
});
});
describe("buildAutoRenameBaseNameFromFolders", () => {
it("uses parent folder when current folder is not a scene template", () => {
const result = buildAutoRenameBaseNameFromFolders(
[
"Episode 01",
"Banshee.S02.German.720p.BluRay.x264-RIPLEY"
],
"r-banshee.s02e01-720p"
);
expect(result).toBe("Banshee.S02.German.720p.BluRay.x264-RIPLEY");
});
it("uses nested scene subfolder directly", () => {
const result = buildAutoRenameBaseNameFromFolders(
[
"Banshee.S02E01.German.720p.BluRay.x264-RIPLEY",
"Banshee.S02.German.720p.BluRay.x264-RIPLEY"
],
"r-banshee.s02e01-720p"
);
expect(result).toBe("Banshee.S02E01.German.720p.BluRay.x264-RIPLEY");
});
it("injects REPACK when parent folder carries repack hint", () => {
const result = buildAutoRenameBaseNameFromFolders(
[
"Banshee.S02E01.German.720p.BluRay.x264-RIPLEY",
"Banshee.S02.German.RP.720p.BluRay.x264-RIPLEY"
],
"r-banshee.s02e01-720p"
);
expect(result).toBe("Banshee.S02E01.German.REPACK.720p.BluRay.x264-RIPLEY");
});
it("uses nested Arrow episode folder with title", () => {
const result = buildAutoRenameBaseNameFromFolders(
[
"Arrow.S04E01.Green.Arrow.German.DL.720p.BluRay.x264-RSG",
"Arrow.S04.German.DL.720p.BluRay.x264-RSG"
],
"rsg-arrow-s04e01-720p"
);
expect(result).toBe("Arrow.S04E01.Green.Arrow.German.DL.720p.BluRay.x264-RSG");
});
it("adds REPACK for Arrow when source contains rp token", () => {
const result = buildAutoRenameBaseNameFromFolders(
[
"Arrow.S04E01.Green.Arrow.German.DL.720p.BluRay.x264-RSG",
"Arrow.S04.German.DL.720p.BluRay.x264-RSG"
],
"rsg-arrow-s04e01.rp.720p"
);
expect(result).toBe("Arrow.S04E01.Green.Arrow.German.DL.REPACK.720p.BluRay.x264-RSG");
});
it("converts Teil token to episode using parent season", () => {
const result = buildAutoRenameBaseNameFromFolders(
[
"Last.Impact.Der.Einschlag.Teil1.GERMAN.DL.720p.WEB.H264-SunDry",
"Last.Impact.Der.Einschlag.S01.GERMAN.DL.720p.WEB.H264-SunDry"
],
"sundry-last.impact.der.einschlag.teil1.720p.web.h264"
);
expect(result).toBe("Last.Impact.Der.Einschlag.S01E01.GERMAN.DL.720p.WEB.H264-SunDry");
});
it("converts Teil token to episode with REPACK", () => {
const result = buildAutoRenameBaseNameFromFolders(
[
"Last.Impact.Der.Einschlag.Teil1.GERMAN.DL.720p.WEB.H264-SunDry",
"Last.Impact.Der.Einschlag.S01.GERMAN.DL.720p.WEB.H264-SunDry"
],
"sundry-last.impact.der.einschlag.teil1.rp.720p.web.h264"
);
expect(result).toBe("Last.Impact.Der.Einschlag.S01E01.GERMAN.DL.REPACK.720p.WEB.H264-SunDry");
});
it("forces episode insertion for flat season folder when many files share directory", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Arrow.S08.GERMAN.DUBBED.DL.720p.BluRay.x264-TMSF"
],
"tmsf-arrow-s08e03-720p",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Arrow.S08E03.GERMAN.DUBBED.DL.720p.BluRay.x264-TMSF");
});
it("forces episode insertion plus REPACK for flat season folder", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Gotham.S05.GERMAN.DUBBED.720p.BLURAY.x264-ZZGtv"
],
"zzgtv-gotham-s05e02.rp",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Gotham.S05E02.GERMAN.DUBBED.REPACK.720p.BLURAY.x264-ZZGtv");
});
it("uses nested episode title folder for Gotham TvR style", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Gotham.S04E01.Pax.Penguina.GERMAN.5.1.DL.AC3.720p.BDRiP.x264-TvR",
"Gotham.S04.GERMAN.5.1.DL.AC3.720p.BDRiP.x264-TvR"
],
"tvr-gotham-s04e01-720p",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Gotham.S04E01.Pax.Penguina.GERMAN.5.1.DL.AC3.720p.BDRiP.x264-TvR");
});
it("uses nested title folder for Britannia TV4A style", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Britannia.S01E01.Die.Landung.German.DL.720p.BluRay.x264-TV4A",
"Britannia.S01.German.DL.720p.BluRay.x264-TV4A"
],
"tv4a-britannia.s01e01-720p",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Britannia.S01E01.Die.Landung.German.DL.720p.BluRay.x264-TV4A");
});
it("handles odd source token style 101 by using nested Agent X folder", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Agent.X.S01E01.Pilot.German.DD51.Dubbed.DL.720p.iTunesHD.x264-TVS",
"Agent.X.S01.German.DD51.Dubbed.DL.720p.iTunesHD.x264-TVS"
],
"tvs-agent-x-dd51-ded-dl-7p-ithd-x264-101",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Agent.X.S01E01.Pilot.German.DD51.Dubbed.DL.720p.iTunesHD.x264-TVS");
});
it("maps compact code 301 to S03E01 for nested Legion folder", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Legion.S03E01.Kapitel.20.German.DD51.Dubbed.DL.720p.AmazonHD.AVC-TVS",
"Legion.S03.German.DD51.Dubbed.DL.720p.AmazonHD.AVC-TVS"
],
"tvs-legion-dd51-ded-dl-7p-azhd-avc-301",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Legion.S03E01.Kapitel.20.German.DD51.Dubbed.DL.720p.AmazonHD.AVC-TVS");
});
it("maps compact code 211 in flat season folder", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Lethal.Weapon.S02.German.DD51.Dubbed.DL.720p.AmazonHD.x264-TVS"
],
"tvs-lethal-weapon-dd51-ded-dl-7p-azhd-x264-211",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Lethal.Weapon.S02E11.German.DD51.Dubbed.DL.720p.AmazonHD.x264-TVS");
});
it("maps compact code 319a to episode 19 in season 3 folder", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Die.Bergpolizei.-.Ganz.nah.am.Himmel.S03.GERMAN.AC3.720p.HDTV.x264-hrs"
],
"hrs-bpol.hdtv.7p-319a",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Die.Bergpolizei.-.Ganz.nah.am.Himmel.S03E19.GERMAN.AC3.720p.HDTV.x264-hrs");
});
it("maps compact code 319b to next episode in season 3 folder", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Die.Bergpolizei.-.Ganz.nah.am.Himmel.S03.GERMAN.AC3.720p.HDTV.x264-hrs"
],
"hrs-bpol.hdtv.7p-319b",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Die.Bergpolizei.-.Ganz.nah.am.Himmel.S03E20.GERMAN.AC3.720p.HDTV.x264-hrs");
});
it("maps episode-only token e01 via season folder hint and keeps REPACK", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Cheat.der.Betrug.S01.GERMAN.720p.WEB.h264-TMSF"
],
"tmsf-cheatderbetrug-e01-720p-repack",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Cheat.der.Betrug.S01E01.GERMAN.REPACK.720p.WEB.h264-TMSF");
});
it("maps episode-only token e02 via season folder hint", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Cheat.der.Betrug.S01.GERMAN.720p.WEB.h264-TMSF"
],
"tmsf-cheatderbetrug-e02-720p",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Cheat.der.Betrug.S01E02.GERMAN.720p.WEB.h264-TMSF");
});
it("keeps renaming for odd source order like 4sf-bs-720p-s01e05", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Cheat.der.Betrug.S01.GERMAN.720p.WEB.h264-TMSF"
],
"4sf-bs-720p-s01e05",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Cheat.der.Betrug.S01E05.GERMAN.720p.WEB.h264-TMSF");
});
it("accepts lowercase scene group suffixes", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Cheat.der.Betrug.S01.GERMAN.720p.WEB.h264-tmsf"
],
"tmsf-cheatderbetrug-e01-720p",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Cheat.der.Betrug.S01E01.GERMAN.720p.WEB.h264-tmsf");
});
it("renames double episode file into season folder (Mammon style)", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Mammon.S01.German.1080P.Bluray.x264-SMAHD"
],
"tvr-mammon-s01e01e02-720p",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Mammon.S01E01E02.German.1080P.Bluray.x264-SMAHD");
});
it("renames second double episode file correctly", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Mammon.S01.German.1080P.Bluray.x264-SMAHD"
],
"tvr-mammon-s01e03e04-720p",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Mammon.S01E03E04.German.1080P.Bluray.x264-SMAHD");
});
it("renames third double episode file correctly", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Mammon.S01.German.1080P.Bluray.x264-SMAHD"
],
"tvr-mammon-s01e05e06-720p",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Mammon.S01E05E06.German.1080P.Bluray.x264-SMAHD");
});
it("renames when folder has season but no scene group suffix (Mystery Road case)", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
["Mystery Road S02"],
"myst.road.de.dl.hdtv.7p-s02e05",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Mystery Road S02E05");
});
it("renames with season-only folder and custom name without dots", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
["Meine Serie S03"],
"meine-serie-s03e10-720p",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Meine Serie S03E10");
});
it("prefers scene-group folder over season-only fallback", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"Mystery Road S02",
"Mystery.Road.S02.GERMAN.DL.AC3.720p.HDTV.x264-hrs"
],
"myst.road.de.dl.hdtv.7p-s02e05",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Mystery.Road.S02E05.GERMAN.DL.AC3.720p.HDTV.x264-hrs");
});
it("does not use season-only fallback when forceEpisodeForSeasonFolder is false", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
["Mystery Road S02"],
"myst.road.de.dl.hdtv.7p-s02e05",
{ forceEpisodeForSeasonFolder: false }
);
expect(result).toBeNull();
});
it("renames Riviera S02 with single-digit episode s02e2", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
["Riviera.S02.GERMAN.DUBBED.DL.720p.WebHD.x264-TVP"],
"tvp-riviera-s02e2-720p",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Riviera.S02E02.GERMAN.DUBBED.DL.720p.WebHD.x264-TVP");
});
it("renames Room 104 abbreviated source r104.de.dl.web.7p-s04e02", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
["Room.104.S04.GERMAN.DL.720p.WEBRiP.x264-LAW"],
"r104.de.dl.web.7p-s04e02",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Room.104.S04E02.GERMAN.DL.720p.WEBRiP.x264-LAW");
});
it("renames Room 104 wayne source with episode", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
["Room.104.S04.GERMAN.DL.720p.WEBRiP.x264-LAW"],
"room.104.s04e01.german.dl.720p.web.h264-wayne",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Room.104.S04E01.GERMAN.DL.720p.WEBRiP.x264-LAW");
});
it("renames Carter when source joins title and season token", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
["Carter.S02.GERMAN.DL.720p.HDTV.x264-MDGP"],
"mdgp-carters02e01-720p",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Carter.S02E01.GERMAN.DL.720p.HDTV.x264-MDGP");
});
it("renames abbreviated source bupr.de.dl.web.7p-s01e03 via season folder", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
["Burning.Promise.S01.GERMAN.DL.720p.WEB.H264-WvF"],
"bupr.de.dl.web.7p-s01e03",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("Burning.Promise.S01E03.GERMAN.DL.720p.WEB.H264-WvF");
});
it("renames abbreviated 4SF source amilllt.de.dl.web.7p-s03e10 via season folder", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
["A.Million.Little.Things.S03.GERMAN.DL.720p.WEB.H264-4SF"],
"4sf-amilllt.de.dl.web.7p-s03e10",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("A.Million.Little.Things.S03E10.GERMAN.DL.720p.WEB.H264-4SF");
});
it("renames abbreviated source jkl.web.7p-s01e13 via season folder", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
["9JKL.S01.GERMAN.720p.WEB.x264-WvF"],
"jkl.web.7p-s01e13",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("9JKL.S01E13.GERMAN.720p.WEB.x264-WvF");
});
it("renames abbreviated source jkl.web.7p-s01e14 via season folder", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
["9JKL.S01.GERMAN.720p.WEB.x264-WvF"],
"jkl.web.7p-s01e14",
{ forceEpisodeForSeasonFolder: true }
);
expect(result).toBe("9JKL.S01E14.GERMAN.720p.WEB.x264-WvF");
});
it("documents malformed package name (S01GERMAN) limitation", () => {
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
[
"3MH.web.7p-101",
"Drei.Meter.ueber.dem.Himmel.S01GERMAN.DL.720P.WEB.X264-WAYNE"
],
"Drei.Meter.ueber.dem.Himmel.S01E01.GERMAN.DL.720P.WEB.X264-WAYNE",
{ forceEpisodeForSeasonFolder: true }
);
if (result !== null) {
expect(typeof result).toBe("string");
}
});
});
describe("isBonusContent (numbered episodes are never bonus)", () => {
const pkgDir = "/pkg/Show.S04.GERMAN.DL.720p.WEB.x264-GRP";
it("does NOT treat a numbered episode as bonus even when its TITLE is a bonus word", () => {
const name = "Revenge.2011.S04E19.Interview.GERMAN.DL.720p.WEB.x264-TSCC";
const fp = `${pkgDir}/${name}/${name}.mkv`;
expect(isBonusContent(fp, pkgDir, name)).toBe(false);
});
it("covers further bonus-word episode titles with a token", () => {
for (const title of ["Special", "Featurette", "Outtakes", "Bloopers", "Making.Of"]) {
const name = `Show.S04E07.${title}.GERMAN.720p.WEB.x264-GRP`;
expect(isBonusContent(`${pkgDir}/${name}.mkv`, pkgDir, name)).toBe(false);
}
});
it("STILL treats genuine extras WITHOUT an episode token as bonus", () => {
for (const name of [
"Show.Making.Of.GERMAN.720p.WEB.x264-GRP",
"Show.Behind.The.Scenes.GERMAN-GRP",
"Some.Interview.With.Cast"
]) {
expect(isBonusContent(`${pkgDir}/${name}.mkv`, pkgDir, name)).toBe(true);
}
});
it("a token-bearing file inside an Extras subfolder is still kept (numbered episode wins)", () => {
const name = "Show.S04E19.Interview.GROUP";
const fp = `${pkgDir}/Extras/${name}/${name}.mkv`;
expect(isBonusContent(fp, pkgDir, name)).toBe(false);
});
it("a token-less file inside an Extras subfolder is bonus", () => {
const fp = `${pkgDir}/Extras/Making.Of.mkv`;
expect(isBonusContent(fp, pkgDir, "Making.Of")).toBe(true);
});
});
describe("complete episode folder WITHOUT group suffix (codec/resolution only)", () => {
const hash = "c284d9d9072eaf3ac314d05f951dd115";
it("uses the clean folder name when it has an episode token + codec but no -GROUP (safari S04E08a)", () => {
const folder = "Fluss-Monster.S04E08a.Am.Essequibo.Teil.1.German.DOKU.SATRiP.XviD";
const decision = decideAutoRenameBaseName([folder, hash], "safari-fm-s04e08a.avi", "safari-fm-s04e08a", hash, hash);
expect(decision).toEqual({ kind: "rename", baseName: folder });
});
it("keeps multi-part letters a/b distinct (Teil.1 vs Teil.2 do NOT collide)", () => {
const fa = "Fluss-Monster.S04E08a.Am.Essequibo.Teil.1.German.DOKU.SATRiP.XviD";
const fb = "Fluss-Monster.S04E08b.Am.Essequibo.Teil.2.German.DOKU.SATRiP.XviD";
const da = decideAutoRenameBaseName([fa, hash], "safari-fm-s04e08a.avi", "safari-fm-s04e08a", hash, hash);
const db = decideAutoRenameBaseName([fb, hash], "safari-fm-s04e08b.avi", "safari-fm-s04e08b", hash, hash);
expect(da).toEqual({ kind: "rename", baseName: fa });
expect(db).toEqual({ kind: "rename", baseName: fb });
expect((da as any).baseName).not.toBe((db as any).baseName);
});
it("the previously-working group-suffix folder still works (no regression)", () => {
const folder = "Fluss-Monster.S01E02.Auf.der.Suche.nach.dem.Killer-Wels.German.DOKU.SATRiP.XviD-SAFARi";
const decision = decideAutoRenameBaseName([folder, hash], "safari-fm-s01e02.avi", "safari-fm-s01e02", hash, hash);
expect(decision).toEqual({ kind: "rename", baseName: folder });
});
it("does NOT use a bare episode folder WITHOUT any codec/resolution marker (stays conservative)", () => {
const decision = decideAutoRenameBaseName(["Show.S01E01", hash], "abc-s01e01.avi", "abc-s01e01", hash, hash);
expect(decision.kind).toBe("skip");
});
it("does NOT fabricate a name from a token-LESS folder (Mega-Direct guard intact)", () => {
const decision = decideAutoRenameBaseName(["Mega-Direct-Pack", hash], "Direct.Show.S01E01.DIRECT.mkv", "Direct.Show.S01E01.DIRECT", hash, hash);
expect(decision.kind).toBe("skip");
});
});
describe("collect must not mangle an already-clean SxxExx name via an episode-title folder", () => {
const hash = "c284d9d9072eaf3ac314d05f951dd115";
const epFolder = "Steven.Spielbergs.Taken.E01.Hinter.dem.Himmel.German.720p.HDTV.x264-GTVG";
const pkgFolder = "Steven.Spielbergs.Taken.S01.German.720p.HDTV.x264-GTVG";
const cleanSource = "Steven.Spielbergs.Taken.S01E01.German.720p.HDTV.x264-GTVG";
it("keeps the clean source (skip) instead of appending the token to the episode-title folder", () => {
const decision = decideAutoRenameBaseName([epFolder, pkgFolder], cleanSource + ".mkv", cleanSource, epFolder, pkgFolder);
expect(decision.kind).toBe("skip");
expect(JSON.stringify(decision)).not.toContain("GTVG.S01E01");
});
it("still cleans a JUNK/obfuscated source via an episode-title folder (append path intact, no skip)", () => {
const epFolder = "Show.E05.Die.Sache.German.720p.HDTV.x264-GRP";
const seasonFolder = "Show.S01.German.720p.HDTV.x264-GRP";
const decision = decideAutoRenameBaseName([epFolder, seasonFolder], "scn-show7-S01E05.mkv", "scn-show7-S01E05", epFolder, seasonFolder);
expect(decision.kind).toBe("rename");
expect(extractEpisodeToken((decision as any).baseName)).toBe("S01E05");
});
it("does NOT affect a folder that already carries an SxxExx token (safari S04E08a stays a rename)", () => {
const folder = "Fluss-Monster.S04E08a.Am.Essequibo.Teil.1.German.DOKU.SATRiP.XviD";
const decision = decideAutoRenameBaseName([folder, hash], "safari-fm-s04e08a.avi", "safari-fm-s04e08a", hash, hash);
expect(decision).toEqual({ kind: "rename", baseName: folder });
});
it("keeps a clean SHORT-prefix series source (ER) instead of the crippled token append", () => {
const epFolder = "ER.E01.Tag.und.Nacht.German.720p.HDTV.x264-GROUP";
const seasonFolder = "ER.S01.German.720p.HDTV.x264-GROUP";
const cleanSource = "ER.S01E01.German.720p.HDTV.x264-GROUP";
const decision = decideAutoRenameBaseName([epFolder, seasonFolder], cleanSource + ".mkv", cleanSource, epFolder, seasonFolder);
expect(decision.kind).toBe("skip");
expect(JSON.stringify(decision)).not.toContain("GROUP.S01E01");
});
});