Fix: Mega-Debrid-Umwandlung robust + parallel — schneller Retry, alle Accounts, sichtbarer Grund

R3 (Kern): Transiente Resolve-Fehler ("Datei beim Hoster gerade nicht
abrufbar", frz. "Fichier supprimé") werden jetzt mit kurzem Delay
(3–6–10s) neu versucht statt mit dem 5s..120s-Exponential. Ein solcher
Einzel-Fehler zählt nicht mehr zum Provider-Circuit-Breaker (der bei 20
Fehlern 30–300s Cooldown setzt und die Mega-Session invalidiert) und
löst keine Provider-Cooldown-Inflation aus — dadurch bremst er die
gesunden Links nicht mehr aus. Neuer Branch vor der generischen
Unrestrict-Retry-Logik; Detektor isMegaDebridTransientResolveFailure
matcht sowohl die rohe französische als auch die gerenderte deutsche
Meldung (am Download-Manager kommt die klassifizierte deutsche an).

R2 (parallel): getSerializedValidatingLimit für megadebrid-web ist nicht
mehr hart 1, sondern die Anzahl nutzbarer Accounts. Der harte Wert 1
stammte aus der Einzel-Account-Zeit (Commit a3e3d6f). Sicher, weil die
Web-Ebene ohnehin pro Account serialisiert (runExclusive, key=login) und
die Account-Rotation gleichzeitige Aufrufe atomar auf distinkte Accounts
verteilt (megaDebridInFlight wird synchron vor dem await erhöht). Ein
Account → Limit 1 (keine Änderung).

R4 (Sichtbarkeit): Der echte Grund steht inline im Status und als Tooltip
(item.lastError); kaputten "?"-Separator zu "(Rn)" gesäubert.

UI-Bugfixes: Account-Duplizierung beim Entfernen (React-Key-Kollision
API/Web → rowKey nach kind), Username-Spalte zeigt eingegebenen Login
statt Anbieter-Mail (Mail als Tooltip).

Tests: transientResolveRetryDelayMs, isMegaDebridTransientResolveFailure,
2 behaviorale Download-Manager-Tests (schneller Re-Try mit deutschem
Grund; 1 Web-Umwandlung pro Account parallel). 833/833 grün, tsc
unverändert.
This commit is contained in:
Sucukdeluxe
2026-06-17 02:07:25 +02:00
parent bec119583d
commit 6d4da02f92
6 changed files with 250 additions and 8 deletions
+136
View File
@@ -6429,6 +6429,67 @@ describe("download manager", () => {
}
}, 20000);
it("retries a transient Mega-Debrid resolve failure fast (no long cooldown) with a German reason", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);
let unrestrictCalls = 0;
const manager = new DownloadManager(
{
...defaultSettings(),
megaCredentials: "mega-user:mega-pass",
megaDebridWebEnabled: true,
megaDebridApiEnabled: false,
megaDebridPreferApi: false,
providerOrder: [],
providerPrimary: "megadebrid",
providerSecondary: "none",
providerTertiary: "none",
outputDir: path.join(root, "downloads"),
extractDir: path.join(root, "extract"),
autoExtract: false,
autoReconnect: false,
enableIntegrityCheck: false,
maxParallel: 1
},
emptySession(),
createStoragePaths(path.join(root, "state")),
{
megaWebUnrestrict: vi.fn(async (_link: string, _signal?: AbortSignal) => {
unrestrictCalls += 1;
throw new Error("Fichier supprimé chez l'hébergeur");
})
}
);
manager.addPackages([{
name: "mega-transient",
links: ["https://rapidgator.net/file/mega-transient.part1.rar.html"]
}]);
await manager.start();
await waitFor(
() => String(Object.values(manager.getSnapshot().session.items)[0]?.fullStatus || "").includes("neuer Versuch"),
12000
);
const item = Object.values(manager.getSnapshot().session.items)[0];
expect(item.status).not.toBe("failed");
expect(unrestrictCalls).toBeGreaterThanOrEqual(1);
expect(item.lastError).toBe("Datei beim Hoster gerade nicht abrufbar");
expect(String(item.fullStatus || "")).toContain("Datei beim Hoster gerade nicht abrufbar");
expect(String(item.fullStatus || "")).toContain("neuer Versuch");
const countdownMatch = String(item.fullStatus || "").match(/\((\d+)s\)/);
expect(countdownMatch).not.toBeNull();
expect(Number(countdownMatch![1])).toBeLessThanOrEqual(10);
await waitFor(() => unrestrictCalls >= 2, 12000);
manager.stop();
await new Promise((resolve) => setTimeout(resolve, 150));
}, 20000);
it("limits Mega-Debrid Web validating starts to one item at a time", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);
@@ -6506,6 +6567,81 @@ describe("download manager", () => {
await new Promise((resolve) => setTimeout(resolve, 150));
});
it("runs one Mega-Debrid Web validation per available account in parallel", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);
let unrestrictCalls = 0;
const pendingRejectors = new Set<(error: Error) => void>();
const manager = new DownloadManager(
{
...defaultSettings(),
megaCredentials: "mega-user-a:pass-a\nmega-user-b:pass-b",
megaDebridWebEnabled: true,
megaDebridApiEnabled: false,
megaDebridPreferApi: false,
providerOrder: [],
providerPrimary: "megadebrid",
providerSecondary: "none",
providerTertiary: "none",
outputDir: path.join(root, "downloads"),
extractDir: path.join(root, "extract"),
autoExtract: false,
autoReconnect: false,
enableIntegrityCheck: false,
maxParallel: 4
},
emptySession(),
createStoragePaths(path.join(root, "state")),
{
megaWebUnrestrict: vi.fn(async (_link: string, signal?: AbortSignal) => {
unrestrictCalls += 1;
return await new Promise<UnrestrictedLink | null>((resolve, reject) => {
const rejector = (error: Error): void => {
signal?.removeEventListener("abort", onAbort);
pendingRejectors.delete(rejector);
reject(error);
};
const onAbort = (): void => {
rejector(new Error("aborted:test-mega-web"));
};
if (signal?.aborted) {
onAbort();
return;
}
signal?.addEventListener("abort", onAbort, { once: true });
pendingRejectors.add(rejector);
});
})
}
);
manager.addPackages([{
name: "mega-web-parallel",
links: [
"https://rapidgator.net/file/mega-web-1.part1.rar.html",
"https://rapidgator.net/file/mega-web-2.part2.rar.html",
"https://rapidgator.net/file/mega-web-3.part3.rar.html"
]
}]);
await manager.start();
await waitFor(() => unrestrictCalls === 2, 10000);
await new Promise((resolve) => setTimeout(resolve, 250));
const items = Object.values(manager.getSnapshot().session.items);
expect(items.filter((item) => item.status === "validating")).toHaveLength(2);
expect(items.filter((item) => item.status === "queued")).toHaveLength(1);
expect(unrestrictCalls).toBe(2);
manager.stop();
for (const reject of Array.from(pendingRejectors)) {
reject(new Error("aborted:test-mega-web"));
}
await new Promise((resolve) => setTimeout(resolve, 150));
});
it("shows the same AllDebrid countdown for all immediately free slots", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);