diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 6922938..279cbd7 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -728,9 +728,11 @@ const SCENE_EPISODE_ONLY_RE = /(?:^|[._\-\s])e(?:p(?:isode)?)?\s*0*(\d{1,3})(?:[ const SCENE_PART_TOKEN_RE = /(?:^|[._\-\s])(?:teil|part)\s*0*(\d{1,3})(?=[._\-\s]|$)/i; const SCENE_COMPACT_EPISODE_CODE_RE = /(?:^|[._\-\s])(\d{3,4})([a-z])?(?=$|[._\-\s])/i; 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_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_FLEXIBLE_GROUP_SUFFIX_RE = /-([A-Za-z0-9]+(?:_[A-Za-z0-9]+)*)$/; +const SCENE_MIXED_GROUP_SUFFIX_RE = /-[^-]*[\/\\|\u2044\u2215][^-]*$/; const SCENE_NON_GROUP_SUFFIXES = new Set([ "x264", "x265", @@ -773,6 +775,49 @@ function isValidSceneGroupSuffix(suffix: string): boolean { return /[a-z]/i.test(normalizedSuffix); } +function extractFlexibleSceneGroupSuffix(fileName: string): string | null { + const text = String(fileName || "").trim(); + if (!text) { + return null; + } + + const match = text.match(SCENE_FLEXIBLE_GROUP_SUFFIX_RE); + const suffix = String(match?.[1] || "").trim(); + if (!suffix || !/[a-z]/i.test(suffix)) { + return null; + } + + const suffixParts = suffix.split("_").filter(Boolean); + if (suffixParts.length === 0) { + return null; + } + if (!suffixParts.every((part) => isValidSceneGroupSuffix(part))) { + return null; + } + return suffix; +} + +function hasMixedSceneGroupSuffix(fileName: string): boolean { + const text = String(fileName || "").trim(); + if (!text) { + return false; + } + return SCENE_MIXED_GROUP_SUFFIX_RE.test(text); +} + +function applySourceSceneGroupSuffix(targetBaseName: string, sourceFileName: string): string { + const target = String(targetBaseName || "").trim(); + const suffix = extractFlexibleSceneGroupSuffix(sourceFileName); + if (!target || !suffix) { + return target; + } + + if (/-[^-]+$/.test(target)) { + return target.replace(/-[^-]+$/, `-${suffix}`); + } + return `${target}-${suffix}`; +} + function hasSceneGroupSuffix(fileName: string): boolean { const text = String(fileName || "").trim(); if (!text) { @@ -1091,26 +1136,33 @@ export function buildAutoRenameBaseNameFromFoldersWithOptions( continue; } - if (resolvedEpisode - && forceEpisodeForSeasonFolder + if (resolvedEpisode + && forceEpisodeForSeasonFolder + && hasSceneGroupSuffix(target) + && !extractEpisodeToken(target) + && SCENE_SEASON_ONLY_RE.test(target)) { + target = applyEpisodeTokenToFolderName(target, resolvedEpisode.token); + } + + if (resolvedEpisode?.fromPart && hasSceneGroupSuffix(target) && !extractEpisodeToken(target) - && SCENE_SEASON_ONLY_RE.test(target)) { - target = applyEpisodeTokenToFolderName(target, resolvedEpisode.token); - } - - if (resolvedEpisode?.fromPart - && hasSceneGroupSuffix(target) - && !extractEpisodeToken(target) - && SCENE_SEASON_ONLY_RE.test(target)) { - target = applyEpisodeTokenToFolderName(target, resolvedEpisode.token); - } - - if (globalRepackHint) { - target = ensureRepackToken(removeRpTokens(target)); - } - return sanitizeFilename(target); - } + && SCENE_SEASON_ONLY_RE.test(target)) { + target = applyEpisodeTokenToFolderName(target, resolvedEpisode.token); + } + + if (resolvedEpisode + && folderHasSeason + && !folderHasEpisode + && (hasMixedSceneGroupSuffix(folderName) || !hasSceneGroupSuffix(folderName))) { + target = applySourceSceneGroupSuffix(target, normalizedSourceFileName); + } + + if (globalRepackHint) { + target = ensureRepackToken(removeRpTokens(target)); + } + return sanitizeFilename(target); + } // Last-resort fallback: if no scene-group-suffix folder was found but a folder // has a season token and the source has an episode token, inject the episode anyway. @@ -1120,13 +1172,16 @@ export function buildAutoRenameBaseNameFromFoldersWithOptions( for (const folderName of ordered) { if (!SCENE_SEASON_ONLY_RE.test(folderName) || extractEpisodeToken(folderName)) { continue; - } - let target = applyEpisodeTokenToFolderName(folderName, resolvedEpisode.token); - if (globalRepackHint) { - target = ensureRepackToken(removeRpTokens(target)); - } - return sanitizeFilename(target); - } + } + let target = applyEpisodeTokenToFolderName(folderName, resolvedEpisode.token); + if (hasMixedSceneGroupSuffix(folderName) || !hasSceneGroupSuffix(folderName)) { + target = applySourceSceneGroupSuffix(target, normalizedSourceFileName); + } + if (globalRepackHint) { + target = ensureRepackToken(removeRpTokens(target)); + } + return sanitizeFilename(target); + } } return null; diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index b369c3e..82bafb7 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -101,6 +101,28 @@ describe("auto rename base selection", () => { { forceEpisodeForSeasonFolder: true } )).toBe("Alex.und.Co.S03E18.GERMAN.DL.720p.WEB.H264-SunDry"); }); + + it("uses the episode's own scene group when a season folder mixes providers", () => { + expect(buildAutoRenameBaseNameFromFoldersWithOptions( + [ + "amilllt.web.de.7p-101", + "A.Million.Little.Things.S01.GERMAN.DUBBED.720p.WEB.h264-idTV\u2044GDR" + ], + "A.Million.Little.Things.S01E01.GERMAN.DUBBED.720p.WEB.h264-idTV_iNT", + { forceEpisodeForSeasonFolder: true } + )).toBe("A.Million.Little.Things.S01E01.GERMAN.DUBBED.720p.WEB.h264-idTV_iNT"); + }); + + it("keeps per-episode GDR suffixes instead of inheriting a mixed package suffix", () => { + expect(buildAutoRenameBaseNameFromFoldersWithOptions( + [ + "amilllt.web.de.7p-110", + "A.Million.Little.Things.S01.GERMAN.DUBBED.720p.WEB.h264-idTV\u2044GDR" + ], + "A.Million.Little.Things.S01E10.German.DL.Dubbed.720p.WEB.h264-GDR", + { forceEpisodeForSeasonFolder: true } + )).toBe("A.Million.Little.Things.S01E10.GERMAN.DUBBED.720p.WEB.h264-GDR"); + }); }); async function waitFor(predicate: () => boolean, timeoutMs = 15000): Promise {