Fix Mega-Web unrestrict hangs and release v1.4.66
Some checks are pending
Build and Release / build (push) Waiting to run
Some checks are pending
Build and Release / build (push) Waiting to run
This commit is contained in:
parent
237bf6731d
commit
647679f581
18
CHANGELOG.md
18
CHANGELOG.md
@ -2,6 +2,24 @@
|
||||
|
||||
Alle nennenswerten Aenderungen werden in dieser Datei dokumentiert.
|
||||
|
||||
## 1.4.66 - 2026-03-01
|
||||
|
||||
Hotfix fuer haengende "Link wird umgewandelt"-Faelle (insbesondere Mega-Web-Pfad), bei denen nur ein App-Neustart geholfen hat.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Mega-Web-Unrestrict ist jetzt komplett abort-/timeout-faehig:
|
||||
- Abort-Signale werden bis in den Mega-Web-Fallback durchgereicht.
|
||||
- Laufende Polling-/Fetch-Schritte respektieren Stop/Timeout sofort.
|
||||
- Wartende Jobs in der exklusiven Mega-Web-Queue koennen bei Abort sauber abbrechen.
|
||||
- Download-Manager kann haengende Unrestrict-Phasen dadurch wieder automatisch per Timeout + Retry aufloesen, statt dauerhaft in "Link wird umgewandelt" zu bleiben.
|
||||
|
||||
### Tests
|
||||
|
||||
- Neue Tests sichern den Fix ab:
|
||||
- Abort-Weitergabe bei Mega-Web-Unrestrict in `tests/debrid.test.ts`.
|
||||
- Abort waehrend Mega-Web-Polling in `tests/mega-web-fallback.test.ts`.
|
||||
|
||||
## 1.4.33 - 2026-03-02
|
||||
|
||||
Hotfix-Release fuer zwei reale Produktionsprobleme: falsche Gesamt-Statistik bei leerer Queue und stilles DLC-Import-Failure bei Drag-and-Drop.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.4.33",
|
||||
"version": "1.4.66",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.4.33",
|
||||
"version": "1.4.66",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.4.65",
|
||||
"version": "1.4.66",
|
||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
|
||||
@ -57,7 +57,7 @@ export class AppController {
|
||||
password: this.settings.megaPassword
|
||||
}));
|
||||
this.manager = new DownloadManager(this.settings, session, this.storagePaths, {
|
||||
megaWebUnrestrict: (link: string) => this.megaWebFallback.unrestrict(link)
|
||||
megaWebUnrestrict: (link: string, signal?: AbortSignal) => this.megaWebFallback.unrestrict(link, signal)
|
||||
});
|
||||
this.manager.on("state", (snapshot: UiSnapshot) => {
|
||||
this.onStateHandler?.(snapshot);
|
||||
|
||||
@ -23,7 +23,7 @@ interface ProviderUnrestrictedLink extends UnrestrictedLink {
|
||||
providerLabel: string;
|
||||
}
|
||||
|
||||
export type MegaWebUnrestrictor = (link: string) => Promise<UnrestrictedLink | null>;
|
||||
export type MegaWebUnrestrictor = (link: string, signal?: AbortSignal) => Promise<UnrestrictedLink | null>;
|
||||
|
||||
interface DebridServiceOptions {
|
||||
megaWebUnrestrict?: MegaWebUnrestrictor;
|
||||
@ -488,7 +488,7 @@ class MegaDebridClient {
|
||||
if (signal?.aborted) {
|
||||
throw new Error("aborted:debrid");
|
||||
}
|
||||
const web = await this.megaWebUnrestrict(link).catch((error) => {
|
||||
const web = await this.megaWebUnrestrict(link, signal).catch((error) => {
|
||||
lastError = compactErrorText(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
@ -88,6 +88,93 @@ function parseDebridJson(text: string): { link: string; text: string } | null {
|
||||
}
|
||||
}
|
||||
|
||||
function abortError(): Error {
|
||||
return new Error("aborted:mega-web");
|
||||
}
|
||||
|
||||
function withTimeoutSignal(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal {
|
||||
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
||||
if (!signal) {
|
||||
return timeoutSignal;
|
||||
}
|
||||
return AbortSignal.any([signal, timeoutSignal]);
|
||||
}
|
||||
|
||||
function throwIfAborted(signal?: AbortSignal): void {
|
||||
if (signal?.aborted) {
|
||||
throw abortError();
|
||||
}
|
||||
}
|
||||
|
||||
async function sleepWithSignal(ms: number, signal?: AbortSignal): Promise<void> {
|
||||
if (!signal) {
|
||||
await sleep(ms);
|
||||
return;
|
||||
}
|
||||
if (signal.aborted) {
|
||||
throw abortError();
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let timer: NodeJS.Timeout | null = setTimeout(() => {
|
||||
timer = null;
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
resolve();
|
||||
}, Math.max(0, ms));
|
||||
|
||||
const onAbort = (): void => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
reject(abortError());
|
||||
};
|
||||
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Promise<T> {
|
||||
if (!signal) {
|
||||
return promise;
|
||||
}
|
||||
if (signal.aborted) {
|
||||
throw abortError();
|
||||
}
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
let settled = false;
|
||||
|
||||
const onAbort = (): void => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
reject(abortError());
|
||||
};
|
||||
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
|
||||
promise.then((value) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
resolve(value);
|
||||
}, (error) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export class MegaWebFallback {
|
||||
private queue: Promise<unknown> = Promise.resolve();
|
||||
|
||||
@ -101,22 +188,23 @@ export class MegaWebFallback {
|
||||
this.getCredentials = getCredentials;
|
||||
}
|
||||
|
||||
public async unrestrict(link: string): Promise<UnrestrictedLink | null> {
|
||||
public async unrestrict(link: string, signal?: AbortSignal): Promise<UnrestrictedLink | null> {
|
||||
return this.runExclusive(async () => {
|
||||
throwIfAborted(signal);
|
||||
const creds = this.getCredentials();
|
||||
if (!creds.login.trim() || !creds.password.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.cookie || Date.now() - this.cookieSetAt > 20 * 60 * 1000) {
|
||||
await this.login(creds.login, creds.password);
|
||||
await this.login(creds.login, creds.password, signal);
|
||||
}
|
||||
|
||||
const generated = await this.generate(link);
|
||||
const generated = await this.generate(link, signal);
|
||||
if (!generated) {
|
||||
this.cookie = "";
|
||||
await this.login(creds.login, creds.password);
|
||||
const retry = await this.generate(link);
|
||||
await this.login(creds.login, creds.password, signal);
|
||||
const retry = await this.generate(link, signal);
|
||||
if (!retry) {
|
||||
return null;
|
||||
}
|
||||
@ -134,16 +222,21 @@ export class MegaWebFallback {
|
||||
fileSize: null,
|
||||
retriesUsed: 0
|
||||
};
|
||||
});
|
||||
}, signal);
|
||||
}
|
||||
|
||||
private async runExclusive<T>(job: () => Promise<T>): Promise<T> {
|
||||
const run = this.queue.then(job, job);
|
||||
private async runExclusive<T>(job: () => Promise<T>, signal?: AbortSignal): Promise<T> {
|
||||
const guardedJob = async (): Promise<T> => {
|
||||
throwIfAborted(signal);
|
||||
return job();
|
||||
};
|
||||
const run = this.queue.then(guardedJob, guardedJob);
|
||||
this.queue = run.then(() => undefined, () => undefined);
|
||||
return run;
|
||||
return raceWithAbort(run, signal);
|
||||
}
|
||||
|
||||
private async login(login: string, password: string): Promise<void> {
|
||||
private async login(login: string, password: string, signal?: AbortSignal): Promise<void> {
|
||||
throwIfAborted(signal);
|
||||
const response = await fetch(LOGIN_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -156,7 +249,7 @@ export class MegaWebFallback {
|
||||
remember: "on"
|
||||
}),
|
||||
redirect: "manual",
|
||||
signal: AbortSignal.timeout(30000)
|
||||
signal: withTimeoutSignal(signal, 30000)
|
||||
});
|
||||
|
||||
const cookie = parseSetCookieFromHeaders(response.headers);
|
||||
@ -171,7 +264,7 @@ export class MegaWebFallback {
|
||||
Cookie: cookie,
|
||||
Referer: DEBRID_REFERER
|
||||
},
|
||||
signal: AbortSignal.timeout(30000)
|
||||
signal: withTimeoutSignal(signal, 30000)
|
||||
});
|
||||
const verifyHtml = await verify.text();
|
||||
const hasDebridForm = /id=["']debridForm["']/i.test(verifyHtml) || /name=["']links["']/i.test(verifyHtml);
|
||||
@ -183,7 +276,8 @@ export class MegaWebFallback {
|
||||
this.cookieSetAt = Date.now();
|
||||
}
|
||||
|
||||
private async generate(link: string): Promise<{ directUrl: string; fileName: string } | null> {
|
||||
private async generate(link: string, signal?: AbortSignal): Promise<{ directUrl: string; fileName: string } | null> {
|
||||
throwIfAborted(signal);
|
||||
const page = await fetch(DEBRID_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -197,7 +291,7 @@ export class MegaWebFallback {
|
||||
password: "",
|
||||
showLinks: "1"
|
||||
}),
|
||||
signal: AbortSignal.timeout(30000)
|
||||
signal: withTimeoutSignal(signal, 30000)
|
||||
});
|
||||
|
||||
const html = await page.text();
|
||||
@ -207,6 +301,7 @@ export class MegaWebFallback {
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= 60; attempt += 1) {
|
||||
throwIfAborted(signal);
|
||||
const res = await fetch(DEBRID_AJAX_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@ -219,12 +314,12 @@ export class MegaWebFallback {
|
||||
code,
|
||||
autodl: "0"
|
||||
}),
|
||||
signal: AbortSignal.timeout(15000)
|
||||
signal: withTimeoutSignal(signal, 15000)
|
||||
});
|
||||
|
||||
const text = (await res.text()).trim();
|
||||
if (text === "reload") {
|
||||
await sleep(650);
|
||||
await sleepWithSignal(650, signal);
|
||||
continue;
|
||||
}
|
||||
if (text === "false") {
|
||||
@ -238,7 +333,7 @@ export class MegaWebFallback {
|
||||
|
||||
if (!parsed.link) {
|
||||
if (/hoster does not respond correctly|could not be done for this moment/i.test(parsed.text || "")) {
|
||||
await sleep(1200);
|
||||
await sleepWithSignal(1200, signal);
|
||||
continue;
|
||||
}
|
||||
return null;
|
||||
|
||||
@ -290,6 +290,44 @@ describe("debrid service", () => {
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("aborts Mega web unrestrict when caller signal is cancelled", async () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
token: "",
|
||||
bestToken: "",
|
||||
allDebridToken: "",
|
||||
megaLogin: "user",
|
||||
megaPassword: "pass",
|
||||
providerPrimary: "megadebrid" as const,
|
||||
providerSecondary: "none" as const,
|
||||
providerTertiary: "none" as const,
|
||||
autoProviderFallback: false
|
||||
};
|
||||
|
||||
const megaWeb = vi.fn((_link: string, signal?: AbortSignal): Promise<never> => new Promise((_, reject) => {
|
||||
const onAbort = (): void => reject(new Error("aborted:mega-web-test"));
|
||||
if (signal?.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
}));
|
||||
|
||||
const service = new DebridService(settings, { megaWebUnrestrict: megaWeb });
|
||||
const controller = new AbortController();
|
||||
const abortTimer = setTimeout(() => {
|
||||
controller.abort("test");
|
||||
}, 25);
|
||||
|
||||
try {
|
||||
await expect(service.unrestrictLink("https://rapidgator.net/file/abort-mega-web", controller.signal)).rejects.toThrow(/aborted/i);
|
||||
expect(megaWeb).toHaveBeenCalledTimes(1);
|
||||
expect(megaWeb.mock.calls[0]?.[1]).toBe(controller.signal);
|
||||
} finally {
|
||||
clearTimeout(abortTimer);
|
||||
}
|
||||
});
|
||||
|
||||
it("respects provider selection and does not append hidden providers", async () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
|
||||
@ -123,5 +123,56 @@ describe("mega-web-fallback", () => {
|
||||
// Generation fails -> resets cookie -> tries again -> fails again -> returns null
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("aborts pending Mega-Web polling when signal is cancelled", async () => {
|
||||
globalThis.fetch = vi.fn((url: string | URL | Request, init?: RequestInit): Promise<Response> => {
|
||||
const urlStr = String(url);
|
||||
|
||||
if (urlStr.includes("form=login")) {
|
||||
const headers = new Headers();
|
||||
headers.append("set-cookie", "session=goodcookie; path=/");
|
||||
return Promise.resolve(new Response("", { headers, status: 200 }));
|
||||
}
|
||||
|
||||
if (urlStr.includes("page=debrideur")) {
|
||||
return Promise.resolve(new Response('<form id="debridForm"></form>', { status: 200 }));
|
||||
}
|
||||
|
||||
if (urlStr.includes("form=debrid")) {
|
||||
return Promise.resolve(new Response(`
|
||||
<div class="acp-box">
|
||||
<h3>Link: https://mega.debrid/link2</h3>
|
||||
<a href="javascript:processDebrid(1,'secretcode456',0)">Download</a>
|
||||
</div>
|
||||
`, { status: 200 }));
|
||||
}
|
||||
|
||||
if (urlStr.includes("ajax=debrid")) {
|
||||
return new Promise<Response>((_resolve, reject) => {
|
||||
const signal = init?.signal;
|
||||
const onAbort = (): void => reject(new Error("aborted:ajax"));
|
||||
if (signal?.aborted) {
|
||||
onAbort();
|
||||
return;
|
||||
}
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve(new Response("Not found", { status: 404 }));
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const fallback = new MegaWebFallback(() => ({ login: "user", password: "pwd" }));
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => {
|
||||
controller.abort("test");
|
||||
}, 30);
|
||||
|
||||
try {
|
||||
await expect(fallback.unrestrict("https://mega.debrid/link2", controller.signal)).rejects.toThrow(/aborted/i);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user