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:
@@ -135,8 +135,6 @@ export class RealDebridWebFallback {
|
|||||||
|
|
||||||
private onClosed?: () => void;
|
private onClosed?: () => void;
|
||||||
|
|
||||||
private interactiveLoginDismissed = false;
|
|
||||||
|
|
||||||
private persistentPartition: string;
|
private persistentPartition: string;
|
||||||
|
|
||||||
private transientPartition: string;
|
private transientPartition: string;
|
||||||
@@ -172,13 +170,12 @@ export class RealDebridWebFallback {
|
|||||||
if (initial.kind === "success") {
|
if (initial.kind === "success") {
|
||||||
return initial.value;
|
return initial.value;
|
||||||
}
|
}
|
||||||
return this.waitForLoginAndGenerate(link, overallSignal);
|
throw new Error("Real-Debrid Web-Login erforderlich");
|
||||||
}, overallSignal);
|
}, overallSignal);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openLoginWindow(): Promise<void> {
|
public async openLoginWindow(): Promise<void> {
|
||||||
this.throwIfDisposed();
|
this.throwIfDisposed();
|
||||||
this.interactiveLoginDismissed = false;
|
|
||||||
const window = await this.ensureLoginWindow();
|
const window = await this.ensureLoginWindow();
|
||||||
if (window.isMinimized()) {
|
if (window.isMinimized()) {
|
||||||
window.restore();
|
window.restore();
|
||||||
@@ -343,7 +340,6 @@ export class RealDebridWebFallback {
|
|||||||
let closingTokenProbe: Promise<void> = Promise.resolve();
|
let closingTokenProbe: Promise<void> = Promise.resolve();
|
||||||
window.on("close", () => {
|
window.on("close", () => {
|
||||||
if (!this.programmaticClosures.has(window)) {
|
if (!this.programmaticClosures.has(window)) {
|
||||||
this.interactiveLoginDismissed = true;
|
|
||||||
closingTokenProbe = this.primeTokenFromWindow(window, windowGeneration);
|
closingTokenProbe = this.primeTokenFromWindow(window, windowGeneration);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -376,7 +372,6 @@ export class RealDebridWebFallback {
|
|||||||
const changed = token !== this.cachedToken;
|
const changed = token !== this.cachedToken;
|
||||||
this.cachedToken = token;
|
this.cachedToken = token;
|
||||||
this.cachedTokenAt = Date.now();
|
this.cachedTokenAt = Date.now();
|
||||||
this.interactiveLoginDismissed = false;
|
|
||||||
if (changed && this.onAuthenticated) {
|
if (changed && this.onAuthenticated) {
|
||||||
void Promise.resolve().then(() => this.onAuthenticated?.());
|
void Promise.resolve().then(() => this.onAuthenticated?.());
|
||||||
}
|
}
|
||||||
@@ -586,36 +581,4 @@ export class RealDebridWebFallback {
|
|||||||
throw new Error("Real-Debrid Web: Unrestrict fehlgeschlagen");
|
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -166,6 +166,45 @@ describe("realdebrid-web", () => {
|
|||||||
expect(mockBrowserWindow.webContents.executeJavaScript).toHaveBeenCalled();
|
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 () => {
|
it("checks the logged-in browser account without exposing its token", async () => {
|
||||||
mockExecuteJavaScript.mockResolvedValue("token-from-window");
|
mockExecuteJavaScript.mockResolvedValue("token-from-window");
|
||||||
const apiFetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({
|
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("");
|
mockExecuteJavaScript.mockResolvedValue("");
|
||||||
mockSessionFetch.mockImplementation(async () => new Response("<html>login</html>", { status: 200 }));
|
mockSessionFetch.mockImplementation(async () => new Response("<html>login</html>", { status: 200 }));
|
||||||
const fallback = new RealDebridWebFallback("persist:realdebrid-web-rdw_dismissed", () => true);
|
const fallback = new RealDebridWebFallback("persist:realdebrid-web-rdw_dismissed", () => true);
|
||||||
@@ -205,8 +244,8 @@ describe("realdebrid-web", () => {
|
|||||||
await fallback.openLoginWindow();
|
await fallback.openLoginWindow();
|
||||||
mockBrowserWindow.close();
|
mockBrowserWindow.close();
|
||||||
|
|
||||||
await expect(fallback.unrestrict("https://rapidgator.net/file/first")).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("abgebrochen");
|
await expect(fallback.unrestrict("https://rapidgator.net/file/second")).rejects.toThrow("Login erforderlich");
|
||||||
expect(mockBrowserWindowCtor).toHaveBeenCalledTimes(1);
|
expect(mockBrowserWindowCtor).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user