Renaming 100%: collect leitet sauberen Namen selbst ab (gemeinsame Entscheidungsfunktion + Wurzel-Schutz)
User-Report (aus dem Desktop-Rename-Log): 17 Dateien landeten ROH in der Library
("tvarchiv...s07e12-720.mkv", "4sf-...s04e01.mkv") — Auto-Rename hatte sie verpasst, der
MKV-Collect schob sie mit dem rohen Scene-Namen weg.
Root Cause 1: Auto-Rename und collectMkvFilesToLibrary sind entkoppelte Scans. Auto-Rename
benennt nur present-and-stable Dateien in extractDir um; eine verpasste Datei (verpasster
Zyklus ODER lag in "Downloader Unfertig" ausserhalb extractDir) wurde von collect roh
weggeschoben (collect behielt blind den Basename).
Root Cause 2: decideAutoRenameBaseName fabrizierte Namen fuer token-lose generische Ordner
("Mega-Direct-Pack" -> "Mega-Direct-Pack.S01E01") wegen eines hasSceneGroupSuffix-Falsch-
Positivs auf "-Pack" — derselbe latente Bug haette Auto-Rename getroffen.
Fix:
- Namens-Entscheidung in EINE pure Funktion extrahiert: decideAutoRenameBaseName (Single
Source of Truth fuer Auto-Rename UND Collect — koennen nicht mehr divergieren).
- Wurzel-Schutz darin: Rename nur, wenn ein folderCandidate einen echten Season-/Episode-
Token traegt (kein Fabrizieren aus token-losen Ordnern). Fixt beide Pfade.
- collectMkvFilesToLibrary leitet den sauberen Namen via dieser Funktion ab (gegated auf
autoRename4sf4sj — respektiert die Umbenenn-Einstellung), inkl. Companion-Untertitel und
Dedup gegen den sauberen Namen. mkvFiles traegt jetzt sourceRoot fuer die Ordner-Herleitung.
- Auto-Rename-Loop nutzt jetzt die gemeinsame Funktion (behebt nebenbei 2 latente
use-before-declaration/TDZ-Fehler an resolveRenameItem).
- Latenter Bug: Casing-Zaehler renamedCount -> renamed (war undeklariert -> ReferenceError,
vom catch verschluckt -> Casing-Korrekturen wurden still verworfen).
Verifiziert: tsc 6 (von 9 — 3 latente Fehler nebenbei behoben), 678 Tests + 9 neue (7
Charakterisierung der Entscheidung + 2 Collect-Integration: raw->clean + Companion/.srt folgt
+ Datei ausserhalb extractDir), Build gruen. Adversarialer Review-Workflow (4 Linsen) + Advisor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9a71e01417
commit
288a0762a6
+104
-1
@@ -8,9 +8,112 @@ import {
|
||||
buildAutoRenameBaseNameFromFolders,
|
||||
buildAutoRenameBaseNameFromFoldersWithOptions,
|
||||
hasMeaningfulSeriesPrefix,
|
||||
looksLikeObfuscatedSceneFileName
|
||||
looksLikeObfuscatedSceneFileName,
|
||||
decideAutoRenameBaseName
|
||||
} from "../src/main/download-manager";
|
||||
|
||||
describe("decideAutoRenameBaseName (shared naming decision — used by auto-rename AND mkv-collect)", () => {
|
||||
// Characterization corpus: pins the EXACT decision for the real failures from
|
||||
// rename-session_2026-06-02 (17 raw files that mkv-move moved un-renamed) plus
|
||||
// the guard cases. mkv-collect now routes through this same function so a file
|
||||
// auto-rename missed still lands clean in the library.
|
||||
|
||||
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)", () => {
|
||||
// Obfuscated file (E16) inside an explicitly-named E01 folder → trust the folder.
|
||||
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)", () => {
|
||||
// Clean source S01E09 in a folder that says E08 → must NOT rename to E08.
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -9429,6 +9429,138 @@ describe("download manager", () => {
|
||||
void manager;
|
||||
}, 20000);
|
||||
|
||||
it("collect CLEANS a raw scene file that auto-rename never processed (the 17-file library bug)", async () => {
|
||||
// Echter Bug aus rename-session_2026-06-02: Auto-Rename verpasste einzelne Dateien
|
||||
// (verpasster Scan / lag ausserhalb der extractDir), der Collect schob sie dann ROH in
|
||||
// die Library ("tvarchiv...s07e12-720.mkv"). Fix: Collect leitet den sauberen Namen
|
||||
// selbst ab (gleiche Logik wie Auto-Rename) — die Library-Datei heisst garantiert sauber,
|
||||
// auch wenn KEIN Auto-Rename-Pass die Datei je angefasst hat (hier: Collect direkt
|
||||
// aufgerufen, ohne vorherigen Auto-Rename-Lauf).
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
const packageName = "Herzflimmern.Die.Klinik.am.See.S07.German.720p.Webrip.x264-TVARCHiV";
|
||||
const outputDir = path.join(root, "downloads", packageName);
|
||||
const extractDir = path.join(root, "extract", packageName);
|
||||
// Per-Episoden-Ordner (vom Release-Group sauber benannt) mit ROHER MKV darin.
|
||||
const episodeFolder = "Herzflimmern.Die.Klinik.am.See.S07E12.German.720p.Webrip.x264-TVARCHiV";
|
||||
const epDir = path.join(extractDir, episodeFolder);
|
||||
fs.mkdirSync(epDir, { recursive: true });
|
||||
const rawName = "tvarchiv.herzflimmern.die.klinik.am.see.s07e12-720.mkv";
|
||||
const rawPath = path.join(epDir, rawName);
|
||||
fs.writeFileSync(rawPath, Buffer.alloc(4096, 7));
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = `${packageName}-pkg`;
|
||||
const createdAt = Date.now() - 60_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir: path.join(root, "extract"),
|
||||
autoExtract: true,
|
||||
autoRename4sf4sj: true, // Umbenennen AN — wie in der echten User-Config
|
||||
collectMkvToLibrary: true,
|
||||
mkvLibraryDir,
|
||||
enableIntegrityCheck: false,
|
||||
cleanupMode: "none"
|
||||
},
|
||||
session,
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
// Collect DIREKT aufrufen, OHNE vorherigen Auto-Rename-Lauf — simuliert genau die
|
||||
// verpasste Datei. deferFreshFiles=false (finaler Pass).
|
||||
await (manager as any).collectMkvFilesToLibrary(packageId, session.packages[packageId], undefined, false);
|
||||
|
||||
const cleanLibPath = path.join(mkvLibraryDir, `${episodeFolder}.mkv`);
|
||||
const rawLibPath = path.join(mkvLibraryDir, rawName);
|
||||
// Library-Datei heisst SAUBER, nicht roh; Quelle ist weg.
|
||||
expect(fs.existsSync(cleanLibPath)).toBe(true);
|
||||
expect(fs.existsSync(rawLibPath)).toBe(false);
|
||||
expect(fs.existsSync(rawPath)).toBe(false);
|
||||
|
||||
void manager;
|
||||
}, 20000);
|
||||
|
||||
it("collect cleans a raw file sitting OUTSIDE extractDir (Downloader-Unfertig case) AND its .srt follows the rename", async () => {
|
||||
// Die 5 Fritzie-S04-Dateien lagen in "Downloader Unfertig" (= outputDir-Seite, NICHT
|
||||
// extractDir) — Auto-Rename scannt nur extractDir, sah sie also nie. Collect muss sie
|
||||
// trotzdem aus dem Staffel-Ordner heraus sauber benennen, und der Untertitel muss
|
||||
// mit dem Video mitwandern (auf den GEAENDERTEN Namen).
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
const packageName = "Fritzie.-.Der.Himmel.muss.warten.S04.GERMAN.720p.WEB.AVC-4SF";
|
||||
const outputDir = path.join(root, "downloads", packageName); // = "Unfertig"-Aequivalent
|
||||
const extractDir = path.join(root, "extract", packageName); // bleibt leer/fehlt
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
const rawName = "4sf-fritzie.himmel.muss.warten.web.7p-s04e01.mkv";
|
||||
const rawSrt = "4sf-fritzie.himmel.muss.warten.web.7p-s04e01.de.srt";
|
||||
fs.writeFileSync(path.join(outputDir, rawName), Buffer.alloc(4096, 4));
|
||||
fs.writeFileSync(path.join(outputDir, rawSrt), Buffer.from("1\n00:00:01,000 --> 00:00:02,000\nhi\n"));
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = `${packageName}-pkg`;
|
||||
const createdAt = Date.now() - 60_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "completed",
|
||||
itemIds: [],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir: path.join(root, "extract"),
|
||||
autoExtract: true,
|
||||
autoRename4sf4sj: true,
|
||||
collectMkvToLibrary: true,
|
||||
mkvLibraryDir,
|
||||
enableIntegrityCheck: false,
|
||||
cleanupMode: "none"
|
||||
},
|
||||
session,
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
await (manager as any).collectMkvFilesToLibrary(packageId, session.packages[packageId], undefined, false);
|
||||
|
||||
const cleanBase = "Fritzie.-.Der.Himmel.muss.warten.S04E01.GERMAN.720p.WEB.AVC-4SF";
|
||||
expect(fs.existsSync(path.join(mkvLibraryDir, `${cleanBase}.mkv`))).toBe(true);
|
||||
expect(fs.existsSync(path.join(mkvLibraryDir, rawName))).toBe(false);
|
||||
// Untertitel folgt dem Video auf den sauberen Namen (Sprach-Suffix .de erhalten).
|
||||
expect(fs.existsSync(path.join(mkvLibraryDir, `${cleanBase}.de.srt`))).toBe(true);
|
||||
|
||||
void manager;
|
||||
}, 20000);
|
||||
|
||||
it("deferred final pass renames fresh files before collecting them (no scene names in library)", async () => {
|
||||
// Folge-Fund zu 18eada9 (verifiziert via Advisor-Gate): 18eada9 schloss den
|
||||
// "frische Datei landet unbenannt"-Bug nur fuer den HYBRID-Pfad (deferFreshFiles=true
|
||||
|
||||
Reference in New Issue
Block a user