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:
parent
9e165b72a3
commit
19c31caab5
@ -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
|
// 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_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;
|
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_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_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;
|
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, {
|
let targetBaseName = buildAutoRenameBaseNameFromFoldersWithOptions(folderCandidates, sourceBaseName, {
|
||||||
forceEpisodeForSeasonFolder: true
|
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 } => {
|
const resolveRenameItem = (...extra: Array<string | null | undefined>): { item: DownloadItem | null; matchedBy: string | null } => {
|
||||||
if (!pkg) {
|
if (!pkg) {
|
||||||
return { item: null, matchedBy: null };
|
return { item: null, matchedBy: null };
|
||||||
@ -3727,20 +3775,51 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (targetEpisodeToken !== sourceEpisodeToken) {
|
} else if (targetEpisodeToken !== sourceEpisodeToken) {
|
||||||
// Target has a DIFFERENT episode token than source — that's a clear sign
|
// Target has a DIFFERENT episode token than source. Normally that's a
|
||||||
// the rename is wrong (would mislabel the episode). Skip to be safe.
|
// sign the rename would mislabel the episode — BUT scene releases
|
||||||
logger.warn(`Auto-Rename Safety: Skipping rename - Episode-Token Mismatch (source=${sourceEpisodeToken}, target=${targetEpisodeToken})`);
|
// often place obfuscated MKVs (e.g. "awa-diethundermans02e16hd.mkv"
|
||||||
if (pkg) {
|
// = scrambled E16) inside an explicitly named episode folder
|
||||||
const resolved = resolveRenameItem();
|
// (e.g. "Die.Thundermans.S02E01.Der.Thunder.Van.GERMAN.x264-aWake").
|
||||||
this.logRenameProcess(pkg, "WARN", "auto-rename", "Auto-Rename uebersprungen: Episode-Token Mismatch", {
|
// The folder is created by the release group with the REAL episode
|
||||||
sourcePath,
|
// info; the file name is anti-piracy obfuscation. So when the
|
||||||
sourceName,
|
// immediate parent folder carries the same explicit SxxExx token as
|
||||||
sourceEpisodeToken,
|
// our computed targetBaseName, trust the folder and override the
|
||||||
targetEpisodeToken,
|
// misleading source token.
|
||||||
targetBaseName
|
const parentFolderName = path.basename(path.dirname(sourcePath));
|
||||||
}, resolved.item, resolved.matchedBy);
|
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) {
|
if (!targetBaseName) {
|
||||||
|
|||||||
@ -6,9 +6,31 @@ import {
|
|||||||
ensureRepackToken,
|
ensureRepackToken,
|
||||||
buildAutoRenameBaseName,
|
buildAutoRenameBaseName,
|
||||||
buildAutoRenameBaseNameFromFolders,
|
buildAutoRenameBaseNameFromFolders,
|
||||||
buildAutoRenameBaseNameFromFoldersWithOptions
|
buildAutoRenameBaseNameFromFoldersWithOptions,
|
||||||
|
hasMeaningfulSeriesPrefix
|
||||||
} from "../src/main/download-manager";
|
} 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", () => {
|
describe("extractEpisodeToken", () => {
|
||||||
it("extracts S01E01 from standard scene format", () => {
|
it("extracts S01E01 from standard scene format", () => {
|
||||||
expect(extractEpisodeToken("show.name.s01e01.720p")).toBe("S01E01");
|
expect(extractEpisodeToken("show.name.s01e01.720p")).toBe("S01E01");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user