diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 2229672..e30b7b6 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -5750,6 +5750,31 @@ export class DownloadManager extends EventEmitter { return count; } + private getProviderValidatingTaskCount(provider: DebridProvider, excludeItemId?: string): number { + let count = 0; + for (const active of this.activeTasks.values()) { + if (excludeItemId && active.itemId === excludeItemId) { + continue; + } + const activeItem = this.session.items[active.itemId]; + if (!activeItem || activeItem.status !== "validating") { + continue; + } + const expectedProvider = resolveMegaDebridProvider(this.settings, this.getExpectedProviderForItem(activeItem)); + if (expectedProvider === provider) { + count += 1; + } + } + return count; + } + + private getSerializedValidatingLimit(provider: DebridProvider | null): number { + if (provider === "megadebrid-web") { + return 1; + } + return Number.MAX_SAFE_INTEGER; + } + private delayPacedStartForItem(item: DownloadItem, now: number): boolean { const paceKey = this.getPacedStartKeyForItem(item); if (!paceKey) { @@ -5842,7 +5867,11 @@ export class DownloadManager extends EventEmitter { } private shouldDelayStartForItem(item: DownloadItem): boolean { - const provider = this.getExpectedProviderForItem(item); + const provider = resolveMegaDebridProvider(this.settings, this.getExpectedProviderForItem(item)); + const serializedValidatingLimit = this.getSerializedValidatingLimit(provider); + if (provider && Number.isFinite(serializedValidatingLimit) && serializedValidatingLimit < Number.MAX_SAFE_INTEGER) { + return this.getProviderValidatingTaskCount(provider, item.id) >= serializedValidatingLimit; + } if (provider !== "alldebrid") { return false; } diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index 624c77c..25a8075 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -4,7 +4,7 @@ import path from "node:path"; import http from "node:http"; import { EventEmitter, once } from "node:events"; import AdmZip from "adm-zip"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { DownloadManager, extractArchiveNameFromExtractorLogMessage, getAuthoritativeRealDebridTotal } from "../src/main/download-manager"; import { defaultSettings } from "../src/main/constants"; import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys"; @@ -5031,6 +5031,82 @@ describe("download manager", () => { } }, 20000); + it("limits Mega-Debrid Web validating starts to one item at a time", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); + tempDirs.push(root); + + let unrestrictCalls = 0; + const pendingRejectors = new Set<(error: Error) => void>(); + + const manager = new DownloadManager( + { + ...defaultSettings(), + megaLogin: "mega-user", + megaPassword: "mega-pass", + 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: 3 + }, + 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-serialized", + 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);