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.
This commit is contained in:
Sucukdeluxe
2026-08-15 22:00:48 +02:00
parent 935f39d7db
commit c6fb8d928e
2 changed files with 44 additions and 42 deletions
+1 -38
View File
@@ -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<void> {
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<void> = 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<UnrestrictedLink | null> {
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");
}
}
+42 -3
View File
@@ -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("<html>login</html>", { 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("<input name=\"private_token\" value=\"session-token\">", { 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("<html>login</html>", { 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);
});