diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 9b715da..6922938 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -731,10 +731,10 @@ const SCENE_RP_TOKEN_RE = /(?:^|[._\-\s])rp(?:[._\-\s]|$)/i; const SCENE_REPACK_TOKEN_RE = /(?:^|[._\-\s])repack(?:[._\-\s]|$)/i; const SCENE_QUALITY_TOKEN_RE = /([._\-\s])((?:4320|2160|1440|1080|720|576|540|480|360)p)(?=[._\-\s]|$)/i; const SCENE_GROUP_SUFFIX_FALLBACK_RE = /-([A-Za-z0-9]{2,})$/; -const SCENE_NON_GROUP_SUFFIXES = new Set([ - "x264", - "x265", - "h264", +const SCENE_NON_GROUP_SUFFIXES = new Set([ + "x264", + "x265", + "h264", "h265", "avc", "hevc", @@ -745,40 +745,49 @@ const SCENE_NON_GROUP_SUFFIXES = new Set([ "bdrip", "hdtv", "dvdrip", - "remux" -]); - -function hasSceneGroupSuffix(fileName: string): boolean { - const text = String(fileName || "").trim(); - if (!text) { - return false; - } - - if (SCENE_GROUP_SUFFIX_RE.test(text)) { - return true; - } - - const fallbackMatch = text.match(SCENE_GROUP_SUFFIX_FALLBACK_RE); - const suffix = String(fallbackMatch?.[1] || "").trim(); - if (!suffix) { - return false; - } - - const lower = suffix.toLowerCase(); - if (SCENE_NON_GROUP_SUFFIXES.has(lower)) { - return false; - } - if (/^\d+p$/.test(lower) || /^\d+$/.test(lower)) { - return false; - } - if (/^\d/.test(suffix)) { - return false; - } - if (/4s(?:f|j)/i.test(suffix) && !/^(?:4sf|4sj)$/i.test(suffix)) { - return false; - } - return /[a-z]/i.test(suffix); -} + "remux" +]); + +function isValidSceneGroupSuffix(suffix: string): boolean { + const normalizedSuffix = String(suffix || "").trim(); + if (!normalizedSuffix) { + return false; + } + + const lower = normalizedSuffix.toLowerCase(); + if (SCENE_NON_GROUP_SUFFIXES.has(lower)) { + return false; + } + if (/^s\d{1,2}e\d{1,3}(?:e\d{1,3})?$/i.test(normalizedSuffix) || /^s\d{1,2}$/i.test(normalizedSuffix) || /^e\d{1,3}$/i.test(normalizedSuffix)) { + return false; + } + if (/^\d+p$/.test(lower) || /^\d+$/.test(lower)) { + return false; + } + if (/^\d/.test(normalizedSuffix)) { + return false; + } + if (/4s(?:f|j)/i.test(normalizedSuffix) && !/^(?:4sf|4sj)$/i.test(normalizedSuffix)) { + return false; + } + return /[a-z]/i.test(normalizedSuffix); +} + +function hasSceneGroupSuffix(fileName: string): boolean { + const text = String(fileName || "").trim(); + if (!text) { + return false; + } + + if (SCENE_GROUP_SUFFIX_RE.test(text)) { + const directMatch = text.match(SCENE_GROUP_SUFFIX_FALLBACK_RE); + return isValidSceneGroupSuffix(String(directMatch?.[1] || "")); + } + + const fallbackMatch = text.match(SCENE_GROUP_SUFFIX_FALLBACK_RE); + const suffix = String(fallbackMatch?.[1] || "").trim(); + return isValidSceneGroupSuffix(suffix); +} export function extractEpisodeToken(fileName: string): string | null { const text = String(fileName || ""); diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index 1f9418f..b369c3e 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -6,7 +6,7 @@ import crypto from "node:crypto"; import { EventEmitter, once } from "node:events"; import AdmZip from "adm-zip"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { DownloadManager, extractArchiveNameFromExtractorLogMessage, getAuthoritativeRealDebridTotal, resolveArchiveItemsFromList } from "../src/main/download-manager"; +import { DownloadManager, buildAutoRenameBaseNameFromFoldersWithOptions, extractArchiveNameFromExtractorLogMessage, getAuthoritativeRealDebridTotal, resolveArchiveItemsFromList } from "../src/main/download-manager"; import { planDownloadCompletion, validateDownloadedFileCompletion } from "../src/main/download-completion"; import { defaultSettings } from "../src/main/constants"; import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys"; @@ -77,8 +77,33 @@ describe("download completion planning", () => { }); }); }); - -async function waitFor(predicate: () => boolean, timeoutMs = 15000): Promise { + +describe("auto rename base selection", () => { + it("ignores raw episode-suffix folders like 4sf-amilllt...-s03e10 as scene targets", () => { + expect(buildAutoRenameBaseNameFromFoldersWithOptions( + [ + "4sf-amilllt.de.dl.web.7p-s03e10", + "A.Million.Little.Things.S03.GERMAN.DL.720p.WEB.H264-RWP", + "amilllt.de.dl.web.7p-s03e10" + ], + "A.Million.Little.Things.S03E10.Vertraue.mir.GERMAN.DL.720p.WEB.H264-4SF", + { forceEpisodeForSeasonFolder: true } + )).toBe("A.Million.Little.Things.S03E10.GERMAN.DL.720p.WEB.H264-RWP"); + }); + + it("ignores compact archive folder stems like scn-alco7-S03E18 as scene targets", () => { + expect(buildAutoRenameBaseNameFromFoldersWithOptions( + [ + "scn-alco7-S03E18", + "Alex.und.Co.S03.GERMAN.DL.720p.WEB.H264-SunDry" + ], + "alex.und.co.s03e18.720p.web.h264-sundry", + { forceEpisodeForSeasonFolder: true } + )).toBe("Alex.und.Co.S03E18.GERMAN.DL.720p.WEB.H264-SunDry"); + }); +}); + +async function waitFor(predicate: () => boolean, timeoutMs = 15000): Promise { const started = Date.now(); while (!predicate()) { if (Date.now() - started > timeoutMs) {