diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 76ec56c..6a219bd 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -52,7 +52,7 @@ function releaseTlsSkip(): void { } import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup"; import { planDownloadCompletion, validateDownloadedFileCompletion } from "./download-completion"; -import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState } from "./debrid"; +import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, getMegaDebridAccountCooldownState, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState } from "./debrid"; import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, detectArchiveSignature, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree, resetExtractorCachesForPasswordChange, type ExtractArchiveFailureInfo } from "./extractor"; import { validateFileAgainstManifest } from "./integrity"; import { classifyDiskError } from "./fs-error"; @@ -7981,7 +7981,10 @@ export class DownloadManager extends EventEmitter { private getSerializedValidatingLimit(provider: DebridProvider | null): number { if (provider === "megadebrid-web") { - return Math.max(1, getAvailableMegaDebridAccounts(this.settings).length); + const usableAccounts = getAvailableMegaDebridAccounts(this.settings) + .filter((account) => !getMegaDebridAccountCooldownState(`${account.id}:web`)) + .length; + return Math.max(1, usableAccounts); } return Number.MAX_SAFE_INTEGER; } diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index 0de8171..e6290e7 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -14,7 +14,8 @@ import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits"; import { getItemLogPath, initItemLogs, shutdownItemLogs } from "../src/main/item-log"; import { initPackageLogs, shutdownPackageLogs } from "../src/main/package-log"; import { createStoragePaths, emptySession } from "../src/main/storage"; -import { primeDebridLinkRuntimeCooldownForTests, resetDebridLinkRuntimeStateForTests } from "../src/main/debrid"; +import { primeDebridLinkRuntimeCooldownForTests, resetDebridLinkRuntimeStateForTests, primeMegaDebridRuntimeCooldownForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid"; +import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts"; import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "../src/main/rename-log"; import { UnrestrictedLink } from "../src/main/realdebrid"; @@ -155,6 +156,7 @@ async function removeDirWithRetries(dir: string): Promise { afterEach(async () => { globalThis.fetch = originalFetch; resetDebridLinkRuntimeStateForTests(); + resetMegaDebridRuntimeStateForTests(); shutdownItemLogs(); shutdownPackageLogs(); shutdownRenameLog(); @@ -6642,6 +6644,83 @@ describe("download manager", () => { await new Promise((resolve) => setTimeout(resolve, 150)); }); + it("does not over-admit Mega-Debrid Web validations when an account is in cooldown", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); + tempDirs.push(root); + + let unrestrictCalls = 0; + const pendingRejectors = new Set<(error: Error) => void>(); + + primeMegaDebridRuntimeCooldownForTests(`${getMegaDebridAccountId("mega-user-b")}:web`, 120000); + + const manager = new DownloadManager( + { + ...defaultSettings(), + megaCredentials: "mega-user-a:pass-a\nmega-user-b:pass-b", + megaDebridWebEnabled: true, + megaDebridApiEnabled: false, + megaDebridPreferApi: false, + providerOrder: [], + providerPrimary: "megadebrid", + providerSecondary: "none", + providerTertiary: "none", + outputDir: path.join(root, "downloads"), + extractDir: path.join(root, "extract"), + autoExtract: false, + autoReconnect: false, + enableIntegrityCheck: false, + maxParallel: 4 + }, + emptySession(), + createStoragePaths(path.join(root, "state")), + { + megaWebUnrestrict: vi.fn(async (_link: string, signal?: AbortSignal) => { + unrestrictCalls += 1; + return await new Promise((resolve, reject) => { + const rejector = (error: Error): void => { + signal?.removeEventListener("abort", onAbort); + pendingRejectors.delete(rejector); + reject(error); + }; + const onAbort = (): void => { + rejector(new Error("aborted:test-mega-web")); + }; + if (signal?.aborted) { + onAbort(); + return; + } + signal?.addEventListener("abort", onAbort, { once: true }); + pendingRejectors.add(rejector); + }); + }) + } + ); + + manager.addPackages([{ + name: "mega-web-cooldown", + links: [ + "https://rapidgator.net/file/mega-web-1.part1.rar.html", + "https://rapidgator.net/file/mega-web-2.part2.rar.html", + "https://rapidgator.net/file/mega-web-3.part3.rar.html" + ] + }]); + + await manager.start(); + await waitFor(() => unrestrictCalls === 1, 10000); + await new Promise((resolve) => setTimeout(resolve, 250)); + + const items = Object.values(manager.getSnapshot().session.items); + expect(items.filter((item) => item.status === "validating")).toHaveLength(1); + expect(items.filter((item) => item.status === "queued")).toHaveLength(2); + expect(unrestrictCalls).toBe(1); + + manager.stop(); + for (const reject of Array.from(pendingRejectors)) { + reject(new Error("aborted:test-mega-web")); + } + await new Promise((resolve) => setTimeout(resolve, 150)); + }); + it("shows the same AllDebrid countdown for all immediately free slots", async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); tempDirs.push(root);