diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 792a969..d408ca7 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -1502,6 +1502,19 @@ export function decideAutoRenameBaseName( if (targetBaseName && sourceEpisodeToken) { const targetEpisodeToken = extractEpisodeToken(targetBaseName); if (!targetEpisodeToken) { + // Die QUELLE traegt einen sauberen SxxExx-Token, der Ziel-Ordner aber nur einen + // Episoden-Titel mit Episode-only-Token (Ordner "Show.E01.Titel...-GRP", Quelle bereits + // "Show.S01E01...-GRP" — z.B. Miniserien, die der Auto-Rename schon korrekt benannt hat). + // Den Quell-Token an den Ordnernamen anzuhaengen erzeugt einen verkrueppelten Namen + // ("...-GRP.S01E01" — Token HINTER der Gruppe). In DIESEM Zweig traegt die Quelle den + // EINZIGEN SxxExx-Token (der Ordner hat keinen) → ist die Quelle ein sauberer, NICHT + // obfuskierter Scene-Name, ist sie autoritativ → behalten, den Ordner NICHT verwenden. + // (Die Laenge des Serien-Praefixes ist KEIN Kriterium: kurze Serien wie "ER"/"V"/"24" + // sind genauso autoritativ; nur Obfuskierung soll den Ordner gewinnen lassen.) Greift + // v.a. im Collect, der sonst den fertigen Auto-Rename-Namen wieder zerstoeren wuerde. + if (!looksLikeObfuscatedSceneFileName(sourceName)) { + return { kind: "skip", reason: "source-better", targetBaseName }; + } const insertedEpisode = targetBaseName.replace( /(^|[._\-\s])(s\d{1,2})(?=[A-Za-z0-9])/i, `$1${sourceEpisodeToken}.` diff --git a/tasks/lessons.md b/tasks/lessons.md index 6d14793..93276b0 100644 --- a/tasks/lessons.md +++ b/tasks/lessons.md @@ -307,3 +307,29 @@ abgelehnt (kein Over-Firing). v1.7.180-Fallback nutzt jetzt dieselben Module-Con Auto-Rename UND Collect (beide via decideAutoRenameBaseName). 5 Unit- + 1 Collect-Integrationstest. **Methodik-Lektion:** Die naheliegende Hypothese (a/b-Suffix) per Diagnose-Test widerlegt, BEVOR gefixt — das Lookahead genau gelesen statt angenommen. Spart einen Fix am falschen Ort. + +## 2026-06-05 — Collect zerstoert fertigen S01E01-Namen via Episoden-Titel-Ordner (Miniserie) +**Symptom (rename-session 2026-06-05):** Miniserie "Steven Spielbergs Taken" landete als +"...E01.Hinter.dem.Himmel...-GTVG.S01E01.mkv" (Episodentitel + hinten angehaengtes S01E01) statt sauber +"...S01E01...-GTVG.mkv". User: "keine Staffel, nur Episodentitel". +**Root Cause (diagnostisch bewiesen):** Auto-Rename benannte korrekt zu "...S01E01...-GTVG.mkv" (kombiniert +S01 aus dem Paket/Season-Ordner + E01 aus der Quelle). Der COLLECT (deriveCleanCollectFileName -> +decideAutoRenameBaseName) leitet die Datei NEU ab — Quelle ist nun der schon-saubere Name. Der per-Episode- +Ordner traegt aber nur einen Episode-only-Token + Titel ("...E01.Hinter.dem.Himmel...-GTVG", KEIN S01). +buildAutoRenameBaseName nimmt den Ordner (Gruppen-Suffix -GTVG vorhanden). In Guard B `if (!targetEpisodeToken)` +wird der Quell-Token an den Ordnernamen ANGEHAENGT (applyEpisodeTokenToFolderName) -> "...-GTVG.S01E01" +(Token HINTER der Gruppe = verkrueppelt). Der Root-Guard greift NICHT, weil der Season-Ordner einen S01-Token +liefert (anyFolderHasSeasonOrEpisode=true). +**Fix:** In Guard B, im `!targetEpisodeToken`-Zweig VOR dem Anhaengen: ist die QUELLE ein NICHT +obfuskierter Scene-Name (`!looksLikeObfuscatedSceneFileName(sourceName)`), dann +`return {kind:"skip", reason:"source-better"}` -> Collect behaelt den fertigen Namen. In diesem Zweig +traegt die Quelle den EINZIGEN SxxExx-Token (Ordner hat keinen) -> obfuskiert? -> Ordner gewinnt (Append), +sauber? -> Quelle gewinnt. Greift NUR im `!targetEpisodeToken`-Zweig (Ordner ohne SxxExx); safari +(Ordner MIT Token) unberuehrt. 4 Unit- + 1 Collect-Integrationstest. tsc 6 (Baseline), 700/700 gruen, Build gruen. +**Methodik:** Erst Diagnose (decideAutoRenameBaseName mit Collect-Inputs) -> exakt der mangled Name +reproduziert. Per User-Wunsch adversarial via Workflow gegengeprueft (ultracode, 3 Lenses + Synthese). +**Adversarialer Befund (Workflow fing's):** Mein erster Guard hatte einen ZWEITEN Konjunkt +`hasMeaningfulSeriesPrefix(sourceBaseName)` (>=3 Alpha vor S0x). Der ist sachfremd: KURZE Serien (ER, V, +24, Yu) fallen durch -> selber verkrueppelter Name. Gestrichen -> nur `!obfuskiert` gaten. Lehre: ein +zusaetzlicher "klingt-vernuenftig"-Konjunkt (Praefix-Laenge) kann eine ganze reale Klasse (Kurz-Titel) +stumm ausschliessen; adversariale Verifikation mit konkretem Gegenbeispiel (ER.S01E01) hat's gefunden. diff --git a/tests/auto-rename.test.ts b/tests/auto-rename.test.ts index 04fca72..8258fac 100644 --- a/tests/auto-rename.test.ts +++ b/tests/auto-rename.test.ts @@ -1103,3 +1103,51 @@ describe("complete episode folder WITHOUT group suffix (codec/resolution only)", expect(decision.kind).toBe("skip"); }); }); + +describe("collect must not mangle an already-clean SxxExx name via an episode-title folder", () => { + const hash = "c284d9d9072eaf3ac314d05f951dd115"; + // Echter Bug (rename-session 2026-06-05): Miniserie "Steven Spielbergs Taken". Auto-Rename + // benannte korrekt zu "...S01E01...-GTVG". Der per-Episode-Ordner traegt aber nur einen + // Episode-only-Token + Titel ("...E01.Hinter.dem.Himmel...-GTVG", KEIN S01). Der Collect leitete + // daraus neu ab und HAENGTE den Quell-Token verkrueppelt an: "...-GTVG.S01E01" → in der Library + // stand dann "E01.Titel...S01E01" statt sauber "S01E01". + const epFolder = "Steven.Spielbergs.Taken.E01.Hinter.dem.Himmel.German.720p.HDTV.x264-GTVG"; + const pkgFolder = "Steven.Spielbergs.Taken.S01.German.720p.HDTV.x264-GTVG"; + const cleanSource = "Steven.Spielbergs.Taken.S01E01.German.720p.HDTV.x264-GTVG"; + + it("keeps the clean source (skip) instead of appending the token to the episode-title folder", () => { + const decision = decideAutoRenameBaseName([epFolder, pkgFolder], cleanSource + ".mkv", cleanSource, epFolder, pkgFolder); + expect(decision.kind).toBe("skip"); + // NICHT der verkrueppelte "...-GTVG.S01E01"-Name. + expect(JSON.stringify(decision)).not.toContain("GTVG.S01E01"); + }); + + it("still cleans a JUNK/obfuscated source via an episode-title folder (append path intact, no skip)", () => { + // Obfuskierter Hoster-Name (obf=true) → meine Klausel greift NICHT (sie behaelt nur saubere + // Quellen). Mit Season-Ordner als Kontext (Root-Guard ok) wird der Token weiter angewandt: + // die Quelle wird NICHT roh behalten. Pruegt, dass der Fix nicht zu breit ist. + const epFolder = "Show.E05.Die.Sache.German.720p.HDTV.x264-GRP"; + const seasonFolder = "Show.S01.German.720p.HDTV.x264-GRP"; + const decision = decideAutoRenameBaseName([epFolder, seasonFolder], "scn-show7-S01E05.mkv", "scn-show7-S01E05", epFolder, seasonFolder); + expect(decision.kind).toBe("rename"); + expect(extractEpisodeToken((decision as any).baseName)).toBe("S01E05"); + }); + + it("does NOT affect a folder that already carries an SxxExx token (safari S04E08a stays a rename)", () => { + const folder = "Fluss-Monster.S04E08a.Am.Essequibo.Teil.1.German.DOKU.SATRiP.XviD"; + const decision = decideAutoRenameBaseName([folder, hash], "safari-fm-s04e08a.avi", "safari-fm-s04e08a", hash, hash); + expect(decision).toEqual({ kind: "rename", baseName: folder }); + }); + + it("keeps a clean SHORT-prefix series source (ER) instead of the crippled token append", () => { + // Adversarial-Befund: die Praefix-Laenge darf KEIN Kriterium sein. Kurze Serien (ER, V, 24, Yu) + // sind genauso autoritativ; mit dem alten `hasMeaningfulSeriesPrefix`-Konjunkt (>=3 Alpha vor S0x) + // waere ER durchgefallen -> selber verkrueppelter Name wie der gemeldete Bug. + const epFolder = "ER.E01.Tag.und.Nacht.German.720p.HDTV.x264-GROUP"; + const seasonFolder = "ER.S01.German.720p.HDTV.x264-GROUP"; + const cleanSource = "ER.S01E01.German.720p.HDTV.x264-GROUP"; + const decision = decideAutoRenameBaseName([epFolder, seasonFolder], cleanSource + ".mkv", cleanSource, epFolder, seasonFolder); + expect(decision.kind).toBe("skip"); + expect(JSON.stringify(decision)).not.toContain("GROUP.S01E01"); + }); +}); diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index 5bd6a0c..bdfa2dc 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -9752,6 +9752,71 @@ describe("download manager", () => { void manager; }, 20000); + it("collect KEEPS the clean SxxExx name and does NOT mangle it via an episode-title folder (Taken S01E01)", async () => { + // Echter Bug (rename-session 2026-06-05): Auto-Rename hatte die Datei bereits korrekt zu + // "...S01E01...-GTVG.mkv" benannt. Der per-Episode-Ordner traegt nur "E01" + Titel (kein S01). + // Der Collect leitete daraus neu ab und haengte den Token verkrueppelt an + // ("...-GTVG.S01E01"). Erwartung: der saubere S01E01-Name bleibt unangetastet. + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); + tempDirs.push(root); + + const packageName = "Steven.Spielbergs.Taken.S01.German.720p.HDTV.x264-GTVG"; + const outputDir = path.join(root, "downloads", packageName); + const extractDir = path.join(root, "extract", packageName); + const epFolder = "Steven.Spielbergs.Taken.E01.Hinter.dem.Himmel.German.720p.HDTV.x264-GTVG"; + const cleanName = "Steven.Spielbergs.Taken.S01E01.German.720p.HDTV.x264-GTVG.mkv"; + const epDir = path.join(extractDir, epFolder); + fs.mkdirSync(epDir, { recursive: true }); + // Datei liegt bereits SAUBER benannt vor (so wie Auto-Rename sie hinterlassen hat). + fs.writeFileSync(path.join(epDir, cleanName), Buffer.alloc(4096, 5)); + + 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); + + // Sauberer S01E01-Name bleibt; KEIN verkrueppelter "...E01.Titel...S01E01"-Name. + expect(fs.existsSync(path.join(mkvLibraryDir, cleanName))).toBe(true); + const mangled = `${epFolder}.S01E01.mkv`; + expect(fs.existsSync(path.join(mkvLibraryDir, mangled))).toBe(false); + // Nichts mit dem Episoden-Titel im Library-Ordner. + const inLib = fs.readdirSync(mkvLibraryDir); + expect(inLib.some((n) => /Hinter\.dem\.Himmel/i.test(n))).toBe(false); + + 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