Compare commits
3 Commits
bec119583d
...
e91e66a5f3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e91e66a5f3 | ||
|
|
876483da51 | ||
|
|
6d4da02f92 |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.7.211",
|
"version": "1.7.212",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import {
|
|||||||
StartConflictResolutionResult,
|
StartConflictResolutionResult,
|
||||||
UiSnapshot, DebridAccountStatus } from "../shared/types";
|
UiSnapshot, DebridAccountStatus } from "../shared/types";
|
||||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||||
|
import { isMegaDebridTransientResolveFailure, germanMegaDebridResolveReason } from "../shared/mega-debrid-errors";
|
||||||
import {
|
import {
|
||||||
addDebridLinkApiKeyDailyUsageBytes,
|
addDebridLinkApiKeyDailyUsageBytes,
|
||||||
addDebridLinkApiKeyTotalUsageBytes,
|
addDebridLinkApiKeyTotalUsageBytes,
|
||||||
@ -51,7 +52,7 @@ function releaseTlsSkip(): void {
|
|||||||
}
|
}
|
||||||
import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
|
import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
|
||||||
import { planDownloadCompletion, validateDownloadedFileCompletion } from "./download-completion";
|
import { planDownloadCompletion, validateDownloadedFileCompletion } from "./download-completion";
|
||||||
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState } from "./debrid";
|
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, getMegaDebridAccountCooldownState, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState } from "./debrid";
|
||||||
import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, detectArchiveSignature, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree, resetExtractorCachesForPasswordChange, type ExtractArchiveFailureInfo } from "./extractor";
|
import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, detectArchiveSignature, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree, resetExtractorCachesForPasswordChange, type ExtractArchiveFailureInfo } from "./extractor";
|
||||||
import { validateFileAgainstManifest } from "./integrity";
|
import { validateFileAgainstManifest } from "./integrity";
|
||||||
import { classifyDiskError } from "./fs-error";
|
import { classifyDiskError } from "./fs-error";
|
||||||
@ -701,6 +702,12 @@ function isTemporaryUnrestrictError(errorText: string): boolean {
|
|||||||
|| text.includes("worker error");
|
|| text.includes("worker error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function transientResolveRetryDelayMs(retryCount: number): number {
|
||||||
|
const steps = [3000, 6000, 10000];
|
||||||
|
const n = Math.max(1, Math.floor(Number(retryCount) || 1));
|
||||||
|
return steps[Math.min(n - 1, steps.length - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
function isFinishedStatus(status: DownloadStatus): boolean {
|
function isFinishedStatus(status: DownloadStatus): boolean {
|
||||||
return status === "completed" || status === "failed" || status === "cancelled";
|
return status === "completed" || status === "failed" || status === "cancelled";
|
||||||
}
|
}
|
||||||
@ -7974,7 +7981,10 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
private getSerializedValidatingLimit(provider: DebridProvider | null): number {
|
private getSerializedValidatingLimit(provider: DebridProvider | null): number {
|
||||||
if (provider === "megadebrid-web") {
|
if (provider === "megadebrid-web") {
|
||||||
return 1;
|
const usableAccounts = getAvailableMegaDebridAccounts(this.settings)
|
||||||
|
.filter((account) => !getMegaDebridAccountCooldownState(`${account.id}:web`))
|
||||||
|
.length;
|
||||||
|
return Math.max(1, usableAccounts);
|
||||||
}
|
}
|
||||||
return Number.MAX_SAFE_INTEGER;
|
return Number.MAX_SAFE_INTEGER;
|
||||||
}
|
}
|
||||||
@ -9297,6 +9307,35 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isMegaDebridTransientResolveFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) {
|
||||||
|
active.unrestrictRetries += 1;
|
||||||
|
item.retries += 1;
|
||||||
|
const transientDelayMs = transientResolveRetryDelayMs(active.unrestrictRetries);
|
||||||
|
const transientReason = germanMegaDebridResolveReason(errorText);
|
||||||
|
logger.warn(`Transienter Mega-Debrid-Resolve-Fehler: item=${item.fileName || item.id}, retry=${active.unrestrictRetries}/${retryDisplayLimit}, delay=${transientDelayMs}ms, error=${errorText}, link=${item.url.slice(0, 80)}`);
|
||||||
|
if (item.downloadedBytes > 0) {
|
||||||
|
const targetFile = this.claimedTargetPathByItem.get(item.id) || "";
|
||||||
|
if (targetFile) {
|
||||||
|
try { fs.rmSync(targetFile, { force: true }); } catch { }
|
||||||
|
}
|
||||||
|
this.releaseTargetPath(item.id);
|
||||||
|
item.downloadedBytes = 0;
|
||||||
|
item.progressPercent = 0;
|
||||||
|
item.totalBytes = null;
|
||||||
|
this.dropItemContribution(item.id);
|
||||||
|
}
|
||||||
|
this.queueRetry(
|
||||||
|
item,
|
||||||
|
active,
|
||||||
|
transientDelayMs,
|
||||||
|
`${transientReason} — neuer Versuch ${active.unrestrictRetries}/${retryDisplayLimit} (${Math.ceil(transientDelayMs / 1000)}s)`
|
||||||
|
);
|
||||||
|
item.lastError = transientReason;
|
||||||
|
this.persistSoon();
|
||||||
|
this.emitState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (isUnrestrictFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) {
|
if (isUnrestrictFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) {
|
||||||
const debridLinkCooldown = parseDebridLinkCooldownRetry(errorText);
|
const debridLinkCooldown = parseDebridLinkCooldownRetry(errorText);
|
||||||
if (debridLinkCooldown) {
|
if (debridLinkCooldown) {
|
||||||
|
|||||||
@ -2561,7 +2561,7 @@ export function App(): ReactElement {
|
|||||||
const used = getMegaDebridAccountDailyUsageBytes(snapshot.settings, acc.id);
|
const used = getMegaDebridAccountDailyUsageBytes(snapshot.settings, acc.id);
|
||||||
const limit = getMegaDebridAccountDailyLimitBytes(settingsDraft, acc.id);
|
const limit = getMegaDebridAccountDailyLimitBytes(settingsDraft, acc.id);
|
||||||
rows.push({
|
rows.push({
|
||||||
rowKey: `mega-${acc.id}`,
|
rowKey: `mega-${entry.kind}-${acc.id}`,
|
||||||
entry,
|
entry,
|
||||||
hosterLabel: entry.serviceLabel,
|
hosterLabel: entry.serviceLabel,
|
||||||
modeLabel: entry.modeLabel,
|
modeLabel: entry.modeLabel,
|
||||||
@ -2684,7 +2684,8 @@ export function App(): ReactElement {
|
|||||||
else if (!st.isPremium) { statusCls = "free"; statusText = "Free Account"; }
|
else if (!st.isPremium) { statusCls = "free"; statusText = "Free Account"; }
|
||||||
else { statusCls = "ok"; statusText = st.message || "Premium Account"; }
|
else { statusCls = "ok"; statusText = st.message || "Premium Account"; }
|
||||||
const isProblem = statusCls === "invalid";
|
const isProblem = statusCls === "invalid";
|
||||||
const username = st && st.email ? st.email : row.username;
|
const username = row.username;
|
||||||
|
const usernameTitle = st && st.email && st.email.toLowerCase() !== row.username.toLowerCase() ? `${row.username} (Mail: ${st.email})` : row.username;
|
||||||
const expiry = st && st.premiumUntilMs && st.premiumUntilMs > 0 ? new Date(st.premiumUntilMs).toLocaleDateString("de-DE") : "—";
|
const expiry = st && st.premiumUntilMs && st.premiumUntilMs > 0 ? new Date(st.premiumUntilMs).toLocaleDateString("de-DE") : "—";
|
||||||
const traffic = row.dailyLimitBytes > 0
|
const traffic = row.dailyLimitBytes > 0
|
||||||
? `${humanSize(row.dailyRemainingBytes)} von ${humanSize(row.dailyLimitBytes)} übrig`
|
? `${humanSize(row.dailyRemainingBytes)} von ${humanSize(row.dailyLimitBytes)} übrig`
|
||||||
@ -2714,7 +2715,7 @@ export function App(): ReactElement {
|
|||||||
? <span className="acct2-nostatus" title="Für diesen Anbieter gibt es keine Status-Prüfung">—</span>
|
? <span className="acct2-nostatus" title="Für diesen Anbieter gibt es keine Status-Prüfung">—</span>
|
||||||
: <span className={`account-validity-badge ${statusCls}`}>{statusText}</span>}
|
: <span className={`account-validity-badge ${statusCls}`}>{statusText}</span>}
|
||||||
</span>
|
</span>
|
||||||
<span className="acct2-user" title={username}>{username}</span>
|
<span className="acct2-user" title={usernameTitle}>{username}</span>
|
||||||
<span className="acct2-expiry">{expiry}</span>
|
<span className="acct2-expiry">{expiry}</span>
|
||||||
<span className="acct2-c-actions">
|
<span className="acct2-c-actions">
|
||||||
{row.toggleKind === "single" && getAccountQuickActionMeta(row.entry.kind) && (
|
{row.toggleKind === "single" && getAccountQuickActionMeta(row.entry.kind) && (
|
||||||
@ -6784,7 +6785,13 @@ const ItemRow = memo(function ItemRow({ item, packageId, isSelected, sessionRunn
|
|||||||
}, [packageId, item.id, onContextMenu]);
|
}, [packageId, item.id, onContextMenu]);
|
||||||
const formattedCreatedAt = useMemo(() => formatDateTime(item.createdAt), [item.createdAt]);
|
const formattedCreatedAt = useMemo(() => formatDateTime(item.createdAt), [item.createdAt]);
|
||||||
const displayStatus = useMemo(() => computeDisplayedItemStatus(item, sessionRunning), [item, sessionRunning]);
|
const displayStatus = useMemo(() => computeDisplayedItemStatus(item, sessionRunning), [item, sessionRunning]);
|
||||||
const statusTitle = displayStatus ? (item.retries > 0 ? `${displayStatus} ? R${item.retries}` : displayStatus) : "";
|
const retrySuffix = item.retries > 0 ? ` (R${item.retries})` : "";
|
||||||
|
const lastErrorText = String(item.lastError || "").trim();
|
||||||
|
const statusTitle = displayStatus
|
||||||
|
? (lastErrorText && lastErrorText !== displayStatus && !displayStatus.includes(lastErrorText)
|
||||||
|
? `${displayStatus}${retrySuffix}\n${lastErrorText}`
|
||||||
|
: `${displayStatus}${retrySuffix}`)
|
||||||
|
: lastErrorText;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -7,9 +7,17 @@ export function isMegaDebridResolveFailure(errorText: string): boolean {
|
|||||||
|| text.includes("fichier inexistant");
|
|| text.includes("fichier inexistant");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isMegaDebridTransientResolveFailure(errorText: string): boolean {
|
||||||
|
const text = String(errorText || "").toLowerCase();
|
||||||
|
return isMegaDebridResolveFailure(text)
|
||||||
|
|| text.includes("datei beim hoster gerade nicht abrufbar")
|
||||||
|
|| text.includes("datei beim hoster nicht gefunden");
|
||||||
|
}
|
||||||
|
|
||||||
export function germanMegaDebridResolveReason(errorText: string): string {
|
export function germanMegaDebridResolveReason(errorText: string): string {
|
||||||
const text = String(errorText || "").toLowerCase();
|
const text = String(errorText || "").toLowerCase();
|
||||||
if (text.includes("introuvable") || text.includes("fichier inexistant") || text.includes("n'existe plus") || text.includes("n existe plus")) {
|
if (text.includes("datei beim hoster nicht gefunden")
|
||||||
|
|| text.includes("introuvable") || text.includes("fichier inexistant") || text.includes("n'existe plus") || text.includes("n existe plus")) {
|
||||||
return "Datei beim Hoster nicht gefunden";
|
return "Datei beim Hoster nicht gefunden";
|
||||||
}
|
}
|
||||||
return "Datei beim Hoster gerade nicht abrufbar";
|
return "Datei beim Hoster gerade nicht abrufbar";
|
||||||
|
|||||||
@ -14,7 +14,8 @@ import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
|||||||
import { getItemLogPath, initItemLogs, shutdownItemLogs } from "../src/main/item-log";
|
import { getItemLogPath, initItemLogs, shutdownItemLogs } from "../src/main/item-log";
|
||||||
import { initPackageLogs, shutdownPackageLogs } from "../src/main/package-log";
|
import { initPackageLogs, shutdownPackageLogs } from "../src/main/package-log";
|
||||||
import { createStoragePaths, emptySession } from "../src/main/storage";
|
import { createStoragePaths, emptySession } from "../src/main/storage";
|
||||||
import { primeDebridLinkRuntimeCooldownForTests, resetDebridLinkRuntimeStateForTests } from "../src/main/debrid";
|
import { primeDebridLinkRuntimeCooldownForTests, resetDebridLinkRuntimeStateForTests, primeMegaDebridRuntimeCooldownForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid";
|
||||||
|
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||||
import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "../src/main/rename-log";
|
import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "../src/main/rename-log";
|
||||||
import { UnrestrictedLink } from "../src/main/realdebrid";
|
import { UnrestrictedLink } from "../src/main/realdebrid";
|
||||||
|
|
||||||
@ -155,6 +156,7 @@ async function removeDirWithRetries(dir: string): Promise<void> {
|
|||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
resetDebridLinkRuntimeStateForTests();
|
resetDebridLinkRuntimeStateForTests();
|
||||||
|
resetMegaDebridRuntimeStateForTests();
|
||||||
shutdownItemLogs();
|
shutdownItemLogs();
|
||||||
shutdownPackageLogs();
|
shutdownPackageLogs();
|
||||||
shutdownRenameLog();
|
shutdownRenameLog();
|
||||||
@ -6429,6 +6431,67 @@ describe("download manager", () => {
|
|||||||
}
|
}
|
||||||
}, 20000);
|
}, 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 () => {
|
it("limits Mega-Debrid Web validating starts to one item at a time", async () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
tempDirs.push(root);
|
tempDirs.push(root);
|
||||||
@ -6506,6 +6569,158 @@ describe("download manager", () => {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
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("does not over-admit Mega-Debrid Web validations when an account is in cooldown", async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
|
||||||
|
let unrestrictCalls = 0;
|
||||||
|
const pendingRejectors = new Set<(error: Error) => void>();
|
||||||
|
|
||||||
|
primeMegaDebridRuntimeCooldownForTests(`${getMegaDebridAccountId("mega-user-b")}:web`, 120000);
|
||||||
|
|
||||||
|
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-cooldown",
|
||||||
|
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 === 1, 10000);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||||
|
|
||||||
|
const items = Object.values(manager.getSnapshot().session.items);
|
||||||
|
expect(items.filter((item) => item.status === "validating")).toHaveLength(1);
|
||||||
|
expect(items.filter((item) => item.status === "queued")).toHaveLength(2);
|
||||||
|
expect(unrestrictCalls).toBe(1);
|
||||||
|
|
||||||
|
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 () => {
|
it("shows the same AllDebrid countdown for all immediately free slots", async () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||||
tempDirs.push(root);
|
tempDirs.push(root);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { isMegaDebridResolveFailure, germanMegaDebridResolveReason } from "../src/shared/mega-debrid-errors";
|
import { isMegaDebridResolveFailure, germanMegaDebridResolveReason, isMegaDebridTransientResolveFailure } from "../src/shared/mega-debrid-errors";
|
||||||
|
|
||||||
describe("isMegaDebridResolveFailure", () => {
|
describe("isMegaDebridResolveFailure", () => {
|
||||||
it("detects the real Mega-Debrid French resolve-failure phrase", () => {
|
it("detects the real Mega-Debrid French resolve-failure phrase", () => {
|
||||||
@ -38,3 +38,27 @@ describe("germanMegaDebridResolveReason (transient wording, NOT 'tot')", () => {
|
|||||||
expect(germanMegaDebridResolveReason("Fichier introuvable")).toBe("Datei beim Hoster nicht gefunden");
|
expect(germanMegaDebridResolveReason("Fichier introuvable")).toBe("Datei beim Hoster nicht gefunden");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("isMegaDebridTransientResolveFailure (matches raw French AND rendered German)", () => {
|
||||||
|
it("matches the raw French phrase that may reach the download-manager", () => {
|
||||||
|
expect(isMegaDebridTransientResolveFailure("Mega-Debrid API: Fichier supprimé chez l'hébergeur")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches the German rendered reason that classifyAccountFailure produces", () => {
|
||||||
|
const aggregated = "Mega-Debrid (Account 1/4, ab***@x): Datei beim Hoster gerade nicht abrufbar | Mega-Debrid (Account 2/4, cd***@y): Datei beim Hoster gerade nicht abrufbar";
|
||||||
|
expect(isMegaDebridTransientResolveFailure(aggregated)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("matches the German not-found rendered reason", () => {
|
||||||
|
expect(isMegaDebridTransientResolveFailure("Mega-Debrid (Account 1/4): Datei beim Hoster nicht gefunden")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT match a Mega-Debrid timeout/abort (that has its own account cooldown path)", () => {
|
||||||
|
expect(isMegaDebridTransientResolveFailure("Mega-Debrid (Account 1/4): Abbruch/Timeout nach 60s")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT match unrelated provider errors", () => {
|
||||||
|
expect(isMegaDebridTransientResolveFailure("AllDebrid: zu viele aktive Downloads")).toBe(false);
|
||||||
|
expect(isMegaDebridTransientResolveFailure("Debrid-Link: badToken")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
31
tests/unrestrict-retry.test.ts
Normal file
31
tests/unrestrict-retry.test.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { transientResolveRetryDelayMs } from "../src/main/download-manager";
|
||||||
|
|
||||||
|
describe("transientResolveRetryDelayMs (fast, bounded retry for transient resolve failures)", () => {
|
||||||
|
it("starts fast (<= 3s) instead of the 5s..120s exponential", () => {
|
||||||
|
expect(transientResolveRetryDelayMs(1)).toBeLessThanOrEqual(3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ramps gently and caps at 10s", () => {
|
||||||
|
expect(transientResolveRetryDelayMs(2)).toBeLessThanOrEqual(7000);
|
||||||
|
expect(transientResolveRetryDelayMs(3)).toBeLessThanOrEqual(10000);
|
||||||
|
expect(transientResolveRetryDelayMs(10)).toBe(10000);
|
||||||
|
expect(transientResolveRetryDelayMs(100)).toBe(10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never schedules anywhere near the 5s..120s exponential cap", () => {
|
||||||
|
for (let n = 1; n <= 50; n += 1) {
|
||||||
|
expect(transientResolveRetryDelayMs(n)).toBeLessThanOrEqual(10000);
|
||||||
|
expect(transientResolveRetryDelayMs(n)).toBeGreaterThanOrEqual(1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is monotonic non-decreasing", () => {
|
||||||
|
let prev = 0;
|
||||||
|
for (let n = 1; n <= 12; n += 1) {
|
||||||
|
const d = transientResolveRetryDelayMs(n);
|
||||||
|
expect(d).toBeGreaterThanOrEqual(prev);
|
||||||
|
prev = d;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user