From c6fb8d928ea8b7976017b0118cc7d9f26e5b7357 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sat, 15 Aug 2026 22:00:48 +0200 Subject: [PATCH] fix(realdebrid): keep background downloads noninteractive Restrict browser window creation to explicit login actions so account activation, status probes, and download retries never open or reopen Real-Debrid windows. Remove the automatic login polling loop from web unrestrict and return a clear login-required error instead. Cover concurrent fresh fallback instances, manually closed windows, and authenticated fair-use failures without popup creation. --- src/main/realdebrid-web.ts | 41 ++------------------------------ tests/realdebrid-web.test.ts | 45 +++++++++++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/src/main/realdebrid-web.ts b/src/main/realdebrid-web.ts index a78b26e..edbab9f 100644 --- a/src/main/realdebrid-web.ts +++ b/src/main/realdebrid-web.ts @@ -135,8 +135,6 @@ export class RealDebridWebFallback { private onClosed?: () => void; - private interactiveLoginDismissed = false; - private persistentPartition: string; private transientPartition: string; @@ -172,13 +170,12 @@ export class RealDebridWebFallback { if (initial.kind === "success") { return initial.value; } - return this.waitForLoginAndGenerate(link, overallSignal); + throw new Error("Real-Debrid Web-Login erforderlich"); }, overallSignal); } public async openLoginWindow(): Promise { this.throwIfDisposed(); - this.interactiveLoginDismissed = false; const window = await this.ensureLoginWindow(); if (window.isMinimized()) { window.restore(); @@ -343,7 +340,6 @@ export class RealDebridWebFallback { let closingTokenProbe: Promise = Promise.resolve(); window.on("close", () => { if (!this.programmaticClosures.has(window)) { - this.interactiveLoginDismissed = true; closingTokenProbe = this.primeTokenFromWindow(window, windowGeneration); } }); @@ -376,7 +372,6 @@ export class RealDebridWebFallback { const changed = token !== this.cachedToken; this.cachedToken = token; this.cachedTokenAt = Date.now(); - this.interactiveLoginDismissed = false; if (changed && this.onAuthenticated) { void Promise.resolve().then(() => this.onAuthenticated?.()); } @@ -586,36 +581,4 @@ export class RealDebridWebFallback { throw new Error("Real-Debrid Web: Unrestrict fehlgeschlagen"); } - private async waitForLoginAndGenerate(link: string, signal?: AbortSignal): Promise { - if (this.interactiveLoginDismissed) { - throw new Error("Real-Debrid Web-Login abgebrochen"); - } - const window = await this.ensureLoginWindow(); - if (window.isMinimized()) { - window.restore(); - } - window.show(); - window.focus(); - - const startedAt = Date.now(); - while (Date.now() - startedAt < 10 * 60 * 1000) { - throwIfAborted(signal); - if (window.isDestroyed()) { - throw new Error("Real-Debrid Web-Login abgebrochen"); - } - - const outcome = await this.generate(link, signal); - if (outcome.kind === "success") { - if (!window.isDestroyed()) { - this.programmaticClosures.add(window); - window.close(); - } - return outcome.value; - } - - await sleepWithSignal(1_500, signal); - } - - throw new Error("Real-Debrid Web-Login Timeout"); - } -} +} diff --git a/tests/realdebrid-web.test.ts b/tests/realdebrid-web.test.ts index 4895cc5..49744ad 100644 --- a/tests/realdebrid-web.test.ts +++ b/tests/realdebrid-web.test.ts @@ -166,6 +166,45 @@ describe("realdebrid-web", () => { expect(mockBrowserWindow.webContents.executeJavaScript).toHaveBeenCalled(); }); + it("never opens a login window from a background unrestrict request", async () => { + mockSessionFetch.mockImplementation(async () => new Response("login", { status: 200 })); + const first = new RealDebridWebFallback("persist:realdebrid-web-rdw_background_first", () => true); + const second = new RealDebridWebFallback("persist:realdebrid-web-rdw_background_second", () => true); + + const firstController = new AbortController(); + const secondController = new AbortController(); + const abortTimer = setTimeout(() => { + firstController.abort(); + secondController.abort(); + }, 50); + try { + await Promise.all([ + expect(first.unrestrict("https://rapidgator.net/file/background-first", firstController.signal)) + .rejects.toThrow("Login erforderlich"), + expect(second.unrestrict("https://rapidgator.net/file/background-second", secondController.signal)) + .rejects.toThrow("Login erforderlich") + ]); + } finally { + clearTimeout(abortTimer); + } + + expect(mockBrowserWindowCtor).not.toHaveBeenCalled(); + }); + + it("does not open a login window for an authenticated account with a fair-use error", async () => { + mockSessionFetch.mockResolvedValue(new Response("", { status: 200 })); + vi.stubGlobal("fetch", vi.fn().mockImplementation(async () => new Response(JSON.stringify({ + error: "fair_usage_limit", + error_code: 36 + }), { status: 440 }))); + const fallback = new RealDebridWebFallback("persist:realdebrid-web-rdw_limited", () => true); + + await expect(fallback.unrestrict("https://rapidgator.net/file/limited")) + .rejects.toThrow("Real-Debrid Web HTTP 440"); + + expect(mockBrowserWindowCtor).not.toHaveBeenCalled(); + }); + it("checks the logged-in browser account without exposing its token", async () => { mockExecuteJavaScript.mockResolvedValue("token-from-window"); const apiFetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({ @@ -197,7 +236,7 @@ describe("realdebrid-web", () => { ); }); - it("does not reopen an automatically required login after the user closed it", async () => { + it("does not reopen a login window from downloads after the user closed it", async () => { mockExecuteJavaScript.mockResolvedValue(""); mockSessionFetch.mockImplementation(async () => new Response("login", { status: 200 })); const fallback = new RealDebridWebFallback("persist:realdebrid-web-rdw_dismissed", () => true); @@ -205,8 +244,8 @@ describe("realdebrid-web", () => { await fallback.openLoginWindow(); mockBrowserWindow.close(); - await expect(fallback.unrestrict("https://rapidgator.net/file/first")).rejects.toThrow("abgebrochen"); - await expect(fallback.unrestrict("https://rapidgator.net/file/second")).rejects.toThrow("abgebrochen"); + await expect(fallback.unrestrict("https://rapidgator.net/file/first")).rejects.toThrow("Login erforderlich"); + await expect(fallback.unrestrict("https://rapidgator.net/file/second")).rejects.toThrow("Login erforderlich"); expect(mockBrowserWindowCtor).toHaveBeenCalledTimes(1); });