fix(downloads): restore actionable package states and extraction controls

Resolve child archive selections to complete multipart sets, support bulk package extraction, refresh extraction passwords at execution time, and keep archive failures scoped by their full paths.

Restore sortable download columns, preserve verified availability, classify package retry and extraction states, secure link copying through the preload bridge, and release cancelled provider work without stale cooldowns.
This commit is contained in:
Sucukdeluxe
2026-08-22 22:52:49 +02:00
parent a5920869a3
commit 55b8911e94
33 changed files with 2187 additions and 561 deletions
+55 -5
View File
@@ -346,7 +346,7 @@ describe("mega-web-fallback", () => {
expect(maxActiveLogins).toBe(2);
}, 10000);
it("aborts pending Mega-Web polling when signal is cancelled", async () => {
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);
@@ -394,10 +394,60 @@ describe("mega-web-fallback", () => {
await expect(fallback.unrestrict("https://mega.debrid/link2", controller.signal)).rejects.toThrow(/aborted/i);
} finally {
clearTimeout(timer);
}
});
it("klassifiziert einen Abbruch WAEHREND in der Queue als Queue-Timeout (nicht harter Abbruch), damit der belegte Account nicht bestraft wird", async () => {
}
});
it("starts an already queued account job after caller abort even when the old raw job ignores its signal", async () => {
let releaseFirstLogin: () => void = () => {};
let markFirstLoginStarted: () => void = () => {};
const firstLoginGate = new Promise<void>((resolve) => {
releaseFirstLogin = resolve;
});
const firstLoginStarted = new Promise<void>((resolve) => {
markFirstLoginStarted = resolve;
});
const fallback = new MegaWebFallback(() => ({ login: "same", password: "pw" }));
const internals = fallback as unknown as {
login: (login: string, password: string) => Promise<string>;
generate: (link: string, cookie: string) => Promise<{ directUrl: string; fileName: string }>;
sessions: Map<string, { cookie: string; setAt: number }>;
};
let loginCount = 0;
vi.spyOn(internals, "login").mockImplementation(async () => {
loginCount += 1;
if (loginCount === 1) {
markFirstLoginStarted();
await firstLoginGate;
return "stale-cookie";
}
return "fresh-cookie";
});
vi.spyOn(internals, "generate").mockImplementation(async (link, cookie) => ({
directUrl: `https://mega.direct/${cookie}/${link.endsWith("second") ? "second" : "first"}`,
fileName: "result.bin"
}));
const firstController = new AbortController();
const first = fallback.unrestrict("https://mega.debrid/first", firstController.signal, { login: "same", password: "pw" });
await firstLoginStarted;
const second = fallback.unrestrict("https://mega.debrid/second", undefined, { login: "same", password: "pw" });
firstController.abort("stop");
await expect(first).rejects.toThrow(/aborted/i);
try {
const outcome = await Promise.race([
second.then((result) => result?.directUrl || "missing"),
new Promise<string>((resolve) => setTimeout(() => resolve("blocked"), 150))
]);
expect(outcome).toBe("https://mega.direct/fresh-cookie/second");
} finally {
releaseFirstLogin();
}
await new Promise((resolve) => setTimeout(resolve, 20));
expect(internals.sessions.get("same")?.cookie).toBe("fresh-cookie");
});
it("klassifiziert einen Abbruch WAEHREND in der Queue als Queue-Timeout (nicht harter Abbruch), damit der belegte Account nicht bestraft wird", async () => {
let releaseLogin: () => void = () => {};
const loginGate = new Promise<void>((resolve) => { releaseLogin = resolve; });
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {