Bonus dir detection: normalize separators (Making.Of, Behind.The.Scenes)
The v1.7.130 BONUS_DIR_PATTERNS used substring matching with space-separated patterns like "making of" and "behind the scenes", but real-world subfolder names use dot/dash/underscore separators (e.g. "Breaking.Bad.S05.Making.Of"). These were NOT detected as bonus dirs, causing the safety net in v1.7.131 to apply the source filename's episode token to the package name, producing mislabeled bonus files like "Breaking.Bad.S05E10.GERMAN.BluRay.720p.TSCC". Fix: normalize folder segments by stripping all separators ([._-\s]+) before matching against BONUS_DIR_NORMALIZED_PATTERNS. "Breaking.Bad.S05.Making.Of" normalizes to "breakingbads05makingof" which matches "makingof". Also extend BONUS_FILENAME_RE with "inside-e\d+" and "making-of-e\d+" to catch more filename variants from Breaking Bad BluRay extras. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9f59b6e7ca
commit
6e936cd5bc
@ -135,15 +135,24 @@ const LARGE_BINARY_FILE_RE = /\.(?:part\d+\.rar|rar|r\d{2,3}|zip(?:\.\d+)?|7z(?:
|
||||
const KNOWN_SMALL_FILE_RE = /\.(?:sfv|nfo|nzb|md5|sha1|sha256|crc|txt|url|lnk|srr)$/i;
|
||||
|
||||
/** Folder name patterns indicating bonus/extras content that should NOT be moved
|
||||
* to the flat MKV library or auto-renamed. Matches as a substring of the folder name. */
|
||||
const BONUS_DIR_PATTERNS = [
|
||||
"extras", "extra", "bonus", "featurettes", "featurette", "specials", "special features",
|
||||
"behind the scenes", "behindthescenes", "deleted scenes", "deletedscenes",
|
||||
"making of", "makingof", "outtakes", "trailers", "interviews", "documentaries"
|
||||
* to the flat MKV library or auto-renamed. Matches after normalizing separators
|
||||
* (so "Making.Of", "making-of", "making of", "makingof" all match "makingof"). */
|
||||
const BONUS_DIR_NORMALIZED_PATTERNS = [
|
||||
"extras", "extra", "bonus", "featurettes", "featurette",
|
||||
"specials", "specialfeatures",
|
||||
"behindthescenes", "deletedscenes", "deletedscene",
|
||||
"makingof", "outtakes", "trailers", "interviews", "documentaries",
|
||||
"alternateending", "gagreel"
|
||||
];
|
||||
|
||||
/** Filename token patterns for bonus content (e.g. "making-of-e02.mkv"). */
|
||||
const BONUS_FILENAME_RE = /(?:^|[._\-\s])(?:making[._\-\s]?of|behind[._\-\s]?the[._\-\s]?scenes|deleted[._\-\s]?scene|alternate[._\-\s]?ending|gag[._\-\s]?reel|featurette|outtakes?|bloopers?|interview|extended[._\-\s]?scene|exclusive[._\-\s]?scene)(?:[._\-\s]|$)/i;
|
||||
const BONUS_FILENAME_RE = /(?:^|[._\-\s])(?:making[._\-\s]?of|behind[._\-\s]?the[._\-\s]?scenes|deleted[._\-\s]?scene|alternate[._\-\s]?ending|gag[._\-\s]?reel|featurette|outtakes?|bloopers?|interview|extended[._\-\s]?scene|exclusive[._\-\s]?scene|inside[._\-\s]?e\d+|making[._\-\s]?of[._\-\s]?e\d+)(?:[._\-\s]|$)/i;
|
||||
|
||||
/** Normalize a folder/file segment for bonus-pattern matching: lowercase and
|
||||
* strip common separators so "Making.Of" → "makingof". */
|
||||
function normalizeBonusSegment(segment: string): string {
|
||||
return String(segment || "").toLowerCase().replace(/[._\-\s]+/g, "");
|
||||
}
|
||||
|
||||
/** Detect if a file path lies inside a bonus/extras subdirectory of the package.
|
||||
* Walks up the path from filePath until packageDir and checks each segment. */
|
||||
@ -156,9 +165,9 @@ function isInsideBonusDir(filePath: string, packageDir: string): boolean {
|
||||
const resolvedCurrent = path.resolve(current);
|
||||
if (resolvedCurrent === root) return false;
|
||||
if (!isPathInsideDir(current, packageDir)) return false;
|
||||
const segment = path.basename(current).toLowerCase();
|
||||
for (const pattern of BONUS_DIR_PATTERNS) {
|
||||
if (segment.includes(pattern)) return true;
|
||||
const normalized = normalizeBonusSegment(path.basename(current));
|
||||
for (const pattern of BONUS_DIR_NORMALIZED_PATTERNS) {
|
||||
if (normalized.includes(pattern)) return true;
|
||||
}
|
||||
const parent = path.dirname(current);
|
||||
if (!parent || parent === current) break;
|
||||
|
||||
@ -9451,6 +9451,99 @@ describe("download manager", () => {
|
||||
expect(fs.existsSync(flattenedEpisode)).toBe(true);
|
||||
}, 20000);
|
||||
|
||||
it("detects dot-separated bonus subdirectories (Making.Of, Behind.The.Scenes)", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
const packageName = "Breaking.Bad.S05.GERMAN.DL.720p.BluRay.x264-TSCC";
|
||||
const outputDir = path.join(root, "downloads", packageName);
|
||||
const extractDir = path.join(root, "extract", packageName);
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
// Mix of dot-separated bonus subdirs - must all be detected as bonus
|
||||
const zip = new AdmZip();
|
||||
zip.addFile("Breaking.Bad.S05E01.GERMAN.DL.720p.BluRay.x264-TSCC.mkv", Buffer.from("real-episode"));
|
||||
zip.addFile("Breaking.Bad.S05.Making.Of/SomeBonusClip.mkv", Buffer.from("bonus-1"));
|
||||
zip.addFile("Breaking.Bad.S05.Behind.The.Scenes/AnotherClip.mkv", Buffer.from("bonus-2"));
|
||||
zip.addFile("Breaking.Bad.S05.Deleted.Scenes/DeletedClip.mkv", Buffer.from("bonus-3"));
|
||||
zip.addFile("Breaking.Bad.S05.Gag.Reel/GagClip.mkv", Buffer.from("bonus-4"));
|
||||
const archivePath = path.join(outputDir, "episode.zip");
|
||||
zip.writeZip(archivePath);
|
||||
const archiveSize = fs.statSync(archivePath).size;
|
||||
|
||||
const session = emptySession();
|
||||
const packageId = `${packageName}-pkg`;
|
||||
const itemId = `${packageName}-item`;
|
||||
const createdAt = Date.now() - 20_000;
|
||||
session.packageOrder = [packageId];
|
||||
session.packages[packageId] = {
|
||||
id: packageId,
|
||||
name: packageName,
|
||||
outputDir,
|
||||
extractDir,
|
||||
status: "downloading",
|
||||
itemIds: [itemId],
|
||||
cancelled: false,
|
||||
enabled: true,
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
session.items[itemId] = {
|
||||
id: itemId,
|
||||
packageId,
|
||||
url: `https://dummy/${packageName}`,
|
||||
provider: "realdebrid",
|
||||
status: "completed",
|
||||
retries: 0,
|
||||
speedBps: 0,
|
||||
downloadedBytes: archiveSize,
|
||||
totalBytes: archiveSize,
|
||||
progressPercent: 100,
|
||||
fileName: "episode.zip",
|
||||
targetPath: archivePath,
|
||||
resumable: true,
|
||||
attempts: 1,
|
||||
lastError: "",
|
||||
fullStatus: "Fertig (100 MB)",
|
||||
createdAt,
|
||||
updatedAt: createdAt
|
||||
};
|
||||
|
||||
const mkvLibraryDir = path.join(root, "mkv-library");
|
||||
|
||||
new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
token: "rd-token",
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir: path.join(root, "extract"),
|
||||
autoExtract: true,
|
||||
autoRename4sf4sj: false,
|
||||
collectMkvToLibrary: true,
|
||||
mkvLibraryDir,
|
||||
enableIntegrityCheck: false,
|
||||
cleanupMode: "none"
|
||||
},
|
||||
session,
|
||||
createStoragePaths(path.join(root, "state"))
|
||||
);
|
||||
|
||||
const flattenedEpisode = path.join(mkvLibraryDir, "Breaking.Bad.S05E01.GERMAN.DL.720p.BluRay.x264-TSCC.mkv");
|
||||
await waitFor(() => fs.existsSync(flattenedEpisode), 12000);
|
||||
|
||||
// None of the bonus files should have landed in the flat library
|
||||
expect(fs.existsSync(path.join(mkvLibraryDir, "SomeBonusClip.mkv"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(mkvLibraryDir, "AnotherClip.mkv"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(mkvLibraryDir, "DeletedClip.mkv"))).toBe(false);
|
||||
expect(fs.existsSync(path.join(mkvLibraryDir, "GagClip.mkv"))).toBe(false);
|
||||
|
||||
// All bonus files must still exist in their respective subfolders
|
||||
expect(fs.existsSync(path.join(extractDir, "Breaking.Bad.S05.Making.Of", "SomeBonusClip.mkv"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(extractDir, "Breaking.Bad.S05.Behind.The.Scenes", "AnotherClip.mkv"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(extractDir, "Breaking.Bad.S05.Deleted.Scenes", "DeletedClip.mkv"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(extractDir, "Breaking.Bad.S05.Gag.Reel", "GagClip.mkv"))).toBe(true);
|
||||
}, 20000);
|
||||
|
||||
it("keeps existing MKV names and appends a suffix while flattening", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user