Auto-Rename: zwei Mismatch-Bugs gefixt (obfuskierter File-Token + weak Folder)

Bug 1 - Obfuskierter Datei-Token vs Folder:
Hoster verteilen Episoden in EPISODEN-Ordner ABER scrambeln den
Datei-Token zur Anti-Piracy:
  Folder: Die.Thundermans.S02E01.Der.Thunder.Van.GERMAN...
  File:   awa-diethundermans02e16hd.mkv  (Datei sagt E16, Folder E01)

Bisher hat der Episode-Token-Mismatch-Check uebersprungen → Datei
behaelt obfuskierten Namen → MKV-Move kopiert Garbage-Namen ins
Library-Verzeichnis.

Fix: wenn der unmittelbare Eltern-Ordner explizit denselben SxxExx-
Token wie unser computed targetBaseName traegt (also ein Per-Episode
Scene-Folder ist, NICHT der Extract-Root), wird der Folder-Token als
authoritativ behandelt — Scene-Releases benennen Episoden-Folder
deterministisch korrekt, der Datei-Name ist die Obfuskation.

Bug 2 - Weak Folder ueberschreibt perfekten Source-Filename:
Source: Desperate.Housewives.S01E01.German.Synced.DL.720p.WEB-DL.AC3.h264
Folder-Kette: ["S01 Complete", "Desperate.Housewives.S01.Synced..."]

Auto-Rename hat den unmittelbaren Parent "S01 Complete" gewaehlt und
daraus "S01E01 Complete" gebaut — kompletter Verlust des Series-Namens.

Fix: neue Helper-Funktion hasMeaningfulSeriesPrefix() prueft, ob ein
Name mindestens 3 alphabetische Zeichen VOR dem Season-Token hat.
Wenn (a) Source einen Episode-Token hat, (b) Source einen Series-Prefix
hat, (c) Computed Target KEINEN Series-Prefix hat und (d) Target weniger
als die Haelfte der Source-Laenge ist → Source behalten, Rename
ueberspringen. Renaming wuerde Information ZERSTOEREN.

Tests: 3 neue Unit-Tests fuer hasMeaningfulSeriesPrefix decken die
relevanten Faelle ab (echte Series-Namen vs generische Season-Labels
vs. Folder ohne Season-Token). 577/577 Tests gruen.
This commit is contained in:
Sucukdeluxe 2026-04-22 00:45:49 +02:00
parent 9e165b72a3
commit 19c31caab5
2 changed files with 115 additions and 14 deletions

View File

@ -851,6 +851,21 @@ const SCENE_EPISODE_JOINED_RE = /s(\d{1,2})e(\d{1,3})(?:e(\d{1,3}))?(?!\d)/i;
// Scene typo: "S05S01" instead of "S05E01" — second S should be E
const SCENE_EPISODE_TYPO_SS_RE = /(?:^|[._\-\s])s(\d{1,2})s(\d{1,3})(?!\d)/i;
const SCENE_SEASON_ONLY_RE = /(^|[._\-\s])s\d{1,2}(?=[._\-\s]|$)/i;
/** True iff the name has at least 3 alphabetic characters BEFORE the first
* SxxExx / Sxx token. Used to distinguish folders/files with a real series
* name ("Desperate.Housewives.S01...") from generic season labels
* ("S01 Complete", "Season 1"). */
export function hasMeaningfulSeriesPrefix(name: string): boolean {
const text = String(name || "");
const seasonMatch = text.match(/(?:^|[._\-\s])s\d{1,2}/i);
if (!seasonMatch || seasonMatch.index === undefined) {
return false;
}
const prefix = text.slice(0, seasonMatch.index);
const alphaChars = (prefix.match(/[A-Za-z]/g) || []).length;
return alphaChars >= 3;
}
const SCENE_SEASON_CAPTURE_RE = /(?:^|[._\-\s])s(\d{1,2})(?=[._\-\s]|$)/i;
const SCENE_EPISODE_ONLY_RE = /(?:^|[._\-\s])e(?:p(?:isode)?)?\s*0*(\d{1,3})(?:[._\-\s]|$)/i;
const SCENE_PART_TOKEN_RE = /(?:^|[._\-\s])(?:teil|part)\s*0*(\d{1,3})(?=[._\-\s]|$)/i;
@ -3683,6 +3698,39 @@ export class DownloadManager extends EventEmitter {
let targetBaseName = buildAutoRenameBaseNameFromFoldersWithOptions(folderCandidates, sourceBaseName, {
forceEpisodeForSeasonFolder: true
});
// Defense against degenerate folder layouts: when the immediate parent
// folder lacks a real series name (e.g. "S01 Complete", "Season 1",
// "Staffel 02"), buildAutoRenameBaseName can collapse a perfect source
// filename like "Desperate.Housewives.S01E01.German.Synced.DL.720p.WEB-
// DL.AC3.h264" into garbage like "S01E01 Complete". If the source is
// already well-formed (has SxxExx + a meaningful series-name prefix)
// and the computed target is much shorter / lacks that prefix, keep
// the source as-is — renaming would actively destroy information.
if (targetBaseName && sourceBaseName.length > 0) {
const sourceHasEpisode = Boolean(extractEpisodeToken(sourceBaseName));
const targetHasEpisode = Boolean(extractEpisodeToken(targetBaseName));
const sourceHasSeriesPrefix = hasMeaningfulSeriesPrefix(sourceBaseName);
const targetHasSeriesPrefix = hasMeaningfulSeriesPrefix(targetBaseName);
const targetIsMuchShorter = targetBaseName.length * 2 < sourceBaseName.length;
if (sourceHasEpisode
&& targetHasEpisode
&& sourceHasSeriesPrefix
&& !targetHasSeriesPrefix
&& targetIsMuchShorter) {
logger.info(`Auto-Rename uebersprungen: Source "${sourceBaseName}" ist bereits aussagekraeftiger als computed Target "${targetBaseName}"`);
if (pkg) {
const resolved = resolveRenameItem();
this.logRenameProcess(pkg, "INFO", "auto-rename", "Auto-Rename uebersprungen: Source schon besser als computed Target", {
sourcePath,
sourceName,
sourceBaseName,
targetBaseName,
folders: folderCandidates.join(", ")
}, resolved.item, resolved.matchedBy);
}
continue;
}
}
const resolveRenameItem = (...extra: Array<string | null | undefined>): { item: DownloadItem | null; matchedBy: string | null } => {
if (!pkg) {
return { item: null, matchedBy: null };
@ -3727,20 +3775,51 @@ export class DownloadManager extends EventEmitter {
}
}
} else if (targetEpisodeToken !== sourceEpisodeToken) {
// Target has a DIFFERENT episode token than source — that's a clear sign
// the rename is wrong (would mislabel the episode). Skip to be safe.
logger.warn(`Auto-Rename Safety: Skipping rename - Episode-Token Mismatch (source=${sourceEpisodeToken}, target=${targetEpisodeToken})`);
if (pkg) {
const resolved = resolveRenameItem();
this.logRenameProcess(pkg, "WARN", "auto-rename", "Auto-Rename uebersprungen: Episode-Token Mismatch", {
sourcePath,
sourceName,
sourceEpisodeToken,
targetEpisodeToken,
targetBaseName
}, resolved.item, resolved.matchedBy);
// Target has a DIFFERENT episode token than source. Normally that's a
// sign the rename would mislabel the episode — BUT scene releases
// often place obfuscated MKVs (e.g. "awa-diethundermans02e16hd.mkv"
// = scrambled E16) inside an explicitly named episode folder
// (e.g. "Die.Thundermans.S02E01.Der.Thunder.Van.GERMAN.x264-aWake").
// The folder is created by the release group with the REAL episode
// info; the file name is anti-piracy obfuscation. So when the
// immediate parent folder carries the same explicit SxxExx token as
// our computed targetBaseName, trust the folder and override the
// misleading source token.
const parentFolderName = path.basename(path.dirname(sourcePath));
const parentEpisodeToken = extractEpisodeToken(parentFolderName);
const folderIsAuthoritative = Boolean(
parentEpisodeToken
&& parentEpisodeToken === targetEpisodeToken
&& parentFolderName.toLowerCase() !== path.basename(extractDir).toLowerCase()
);
if (folderIsAuthoritative) {
logger.info(`Auto-Rename: source-Token ${sourceEpisodeToken} ignoriert, Folder-Token ${targetEpisodeToken} ist authoritativ (vermutlich obfuskierter Dateiname in ${parentFolderName})`);
if (pkg) {
const resolved = resolveRenameItem();
this.logRenameProcess(pkg, "INFO", "auto-rename", "Auto-Rename: Folder-Token uebersteuert obfuskierten Datei-Token", {
sourcePath,
sourceName,
sourceEpisodeToken,
targetEpisodeToken,
parentFolder: parentFolderName,
targetBaseName
}, resolved.item, resolved.matchedBy);
}
// Fall through to the normal rename path with targetBaseName.
} else {
logger.warn(`Auto-Rename Safety: Skipping rename - Episode-Token Mismatch (source=${sourceEpisodeToken}, target=${targetEpisodeToken})`);
if (pkg) {
const resolved = resolveRenameItem();
this.logRenameProcess(pkg, "WARN", "auto-rename", "Auto-Rename uebersprungen: Episode-Token Mismatch", {
sourcePath,
sourceName,
sourceEpisodeToken,
targetEpisodeToken,
targetBaseName
}, resolved.item, resolved.matchedBy);
}
continue;
}
continue;
}
}
if (!targetBaseName) {

View File

@ -6,9 +6,31 @@ import {
ensureRepackToken,
buildAutoRenameBaseName,
buildAutoRenameBaseNameFromFolders,
buildAutoRenameBaseNameFromFoldersWithOptions
buildAutoRenameBaseNameFromFoldersWithOptions,
hasMeaningfulSeriesPrefix
} from "../src/main/download-manager";
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("extractEpisodeToken", () => {
it("extracts S01E01 from standard scene format", () => {
expect(extractEpisodeToken("show.name.s01e01.720p")).toBe("S01E01");