Fix: Folgen mit Bonus-Wort im Titel bleiben nicht mehr in "Downloader Fertig" liegen
Eine Folge mit gueltigem SxxExx-Token ist eine echte Episode, niemals Bonus/Extras — auch wenn ihr Titel oder der Episoden-Ordnername ein Bonus-Wort enthaelt (Interview/Outtakes/Special/Featurette/Making-Of/...). Bisher stufte der Library- Collect (und Auto-Rename) solche Folgen als Extras ein und verschob sie NIE in die Bibliothek — extrahiert und korrekt benannt, aber stumm liegengelassen (Skip nur via logger.info, im Paket-Log unsichtbar). Betraf u.a. Revenge S04E19 "Interview". Neue isBonusContent()-Guard an beiden Call-Sites: erst SxxExx pruefen (extractEpisodeToken), nur ohne Token greift der Bonus-Filter (isInsideBonusDir / BONUS_FILENAME_RE). Echte Extras ohne Token bleiben gefiltert. 2 Integrationstests + 5 Unit-Tests.
This commit is contained in:
parent
95a951ccc3
commit
afba79cdfd
@ -180,6 +180,24 @@ function isInsideBonusDir(filePath: string, packageDir: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** True if a file is bonus/extras content (Making-Of, Featurette, Interview, …)
|
||||||
|
* that must NOT be collected into the flat episode library.
|
||||||
|
*
|
||||||
|
* CRITICAL GUARD: a file carrying a real SxxExx episode token is a NUMBERED
|
||||||
|
* EPISODE, never extras — even when its TITLE (or per-episode folder name)
|
||||||
|
* happens to contain a bonus keyword, e.g. "Revenge.2011.S04E19.Interview".
|
||||||
|
* Without this guard, episodes titled Interview/Outtakes/Special/Featurette/…
|
||||||
|
* (and entire series whose name is such a word) were silently dropped from the
|
||||||
|
* library — extracted + correctly renamed but never moved, no error (rd-support
|
||||||
|
* bundle 2026-06-04). Genuine extras lack an SxxExx token (or live in an
|
||||||
|
* Extras/Bonus subdir) and are still excluded. */
|
||||||
|
export function isBonusContent(filePath: string, packageDir: string, nameWithoutExt: string): boolean {
|
||||||
|
if (extractEpisodeToken(nameWithoutExt)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return isInsideBonusDir(filePath, packageDir) || BONUS_FILENAME_RE.test(nameWithoutExt);
|
||||||
|
}
|
||||||
|
|
||||||
function expectedMinBytes(totalBytes: number | null | undefined, strict: boolean): number {
|
function expectedMinBytes(totalBytes: number | null | undefined, strict: boolean): number {
|
||||||
if (!totalBytes || totalBytes <= 0) {
|
if (!totalBytes || totalBytes <= 0) {
|
||||||
return 10240;
|
return 10240;
|
||||||
@ -4291,7 +4309,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
// Skip bonus/extras content (Featurettes, Making-Of, Behind-The-Scenes, etc.)
|
// Skip bonus/extras content (Featurettes, Making-Of, Behind-The-Scenes, etc.)
|
||||||
// These have generic descriptive names and would get renamed to misleading
|
// These have generic descriptive names and would get renamed to misleading
|
||||||
// episode names if matched against the package's SxxExx pattern.
|
// episode names if matched against the package's SxxExx pattern.
|
||||||
if (isInsideBonusDir(sourcePath, extractDir) || BONUS_FILENAME_RE.test(sourceBaseName)) {
|
if (isBonusContent(sourcePath, extractDir, sourceBaseName)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const folderCandidates: string[] = [];
|
const folderCandidates: string[] = [];
|
||||||
@ -5033,7 +5051,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
sampleSkipped += 1;
|
sampleSkipped += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isInsideBonusDir(filePath, sourceRoot) || BONUS_FILENAME_RE.test(stem)) {
|
if (isBonusContent(filePath, sourceRoot, stem)) {
|
||||||
bonusSkipped += 1;
|
bonusSkipped += 1;
|
||||||
logger.info(`MKV-Sammelordner: Bonus-Datei uebersprungen: ${path.basename(filePath)} (Pfad: ${path.relative(sourceRoot, filePath)})`);
|
logger.info(`MKV-Sammelordner: Bonus-Datei uebersprungen: ${path.basename(filePath)} (Pfad: ${path.relative(sourceRoot, filePath)})`);
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@ -248,3 +248,44 @@ BONUS_FILENAME_RE) und Collect filtern beide vor der Namensherleitung → gedeck
|
|||||||
**Meta:** 3. „anderes Format" in Folge — diese Klasse (Junk-Quelle + sauberer Ordner) ist die
|
**Meta:** 3. „anderes Format" in Folge — diese Klasse (Junk-Quelle + sauberer Ordner) ist die
|
||||||
groesste verbleibende. Scene-Naming hat aber einen langen Schwanz: ehrlich „diese Klasse ist
|
groesste verbleibende. Scene-Naming hat aber einen langen Schwanz: ehrlich „diese Klasse ist
|
||||||
abgedeckt", nicht „jetzt 100%". Das Desktop-Log liefert jede neue Klasse sofort.
|
abgedeckt", nicht „jetzt 100%". Das Desktop-Log liefert jede neue Klasse sofort.
|
||||||
|
|
||||||
|
## 2026-06-04 — KEINE „Claude/AI"-Spuren in oeffentlichen Releases (GitHub)
|
||||||
|
**Korrektur:** „kein SCHAU MAL wie ich mit claude gearbeitet hab release … entfern alles was da drin
|
||||||
|
steckt." Beim einmaligen GitHub-Sync (Sucukdeluxe/real-debrid-downloader) waren oeffentlich: `CLAUDE.md`,
|
||||||
|
`design-mockups/`, `tasks/lessons.md`+`todo.md`, historisch `.claude/`, und **357 Commits mit
|
||||||
|
`Co-Authored-By: Claude`-Trailer**.
|
||||||
|
**Regel ab jetzt:** Fuer dieses Projekt KEINE `Co-Authored-By: Claude`-Trailer mehr an Commits
|
||||||
|
(ueberschreibt die Default-Git-Anweisung — User-Wunsch hat Vorrang). Keine KI-Artefakte (CLAUDE.md,
|
||||||
|
Mockups, lessons/todo, .claude/) in irgendetwas, das oeffentlich gepusht wird.
|
||||||
|
**Wie sauber gemacht (ohne Gitea/lokal anzufassen):** isolierter `git clone` → `git filter-repo`
|
||||||
|
(`--invert-paths --path …` + `--message-callback` der Trailer-Zeilen droppt) → Force-Push NUR main +
|
||||||
|
v1.7.180 zu GitHub. Alte Tags NICHT geloescht, sondern via `.git/filter-repo/commit-map` auf ihre
|
||||||
|
sauberen Commits **umgehaengt** (89 Tags, alle Releases bleiben erhalten) — besser als Loeschen.
|
||||||
|
**Ehrliche Grenze (Advisor):** Force-Push säubert nur ref-erreichbare Historie. Verwaiste alte Commits
|
||||||
|
bleiben per voller SHA erreichbar, bis GitHub GC'd ODER das Repo neu angelegt wird (nur der User kann
|
||||||
|
das — Token hat kein `delete_repo`). Lokaler Klon verifiziert ≠ GitHub-Zustand: immer per `gh api`
|
||||||
|
gegenpruefen (Datei 404 am Tag, Commit-Messages trailer-frei).
|
||||||
|
**Methodik:** vor Force-Push Voll-Range-Secret-Scan (push-protection killt sonst mitten im Push) +
|
||||||
|
Tree-Content-Grep auf `claude|anthropic` (filter-repo tilgt Pfad-NAMEN + Trailer, nicht Datei-INHALTE).
|
||||||
|
|
||||||
|
## 2026-06-04 — Folge bleibt bei „Downloader Fertig" haengen: Episodentitel == Bonus-Wort
|
||||||
|
**Symptom (User-Screenshot + rd-support-bundle):** `Revenge.2011.S04E19.Interview...mkv` extrahiert +
|
||||||
|
korrekt umbenannt, aber NIE in die Library verschoben — kein Fehler. „selten, 4-5 Folgen pro 1,5TB".
|
||||||
|
**Diagnose (Bundle):** Paket-Log zeigte 22/23 „MKV verschoben", E19 fehlte, KEIN WARN/ERROR. Im
|
||||||
|
HAUPT-Log (`rd_downloader.log`) dann 5× `MKV-Sammelordner: Bonus-Datei uebersprungen: ...S04E19.Interview`.
|
||||||
|
**Root Cause:** `BONUS_FILENAME_RE` enthaelt `interview` (+ outtakes/special/featurette/bloopers/...). Der
|
||||||
|
Episodentitel „Interview" (UND der Episoden-Ordnername — `isInsideBonusDir` macht `.includes()` Substring)
|
||||||
|
matchte → `collectMkvFilesToLibrary` stufte die echte Folge als Bonus/Extras ein und skippte sie. Trifft
|
||||||
|
auch ganze Serien deren NAME ein Bonus-Wort ist. Skip war nur `logger.info` → im Paket-Log UNSICHTBAR
|
||||||
|
(darum „silent orphan", nur via Forensik gefunden).
|
||||||
|
**Fix:** neue exportierte `isBonusContent(filePath, packageDir, nameWithoutExt)` — eine Datei MIT echtem
|
||||||
|
SxxExx-Token (`extractEpisodeToken`) ist eine nummerierte Episode, NIE Bonus (egal welches Titelwort).
|
||||||
|
Echte Extras (kein Token / Extras-Subordner) bleiben gefiltert. Beide Call-Sites umgestellt (Auto-Rename
|
||||||
|
~4312 + Collect ~5054). 2 Integrationstests (Interview wird gesammelt / Making.Of bleibt) + 5 Unit-Tests.
|
||||||
|
**Diagnose-Lektion (Advisor-Gate):** „4-5 Folgen" plural → NICHT beim 1. Fund stoppen. Bundle-weit
|
||||||
|
gegengeprueft: 0 Move-Fehler, nur 1 Bonus-Skip. 4 weitere „noch frisch"-Defers sahen wie Orphans aus,
|
||||||
|
waren aber FALSE POSITIVES — Moves loggen NICHT ins Haupt-Log (nur Paket-Log), und deren Paket-Logs fehlten
|
||||||
|
im Bundle. Per Code bewiesen: finaler Deferred-Collect laeuft fuer jedes fertige Paket (`success` =
|
||||||
|
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.
|
||||||
|
|||||||
@ -9,7 +9,8 @@ import {
|
|||||||
buildAutoRenameBaseNameFromFoldersWithOptions,
|
buildAutoRenameBaseNameFromFoldersWithOptions,
|
||||||
hasMeaningfulSeriesPrefix,
|
hasMeaningfulSeriesPrefix,
|
||||||
looksLikeObfuscatedSceneFileName,
|
looksLikeObfuscatedSceneFileName,
|
||||||
decideAutoRenameBaseName
|
decideAutoRenameBaseName,
|
||||||
|
isBonusContent
|
||||||
} from "../src/main/download-manager";
|
} from "../src/main/download-manager";
|
||||||
|
|
||||||
describe("decideAutoRenameBaseName (shared naming decision — used by auto-rename AND mkv-collect)", () => {
|
describe("decideAutoRenameBaseName (shared naming decision — used by auto-rename AND mkv-collect)", () => {
|
||||||
@ -1021,3 +1022,42 @@ describe("buildAutoRenameBaseNameFromFolders", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("isBonusContent (numbered episodes are never bonus)", () => {
|
||||||
|
const pkgDir = "/pkg/Show.S04.GERMAN.DL.720p.WEB.x264-GRP";
|
||||||
|
|
||||||
|
it("does NOT treat a numbered episode as bonus even when its TITLE is a bonus word", () => {
|
||||||
|
// Der gemeldete Bug: Revenge.2011.S04E19.Interview wurde als Bonus verworfen.
|
||||||
|
const name = "Revenge.2011.S04E19.Interview.GERMAN.DL.720p.WEB.x264-TSCC";
|
||||||
|
const fp = `${pkgDir}/${name}/${name}.mkv`;
|
||||||
|
expect(isBonusContent(fp, pkgDir, name)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("covers further bonus-word episode titles with a token", () => {
|
||||||
|
for (const title of ["Special", "Featurette", "Outtakes", "Bloopers", "Making.Of"]) {
|
||||||
|
const name = `Show.S04E07.${title}.GERMAN.720p.WEB.x264-GRP`;
|
||||||
|
expect(isBonusContent(`${pkgDir}/${name}.mkv`, pkgDir, name)).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("STILL treats genuine extras WITHOUT an episode token as bonus", () => {
|
||||||
|
for (const name of [
|
||||||
|
"Show.Making.Of.GERMAN.720p.WEB.x264-GRP",
|
||||||
|
"Show.Behind.The.Scenes.GERMAN-GRP",
|
||||||
|
"Some.Interview.With.Cast"
|
||||||
|
]) {
|
||||||
|
expect(isBonusContent(`${pkgDir}/${name}.mkv`, pkgDir, name)).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a token-bearing file inside an Extras subfolder is still kept (numbered episode wins)", () => {
|
||||||
|
const name = "Show.S04E19.Interview.GROUP";
|
||||||
|
const fp = `${pkgDir}/Extras/${name}/${name}.mkv`;
|
||||||
|
expect(isBonusContent(fp, pkgDir, name)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a token-less file inside an Extras subfolder is bonus", () => {
|
||||||
|
const fp = `${pkgDir}/Extras/Making.Of.mkv`;
|
||||||
|
expect(isBonusContent(fp, pkgDir, "Making.Of")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -9561,6 +9561,131 @@ describe("download manager", () => {
|
|||||||
void manager;
|
void manager;
|
||||||
}, 20000);
|
}, 20000);
|
||||||
|
|
||||||
|
it("collect MOVES a numbered episode whose TITLE is a bonus keyword (Revenge S04E19 'Interview')", async () => {
|
||||||
|
// Echter Bug aus rd-support-bundle 2026-06-04: Revenge.2011.S04E19.Interview blieb roh
|
||||||
|
// in "Downloader Fertig" haengen — nie in die Library verschoben, KEIN Fehler. Ursache:
|
||||||
|
// der Episodentitel "Interview" (UND der Episoden-Ordnername) matcht BONUS_FILENAME_RE /
|
||||||
|
// isInsideBonusDir -> der Collect stufte die Folge als Bonus/Extras ein und skippte sie
|
||||||
|
// (nur logger.info, im Paket-Log unsichtbar). Eine Folge MIT gueltigem SxxExx-Token ist
|
||||||
|
// aber eine echte Episode, niemals Bonus. Betrifft Interview/Outtakes/Special/Featurette-
|
||||||
|
// Titel -> "selten, aber 4-5 Folgen pro grossem Download".
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
|
||||||
|
const packageName = "Revenge.2011.S04.GERMAN.DL.720p.WEB.x264-TSCC";
|
||||||
|
const outputDir = path.join(root, "downloads", packageName);
|
||||||
|
const extractDir = path.join(root, "extract", packageName);
|
||||||
|
// Per-Episoden-Ordner UND Datei tragen beide das Bonus-Wort "Interview" — exakt der Fall.
|
||||||
|
const episodeFolder = "Revenge.2011.S04E19.Interview.GERMAN.DL.720p.WEB.x264-TSCC";
|
||||||
|
const epDir = path.join(extractDir, episodeFolder);
|
||||||
|
fs.mkdirSync(epDir, { recursive: true });
|
||||||
|
const epName = `${episodeFolder}.mkv`;
|
||||||
|
fs.writeFileSync(path.join(epDir, epName), Buffer.alloc(4096, 9));
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Die Folge MUSS in der Library liegen (nicht als Bonus verworfen) und die Quelle weg sein.
|
||||||
|
expect(fs.existsSync(path.join(mkvLibraryDir, epName))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(epDir, epName))).toBe(false);
|
||||||
|
|
||||||
|
void manager;
|
||||||
|
}, 20000);
|
||||||
|
|
||||||
|
it("collect STILL skips genuine bonus/extras with NO episode token (Making.Of) — proves the filter isn't disabled", async () => {
|
||||||
|
// Guard zum Fix oben: eine echte Bonus-Datei OHNE SxxExx-Token (Making.Of) bleibt Bonus
|
||||||
|
// und darf NICHT in die Library wandern. Sonst haetten wir den Bonus-Filter nur kaputt
|
||||||
|
// gemacht statt praezisiert.
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
|
||||||
|
const packageName = "Some.Show.S01.GERMAN.720p.WEB.x264-GRP";
|
||||||
|
const outputDir = path.join(root, "downloads", packageName);
|
||||||
|
const extractDir = path.join(root, "extract", packageName);
|
||||||
|
fs.mkdirSync(extractDir, { recursive: true });
|
||||||
|
// Echte Episode (mit Token) + echtes Extra (ohne Token) im selben Paket.
|
||||||
|
const epName = "Some.Show.S01E01.GERMAN.720p.WEB.x264-GRP.mkv";
|
||||||
|
const bonusName = "Some.Show.Making.Of.GERMAN.720p.WEB.x264-GRP.mkv";
|
||||||
|
fs.writeFileSync(path.join(extractDir, epName), Buffer.alloc(4096, 1));
|
||||||
|
fs.writeFileSync(path.join(extractDir, bonusName), Buffer.alloc(4096, 2));
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Echte Episode wandert in die Library; das Making-Of bleibt liegen (Bonus).
|
||||||
|
expect(fs.existsSync(path.join(mkvLibraryDir, epName))).toBe(true);
|
||||||
|
expect(fs.existsSync(path.join(mkvLibraryDir, bonusName))).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(extractDir, bonusName))).toBe(true);
|
||||||
|
|
||||||
|
void manager;
|
||||||
|
}, 20000);
|
||||||
|
|
||||||
it("deferred final pass renames fresh files before collecting them (no scene names in library)", async () => {
|
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
|
// Folge-Fund zu 18eada9 (verifiziert via Advisor-Gate): 18eada9 schloss den
|
||||||
// "frische Datei landet unbenannt"-Bug nur fuer den HYBRID-Pfad (deferFreshFiles=true
|
// "frische Datei landet unbenannt"-Bug nur fuer den HYBRID-Pfad (deferFreshFiles=true
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user