From 339c46bdd2c356eaed7cc0c38d5b38ff060849b2 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 5 Jun 2026 17:54:02 +0200 Subject: [PATCH] Fix: Folgen in vollstaendigem Episoden-Ordner OHNE -GROUP-Suffix werden umbenannt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Alte deutsche Dokus/Serien-Ordner ohne Gruppen-Suffix (Ordner endet auf bare Codec ".XviD", kein "-GROUP") wurden vom Auto-Rename als "kein Zielname" verworfen — die Folge landete dann ROH in der Library (z.B. "safari-fm-s04e08a.avi" statt "Fluss-Monster.S04E08a.Am.Essequibo.Teil.1.German.DOKU.SATRiP.XviD.avi"). buildAutoRenameBaseName akzeptiert jetzt zusaetzlich einen vollstaendigen Episoden- Ordner: echter SxxExx-Token IM Ordnernamen UND ein Codec-/Aufloesungs-Marker (SCENE_RESOLUTION_MARKER_RE / SCENE_CODEC_MARKER_RE, inkl. xvid/divx). Der Part- Buchstabe a/b bleibt erhalten (Ordnername dient unveraendert als Zielname), sodass Teil 1 und Teil 2 nicht kollidieren. Konservativ: ein nackter "Show.S01E01"-Ordner ohne Qualitaets-/Codec-Marker wird weiterhin nicht abgeleitet. Greift in Auto-Rename und Collect. 5 Unit- + 1 Collect-Integrationstest; v1.7.180-Fallback nutzt jetzt dieselben Module-Konstanten (DRY). --- src/main/download-manager.ts | 21 ++++++++--- tasks/lessons.md | 18 ++++++++++ tests/auto-rename.test.ts | 42 ++++++++++++++++++++++ tests/download-manager.test.ts | 66 ++++++++++++++++++++++++++++++++++ 4 files changed, 142 insertions(+), 5 deletions(-) diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 7158dee..792a969 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -946,6 +946,10 @@ const SCENE_COMPACT_EPISODE_CODE_RE = /(?:^|[._\-\s])(\d{3,4})([a-z])?(?=$|[._\- 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; +// Marker, dass ein Name eine vollstaendige Release-Benennung ist (Aufloesung ODER Codec). +// Bewusst inkl. xvid/divx — alte deutsche Dokus/Serien sind oft XviD ohne Aufloesungs-Tag. +const SCENE_RESOLUTION_MARKER_RE = /\b(?:480|540|576|720|1080|1440|2160|4320)[pi]\b/i; +const SCENE_CODEC_MARKER_RE = /\b(?:x264|x265|x266|h\.?264|h\.?265|h\.?266|hevc|avc|vvc|av1|xvid|divx)\b/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][^-]*$/; @@ -1306,7 +1310,16 @@ export function buildAutoRenameBaseName(folderName: string, sourceFileName: stri const isLegacy4sf4sjFolder = SCENE_RELEASE_FOLDER_RE.test(normalizedFolderName); const isSceneGroupFolder = hasSceneGroupSuffix(normalizedFolderName); - if (!isLegacy4sf4sjFolder && !isSceneGroupFolder) { + // Auch einen vollstaendigen Episoden-Ordnernamen OHNE Gruppen-Suffix akzeptieren: hat der + // Ordner einen echten SxxExx-Token UND einen Codec-/Aufloesungs-Marker, ist es eine saubere + // Release-Benennung (z.B. alte deutsche Dokus ohne -GROUP, Ordner endet auf ".XviD": + // "Fluss-Monster.S04E08a.Am.Essequibo.Teil.1.German.DOKU.SATRiP.XviD"). Ohne diese Klausel + // lieferte der Helper null → "kein Zielname" → die Folge blieb mit rohem Hoster-Namen + // ("safari-fm-s04e08a.avi") in der Library liegen. Part-Buchstaben (a/b) bleiben erhalten, + // weil der Ordnername unveraendert als Zielname dient (nur der RANGE-Zweig schreibt Token um). + const isCompleteEpisodeFolder = Boolean(extractEpisodeToken(normalizedFolderName)) + && (SCENE_RESOLUTION_MARKER_RE.test(normalizedFolderName) || SCENE_CODEC_MARKER_RE.test(normalizedFolderName)); + if (!isLegacy4sf4sjFolder && !isSceneGroupFolder && !isCompleteEpisodeFolder) { return null; } @@ -1323,7 +1336,7 @@ export function buildAutoRenameBaseName(folderName: string, sourceFileName: stri // range with the source's specific episode token. Without this, all // episodes in a range-named folder share the same target name, producing // (2)(3)(4) suffixes during MKV collection. - if (!isLegacy4sf4sjFolder && isSceneGroupFolder) { + if (!isLegacy4sf4sjFolder && (isSceneGroupFolder || isCompleteEpisodeFolder)) { const episodeRangeRe = /(^|[._\-\s])s\d{1,2}e\d{1,3}[-]e?\d{1,3}(?=[._\-\s]|$)/i; if (episodeRangeRe.test(normalizedFolderName)) { next = applyEpisodeTokenToFolderName(normalizedFolderName, episodeToken); @@ -1537,14 +1550,12 @@ export function decideAutoRenameBaseName( // Greift NUR ohne Quell-Episode-Token → schliesst sich mit dem Fabrikations-Guard oben aus // (Mega-Direct hat einen Quell-Token und kommt nie hierher). if (!extractEpisodeToken(sourceBaseName)) { - const RESOLUTION_RE = /\b(?:480|540|576|720|1080|1440|2160|4320)[pi]\b/i; - const CODEC_RE = /\b(?:x264|x265|x266|h\.?264|h\.?265|h\.?266|hevc|avc|vvc|av1|xvid|divx)\b/i; for (const folderName of folderCandidates) { const f = sanitizeFilename(String(folderName || "").trim()); if (!f) { continue; } - if (hasSceneGroupSuffix(f) && (RESOLUTION_RE.test(f) || CODEC_RE.test(f)) && !SCENE_SEASON_ONLY_RE.test(f)) { + if (hasSceneGroupSuffix(f) && (SCENE_RESOLUTION_MARKER_RE.test(f) || SCENE_CODEC_MARKER_RE.test(f)) && !SCENE_SEASON_ONLY_RE.test(f)) { return { kind: "rename", baseName: f, note: "folder-as-is" }; } } diff --git a/tasks/lessons.md b/tasks/lessons.md index d37f504..6d14793 100644 --- a/tasks/lessons.md +++ b/tasks/lessons.md @@ -289,3 +289,21 @@ im Bundle. Per Code bewiesen: finaler Deferred-Collect laeuft fuer jedes fertige completed-Items, Z.11904) mit `deferFreshFiles=false` → faengt Frische-Defers. Also Frische orphan't NICHT; Bonus schon (Filter ignoriert deferFreshFiles, skippt in JEDEM Pass inkl. final). Lehre: bevor man „X ist Orphan" behauptet, pruefen ob der GEGENBEWEIS (Move) im verfuegbaren Log ueberhaupt sichtbar WAERE. + +## 2026-06-05 — Folge bleibt ROH: vollstaendiger Episoden-Ordner OHNE -GROUP-Suffix +**Symptom (rename-session 2026-06-04):** `safari-fm-s04e08a.avi` / `...b.avi` landeten ROH in der Library +(entpackt2). Log: `Auto-Rename übersprungen: kein Zielname`. Funktionierende S01E02 hatte Ordner +`...XviD-SAFARi` (Gruppe), die kaputten S04E08a/b hatten `...SATRiP.XviD` (KEIN -GROUP). +**Root Cause (Wegwerf-Diagnose, NICHT geraten):** Erste Hypothese „a/b-Token nicht erkannt" war FALSCH — +`extractEpisodeToken("...s04e08a")`="S04E08" (das Lookahead `(?!\d)` verbietet nur Ziffern, nicht Buchstaben). +Echte Ursache: das Gate in `buildAutoRenameBaseName` (`isLegacy4sf || isSceneGroupFolder`) lehnt einen +vollstaendigen Episoden-Ordner OHNE -GROUP ab (endet auf bare Codec `.XviD`). Die QUELLE hat aber einen +Token → der v1.7.180-Fallback (greift NUR ohne Quell-Token) feuert nicht → no-target → roh gemoved. +**Fix:** Gate um `isCompleteEpisodeFolder` erweitert = echter Episoden-Token IM Ordner UND Codec-/ +Aufloesungs-Marker (neue Module-Consts `SCENE_RESOLUTION_MARKER_RE` / `SCENE_CODEC_MARKER_RE`, inkl. +xvid/divx). Part-Buchstabe a/b bleibt erhalten (Ordnername dient unveraendert als Zielname; nur der +RANGE-Zweig schreibt Token um, und a/b ist kein Range). Konservativ: bare „Show.S01E01" ohne Marker bleibt +abgelehnt (kein Over-Firing). v1.7.180-Fallback nutzt jetzt dieselben Module-Consts (DRY). Greift in +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. diff --git a/tests/auto-rename.test.ts b/tests/auto-rename.test.ts index 73b1cea..04fca72 100644 --- a/tests/auto-rename.test.ts +++ b/tests/auto-rename.test.ts @@ -1061,3 +1061,45 @@ describe("isBonusContent (numbered episodes are never bonus)", () => { expect(isBonusContent(fp, pkgDir, "Making.Of")).toBe(true); }); }); + +describe("complete episode folder WITHOUT group suffix (codec/resolution only)", () => { + const hash = "c284d9d9072eaf3ac314d05f951dd115"; + + it("uses the clean folder name when it has an episode token + codec but no -GROUP (safari S04E08a)", () => { + // Echter Bug (rename-session 2026-06-04): alte deutsche Doku ohne Gruppen-Suffix. + // Ordner endet auf ".XviD" (kein "-GROUP") -> buildAutoRenameBaseName lieferte null -> + // "kein Zielname" -> Datei landete roh als "safari-fm-s04e08a.avi" in der Library. + 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 multi-part letters a/b distinct (Teil.1 vs Teil.2 do NOT collide)", () => { + const fa = "Fluss-Monster.S04E08a.Am.Essequibo.Teil.1.German.DOKU.SATRiP.XviD"; + const fb = "Fluss-Monster.S04E08b.Am.Essequibo.Teil.2.German.DOKU.SATRiP.XviD"; + const da = decideAutoRenameBaseName([fa, hash], "safari-fm-s04e08a.avi", "safari-fm-s04e08a", hash, hash); + const db = decideAutoRenameBaseName([fb, hash], "safari-fm-s04e08b.avi", "safari-fm-s04e08b", hash, hash); + expect(da).toEqual({ kind: "rename", baseName: fa }); + expect(db).toEqual({ kind: "rename", baseName: fb }); + // Verschiedene Zielnamen -> keine "(2)"-Kollision beim Sammeln. + expect((da as any).baseName).not.toBe((db as any).baseName); + }); + + it("the previously-working group-suffix folder still works (no regression)", () => { + const folder = "Fluss-Monster.S01E02.Auf.der.Suche.nach.dem.Killer-Wels.German.DOKU.SATRiP.XviD-SAFARi"; + const decision = decideAutoRenameBaseName([folder, hash], "safari-fm-s01e02.avi", "safari-fm-s01e02", hash, hash); + expect(decision).toEqual({ kind: "rename", baseName: folder }); + }); + + it("does NOT use a bare episode folder WITHOUT any codec/resolution marker (stays conservative)", () => { + // "Show.S01E01" allein (keine Qualitaets-/Codec-Info, kein -GROUP) ist mehrdeutig -> + // weiterhin kein Ableiten (kein Over-Firing auf generische Ordner). + const decision = decideAutoRenameBaseName(["Show.S01E01", hash], "abc-s01e01.avi", "abc-s01e01", hash, hash); + expect(decision.kind).toBe("skip"); + }); + + it("does NOT fabricate a name from a token-LESS folder (Mega-Direct guard intact)", () => { + const decision = decideAutoRenameBaseName(["Mega-Direct-Pack", hash], "Direct.Show.S01E01.DIRECT.mkv", "Direct.Show.S01E01.DIRECT", hash, hash); + expect(decision.kind).toBe("skip"); + }); +}); diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index 2b98e71..5bd6a0c 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -9686,6 +9686,72 @@ describe("download manager", () => { void manager; }, 20000); + it("collect CLEANS a raw .avi whose folder is a complete episode name WITHOUT a -GROUP suffix (safari S04E08a)", async () => { + // Echter Bug (rename-session 2026-06-04): alte deutsche Doku ohne Gruppen-Suffix + // (Ordner endet ".XviD", kein "-GROUP"). buildAutoRenameBaseName lieferte null -> die + // Folge landete ROH als "safari-fm-s04e08a.avi" in der Library. Der Part-Buchstabe a/b + // muss erhalten bleiben (Teil 1 vs Teil 2 duerfen nicht kollidieren). + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); + tempDirs.push(root); + + const packageName = "c284d9d9072eaf3ac314d05f951dd115"; + const outputDir = path.join(root, "downloads", packageName); + const extractDir = path.join(root, "extract", packageName); + const mk = (folder: string, raw: string) => { + const epDir = path.join(extractDir, folder); + fs.mkdirSync(epDir, { recursive: true }); + fs.writeFileSync(path.join(epDir, raw), Buffer.alloc(4096, 8)); + }; + const folderA = "Fluss-Monster.S04E08a.Am.Essequibo.Teil.1.German.DOKU.SATRiP.XviD"; + const folderB = "Fluss-Monster.S04E08b.Am.Essequibo.Teil.2.German.DOKU.SATRiP.XviD"; + mk(folderA, "safari-fm-s04e08a.avi"); + mk(folderB, "safari-fm-s04e08b.avi"); + + 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); + + // Beide Folgen sauber benannt in der Library, Part a/b distinkt, KEINE rohen Namen. + expect(fs.existsSync(path.join(mkvLibraryDir, `${folderA}.avi`))).toBe(true); + expect(fs.existsSync(path.join(mkvLibraryDir, `${folderB}.avi`))).toBe(true); + expect(fs.existsSync(path.join(mkvLibraryDir, "safari-fm-s04e08a.avi"))).toBe(false); + expect(fs.existsSync(path.join(mkvLibraryDir, "safari-fm-s04e08b.avi"))).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