From 2bddd5b3b20c72e6c21954a6766741a0a3e6c0e0 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sun, 1 Mar 2026 04:26:33 +0100 Subject: [PATCH] Apply configurable retry limit and clean empty extract dirs more aggressively --- src/main/download-manager.ts | 29 ++++++++++-- tests/download-manager.test.ts | 82 ++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 2825ca8..28d35ef 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -258,6 +258,16 @@ function isPathInsideDir(filePath: string, dirPath: string): boolean { return file.startsWith(withSep); } +const EMPTY_DIR_IGNORED_FILE_NAMES = new Set([ + "thumbs.db", + "desktop.ini", + ".ds_store" +]); + +function isIgnorableEmptyDirFileName(fileName: string): boolean { + return EMPTY_DIR_IGNORED_FILE_NAMES.has(String(fileName || "").trim().toLowerCase()); +} + function toWindowsLongPathIfNeeded(filePath: string): string { const absolute = path.resolve(String(filePath || "")); if (process.platform !== "win32") { @@ -1510,7 +1520,19 @@ export class DownloadManager extends EventEmitter { let removed = 0; for (const dirPath of dirs) { try { - const entries = fs.readdirSync(dirPath); + let entries = fs.readdirSync(dirPath, { withFileTypes: true }); + for (const entry of entries) { + if (!entry.isFile() || !isIgnorableEmptyDirFileName(entry.name)) { + continue; + } + try { + fs.rmSync(path.join(dirPath, entry.name), { force: true }); + } catch { + // ignore and keep directory untouched + } + } + + entries = fs.readdirSync(dirPath, { withFileTypes: true }); if (entries.length === 0) { fs.rmdirSync(dirPath); removed += 1; @@ -3753,7 +3775,8 @@ export class DownloadManager extends EventEmitter { private recoverRetryableItems(trigger: "startup" | "start"): number { let recovered = 0; const touchedPackages = new Set(); - const maxAutoRetryFailures = Math.max(2, REQUEST_RETRIES); + const configuredRetryLimit = normalizeRetryLimit(this.settings.retryLimit); + const maxAutoRetryFailures = retryLimitToMaxRetries(configuredRetryLimit); for (const packageId of this.session.packageOrder) { const pkg = this.session.packages[packageId]; @@ -3788,7 +3811,7 @@ export class DownloadManager extends EventEmitter { } if (item.status === "completed" && hasZeroByteArchive) { - const maxCompletedZeroByteAutoRetries = Math.max(2, REQUEST_RETRIES); + const maxCompletedZeroByteAutoRetries = retryLimitToMaxRetries(configuredRetryLimit); if (item.retries >= maxCompletedZeroByteAutoRetries) { continue; } diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index c2e8a95..9ee3db7 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -4070,6 +4070,88 @@ describe("download manager", () => { expect(fs.existsSync(suffixedPath)).toBe(true); }); + it("removes empty package folders after MKV flattening even with desktop.ini or thumbs.db", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); + tempDirs.push(root); + + const packageName = "Gotham.S03.GERMAN.5.1.DL.AC3.720p.BDRiP.x264-TvR"; + const outputDir = path.join(root, "downloads", packageName); + const extractDir = path.join(root, "extract", packageName); + fs.mkdirSync(outputDir, { recursive: true }); + + const nestedFolder = "Gotham.S03E11.Ein.Ungeheuer.namens.Eifersucht.GERMAN.5.1.DL.AC3.720p.BDRiP.x264-TvR"; + const sourceFileName = `${nestedFolder}/tvr-gotham-s03e11-720p.mkv`; + const zip = new AdmZip(); + zip.addFile(sourceFileName, Buffer.from("video")); + zip.addFile(`${nestedFolder}/Thumbs.db`, Buffer.from("thumbs")); + zip.addFile("desktop.ini", Buffer.from("system")); + 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/gotham", + 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", + 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 flattenedPath = path.join(mkvLibraryDir, "tvr-gotham-s03e11-720p.mkv"); + await waitFor(() => fs.existsSync(flattenedPath), 12000); + + expect(fs.existsSync(flattenedPath)).toBe(true); + expect(fs.existsSync(extractDir)).toBe(false); + }); + it("throws a controlled error for invalid queue import JSON", () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); tempDirs.push(root);