Compare commits
3 Commits
738ae2e2d5
...
a7d989ebd2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7d989ebd2 | ||
|
|
73004f3864 | ||
|
|
21803f316b |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.7.213",
|
||||
"version": "1.7.214",
|
||||
"description": "Desktop downloader",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
|
||||
@ -661,14 +661,14 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
|
||||
return encryptBackup(JSON.stringify(payloadObj));
|
||||
}
|
||||
|
||||
public exportSupportBundle(): { buffer: Buffer; defaultFileName: string } {
|
||||
public async exportSupportBundle(): Promise<{ buffer: Buffer; defaultFileName: string }> {
|
||||
this.audit("INFO", "Support-Bundle exportiert");
|
||||
logTraceEvent("INFO", "support", "Support-Bundle erstellt", {
|
||||
packageCount: Object.keys(this.manager.getSnapshot().session.packages).length,
|
||||
itemCount: Object.keys(this.manager.getSnapshot().session.items).length
|
||||
});
|
||||
return {
|
||||
buffer: buildSupportBundle(this.manager, this.storagePaths.baseDir, { hostDiagnosticsMode: "cached" }),
|
||||
buffer: await buildSupportBundle(this.manager, this.storagePaths.baseDir, { hostDiagnosticsMode: "cached" }),
|
||||
defaultFileName: getSupportBundleDefaultFileName()
|
||||
};
|
||||
}
|
||||
|
||||
@ -1897,7 +1897,7 @@ class MegaDebridClient {
|
||||
logger.info(`Mega-Debrid (API) unrestrict OK: ${apiResult.fileName}`);
|
||||
return apiResult;
|
||||
}
|
||||
throw new Error("Mega-Debrid API: Login oder Unrestrict fehlgeschlagen");
|
||||
throw new Error("Mega-Debrid API: Linkgenerierung lieferte kein Ergebnis");
|
||||
} catch (error) {
|
||||
const errorText = compactErrorText(error);
|
||||
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
|
||||
@ -2162,7 +2162,11 @@ class MegaDebridClient {
|
||||
return { fatal: true, cooldownMs: 0, message: errorText, category: "temporary" };
|
||||
}
|
||||
|
||||
if (/login|password|auth|credentials|unauthorized|forbidden/i.test(errorText) || /connectUser/i.test(errorText)) {
|
||||
if (/token.?error|please log.?in/i.test(errorText)) {
|
||||
return { fatal: false, cooldownMs: 15_000, message: errorText, category: "temporary" };
|
||||
}
|
||||
|
||||
if (/bad.?login|incorrect.?(login|password)|invalid.?(login|password|credentials)|wrong.?password|unauthorized|forbidden|connectUser/i.test(errorText)) {
|
||||
return {
|
||||
fatal: false,
|
||||
cooldownMs: MEGA_DEBRID_INVALID_ACCOUNT_COOLDOWN_MS,
|
||||
|
||||
@ -837,12 +837,17 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
||||
return;
|
||||
}
|
||||
const fileName = getSupportBundleDefaultFileName();
|
||||
const body = buildSupportBundle(manager, runtimeBaseDir);
|
||||
logTraceEvent("INFO", "support", "Support-Bundle über Debug-Server heruntergeladen", {
|
||||
fileName,
|
||||
sizeBytes: body.length
|
||||
});
|
||||
binaryResponse(res, 200, body, "application/zip", fileName);
|
||||
buildSupportBundle(manager, runtimeBaseDir)
|
||||
.then((body) => {
|
||||
logTraceEvent("INFO", "support", "Support-Bundle über Debug-Server heruntergeladen", {
|
||||
fileName,
|
||||
sizeBytes: body.length
|
||||
});
|
||||
binaryResponse(res, 200, body, "application/zip", fileName);
|
||||
})
|
||||
.catch((error) => {
|
||||
jsonResponse(res, 500, { error: String((error as { message?: string })?.message || error) });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -7998,9 +7998,10 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
|
||||
private getSerializedValidatingLimit(provider: DebridProvider | null): number {
|
||||
if (provider === "megadebrid-web") {
|
||||
if (provider === "megadebrid-web" || provider === "megadebrid-api") {
|
||||
const mode = provider === "megadebrid-web" ? "web" : "api";
|
||||
const usableAccounts = getAvailableMegaDebridAccounts(this.settings)
|
||||
.filter((account) => !getMegaDebridAccountCooldownState(`${account.id}:web`))
|
||||
.filter((account) => !getMegaDebridAccountCooldownState(`${account.id}:${mode}`))
|
||||
.length;
|
||||
return Math.max(1, usableAccounts);
|
||||
}
|
||||
|
||||
@ -592,7 +592,7 @@ function registerIpcHandlers(): void {
|
||||
if (result.canceled || !result.filePath) {
|
||||
return { saved: false };
|
||||
}
|
||||
const exported = controller.exportSupportBundle();
|
||||
const exported = await controller.exportSupportBundle();
|
||||
await fs.promises.writeFile(result.filePath, exported.buffer);
|
||||
return { saved: true, filePath: result.filePath };
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import fs from "node:fs";
|
||||
import { promises as fsp } from "node:fs";
|
||||
import path from "node:path";
|
||||
import AdmZip from "adm-zip";
|
||||
import { APP_VERSION } from "./constants";
|
||||
@ -20,9 +20,9 @@ import type { DownloadManager } from "./download-manager";
|
||||
|
||||
const AI_MANIFEST_FILE = "debug_ai_manifest.json";
|
||||
|
||||
function safeReadJson(filePath: string): unknown {
|
||||
async function safeReadJson(filePath: string): Promise<unknown> {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
|
||||
return JSON.parse(await fsp.readFile(filePath, "utf8")) as unknown;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@ -32,42 +32,50 @@ function addJson(zip: AdmZip, zipPath: string, value: unknown): void {
|
||||
zip.addFile(zipPath, Buffer.from(`${JSON.stringify(value, null, 2)}\n`, "utf8"));
|
||||
}
|
||||
|
||||
function addFileIfExists(zip: AdmZip, sourcePath: string | null, zipPath: string): void {
|
||||
if (!sourcePath || !fs.existsSync(sourcePath)) {
|
||||
async function addFileIfExists(zip: AdmZip, sourcePath: string | null, zipPath: string): Promise<void> {
|
||||
if (!sourcePath) {
|
||||
return;
|
||||
}
|
||||
zip.addLocalFile(sourcePath, path.posix.dirname(zipPath), path.posix.basename(zipPath));
|
||||
try {
|
||||
const buffer = await fsp.readFile(sourcePath);
|
||||
zip.addFile(zipPath, buffer);
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
function addDirectoryIfExists(zip: AdmZip, dirPath: string, zipRoot: string): void {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
async function addDirectoryIfExists(zip: AdmZip, dirPath: string, zipRoot: string): Promise<void> {
|
||||
let entries;
|
||||
try {
|
||||
entries = await fsp.readdir(dirPath, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
const zipPath = path.posix.join(zipRoot, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
addDirectoryIfExists(zip, fullPath, zipPath);
|
||||
await addDirectoryIfExists(zip, fullPath, zipPath);
|
||||
continue;
|
||||
}
|
||||
zip.addLocalFile(fullPath, path.posix.dirname(zipPath), path.posix.basename(zipPath));
|
||||
await addFileIfExists(zip, fullPath, zipPath);
|
||||
}
|
||||
}
|
||||
|
||||
function addRecentDirectoryFiles(zip: AdmZip, dirPath: string, zipRoot: string, maxAgeMs: number): number {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
async function addRecentDirectoryFiles(zip: AdmZip, dirPath: string, zipRoot: string, maxAgeMs: number): Promise<number> {
|
||||
let entries;
|
||||
try {
|
||||
entries = await fsp.readdir(dirPath, { withFileTypes: true });
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
const cutoff = Date.now() - maxAgeMs;
|
||||
let added = 0;
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue;
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
try {
|
||||
if (fs.statSync(fullPath).mtimeMs >= cutoff) {
|
||||
zip.addLocalFile(fullPath, zipRoot, entry.name);
|
||||
if ((await fsp.stat(fullPath)).mtimeMs >= cutoff) {
|
||||
await addFileIfExists(zip, fullPath, path.posix.join(zipRoot, entry.name));
|
||||
added += 1;
|
||||
}
|
||||
} catch { }
|
||||
@ -127,7 +135,7 @@ function resolveHostDiagnostics(mode: HostDiagnosticsMode): unknown {
|
||||
return getWindowsHostDiagnostics();
|
||||
}
|
||||
|
||||
export function buildSupportBundle(manager: DownloadManager, baseDir: string, options: BuildSupportBundleOptions = {}): Buffer {
|
||||
export async function buildSupportBundle(manager: DownloadManager, baseDir: string, options: BuildSupportBundleOptions = {}): Promise<Buffer> {
|
||||
const zip = new AdmZip();
|
||||
const hostDiagnosticsMode = options.hostDiagnosticsMode || "full";
|
||||
const storagePaths = createStoragePaths(baseDir);
|
||||
@ -175,39 +183,39 @@ export function buildSupportBundle(manager: DownloadManager, baseDir: string, op
|
||||
const recentErrors = getRecentErrors();
|
||||
addJson(zip, "overview/recent-errors.json", { count: recentErrors.length, entries: recentErrors });
|
||||
|
||||
addFileIfExists(zip, path.join(baseDir, AI_MANIFEST_FILE), `runtime/${AI_MANIFEST_FILE}`);
|
||||
addFileIfExists(zip, path.join(baseDir, "debug_host.txt"), "runtime/debug_host.txt");
|
||||
addFileIfExists(zip, path.join(baseDir, "debug_port.txt"), "runtime/debug_port.txt");
|
||||
addFileIfExists(zip, getTraceConfigPath(), "runtime/trace_config.json");
|
||||
await addFileIfExists(zip, path.join(baseDir, AI_MANIFEST_FILE), `runtime/${AI_MANIFEST_FILE}`);
|
||||
await addFileIfExists(zip, path.join(baseDir, "debug_host.txt"), "runtime/debug_host.txt");
|
||||
await addFileIfExists(zip, path.join(baseDir, "debug_port.txt"), "runtime/debug_port.txt");
|
||||
await addFileIfExists(zip, getTraceConfigPath(), "runtime/trace_config.json");
|
||||
|
||||
addFileIfExists(zip, getLogFilePath(), "logs/rd_downloader.log");
|
||||
addFileIfExists(zip, `${getLogFilePath()}.old`, "logs/rd_downloader.log.old");
|
||||
addFileIfExists(zip, getAuditLogPath(), "logs/audit.log");
|
||||
addFileIfExists(zip, getAuditLogPath() ? `${getAuditLogPath()}.old` : null, "logs/audit.log.old");
|
||||
addFileIfExists(zip, getRenameLogPath(), "logs/rename.log");
|
||||
addFileIfExists(zip, getRenameLogPath() ? `${getRenameLogPath()}.old` : null, "logs/rename.log.old");
|
||||
addFileIfExists(zip, getDesktopRenameLogPath(), "logs/rename-session-desktop.txt");
|
||||
addFileIfExists(zip, getSessionLogPath(), "logs/session.log");
|
||||
addFileIfExists(zip, getTraceLogPath(), "logs/trace.log");
|
||||
addFileIfExists(zip, getTraceLogPath() ? `${getTraceLogPath()}.old` : null, "logs/trace.log.old");
|
||||
addFileIfExists(zip, getAccountRotationLogPath(), "logs/account-rotation.log");
|
||||
addFileIfExists(zip, getAccountRotationLogPath() ? `${getAccountRotationLogPath()}.old` : null, "logs/account-rotation.log.old");
|
||||
addFileIfExists(zip, getConversionLogPath(), "logs/conversion.log");
|
||||
addFileIfExists(zip, getConversionLogPath() ? `${getConversionLogPath()}.old` : null, "logs/conversion.log.old");
|
||||
await addFileIfExists(zip, getLogFilePath(), "logs/rd_downloader.log");
|
||||
await addFileIfExists(zip, `${getLogFilePath()}.old`, "logs/rd_downloader.log.old");
|
||||
await addFileIfExists(zip, getAuditLogPath(), "logs/audit.log");
|
||||
await addFileIfExists(zip, getAuditLogPath() ? `${getAuditLogPath()}.old` : null, "logs/audit.log.old");
|
||||
await addFileIfExists(zip, getRenameLogPath(), "logs/rename.log");
|
||||
await addFileIfExists(zip, getRenameLogPath() ? `${getRenameLogPath()}.old` : null, "logs/rename.log.old");
|
||||
await addFileIfExists(zip, getDesktopRenameLogPath(), "logs/rename-session-desktop.txt");
|
||||
await addFileIfExists(zip, getSessionLogPath(), "logs/session.log");
|
||||
await addFileIfExists(zip, getTraceLogPath(), "logs/trace.log");
|
||||
await addFileIfExists(zip, getTraceLogPath() ? `${getTraceLogPath()}.old` : null, "logs/trace.log.old");
|
||||
await addFileIfExists(zip, getAccountRotationLogPath(), "logs/account-rotation.log");
|
||||
await addFileIfExists(zip, getAccountRotationLogPath() ? `${getAccountRotationLogPath()}.old` : null, "logs/account-rotation.log.old");
|
||||
await addFileIfExists(zip, getConversionLogPath(), "logs/conversion.log");
|
||||
await addFileIfExists(zip, getConversionLogPath() ? `${getConversionLogPath()}.old` : null, "logs/conversion.log.old");
|
||||
|
||||
const SUPPORT_BUNDLE_LOG_WINDOW_MS = 8 * 60 * 60 * 1000;
|
||||
addDirectoryIfExists(zip, path.join(baseDir, "session-logs"), "logs/session-logs");
|
||||
addRecentDirectoryFiles(zip, path.join(baseDir, "package-logs"), "logs/package-logs", SUPPORT_BUNDLE_LOG_WINDOW_MS);
|
||||
addRecentDirectoryFiles(zip, path.join(baseDir, "item-logs"), "logs/item-logs", SUPPORT_BUNDLE_LOG_WINDOW_MS);
|
||||
await addDirectoryIfExists(zip, path.join(baseDir, "session-logs"), "logs/session-logs");
|
||||
await addRecentDirectoryFiles(zip, path.join(baseDir, "package-logs"), "logs/package-logs", SUPPORT_BUNDLE_LOG_WINDOW_MS);
|
||||
await addRecentDirectoryFiles(zip, path.join(baseDir, "item-logs"), "logs/item-logs", SUPPORT_BUNDLE_LOG_WINDOW_MS);
|
||||
|
||||
for (const packageId of packageIds) {
|
||||
addFileIfExists(zip, manager.getPackageLogPath(packageId) || getPackageLogPath(packageId), `logs/live/package-${packageId}.txt`);
|
||||
await addFileIfExists(zip, manager.getPackageLogPath(packageId) || getPackageLogPath(packageId), `logs/live/package-${packageId}.txt`);
|
||||
}
|
||||
for (const itemId of itemIds) {
|
||||
addFileIfExists(zip, manager.getItemLogPath(itemId), `logs/live/item-${itemId}.txt`);
|
||||
await addFileIfExists(zip, manager.getItemLogPath(itemId), `logs/live/item-${itemId}.txt`);
|
||||
}
|
||||
|
||||
const aiManifest = safeReadJson(path.join(baseDir, AI_MANIFEST_FILE));
|
||||
const aiManifest = await safeReadJson(path.join(baseDir, AI_MANIFEST_FILE));
|
||||
if (aiManifest) {
|
||||
addJson(zip, "overview/ai-manifest.json", aiManifest);
|
||||
}
|
||||
|
||||
@ -1339,6 +1339,84 @@ describe("debrid service", () => {
|
||||
expect(getMegaDebridAccountCooldownState(`${getMegaDebridAccountId("user")}:api`)).toBeNull();
|
||||
});
|
||||
|
||||
it("does NOT slap a 60-minute 'invalid' cooldown on a working account when the API returns no result (transient)", async () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
token: "",
|
||||
bestToken: "",
|
||||
allDebridToken: "",
|
||||
megaLogin: "user",
|
||||
megaPassword: "pass",
|
||||
megaCredentials: "user:pass",
|
||||
megaDebridApiEnabled: true,
|
||||
megaDebridWebEnabled: false,
|
||||
providerPrimary: "megadebrid-api" as const,
|
||||
providerSecondary: "none" as const,
|
||||
providerTertiary: "none" as const,
|
||||
autoProviderFallback: true
|
||||
};
|
||||
|
||||
globalThis.fetch = (async (input: RequestInfo | URL): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url.includes("action=connectUser")) {
|
||||
return new Response(JSON.stringify({ response_code: "ok", token: "tok", vip_end: Math.floor(Date.now() / 1000) + 999999 }), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
if (url.includes("action=getLink")) {
|
||||
return new Response(JSON.stringify({ response_code: "ok" }), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
return new Response("not-found", { status: 404 });
|
||||
}) as typeof fetch;
|
||||
|
||||
const service = new DebridService(settings);
|
||||
const err = await service.unrestrictLink("https://rapidgator.net/file/no-result.rar.html").then(() => null, (e: unknown) => e);
|
||||
expect(err).toBeTruthy();
|
||||
expect(String(err)).not.toMatch(/Login oder Unrestrict/i);
|
||||
|
||||
const cooldown = getMegaDebridAccountCooldownState(`${getMegaDebridAccountId("user")}:api`);
|
||||
if (cooldown) {
|
||||
expect(cooldown.category).not.toBe("invalid");
|
||||
expect(cooldown.remainingMs).toBeLessThan(5 * 60 * 1000);
|
||||
}
|
||||
});
|
||||
|
||||
it("treats a Mega-Debrid API 'Token error, please log-in' as a short transient cooldown, not invalid", async () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
token: "",
|
||||
bestToken: "",
|
||||
allDebridToken: "",
|
||||
megaLogin: "user",
|
||||
megaPassword: "pass",
|
||||
megaCredentials: "user:pass",
|
||||
megaDebridApiEnabled: true,
|
||||
megaDebridWebEnabled: false,
|
||||
providerPrimary: "megadebrid-api" as const,
|
||||
providerSecondary: "none" as const,
|
||||
providerTertiary: "none" as const,
|
||||
autoProviderFallback: true
|
||||
};
|
||||
|
||||
globalThis.fetch = (async (input: RequestInfo | URL): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url.includes("action=connectUser")) {
|
||||
return new Response(JSON.stringify({ response_code: "ok", token: "tok", vip_end: Math.floor(Date.now() / 1000) + 999999 }), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
if (url.includes("action=getLink")) {
|
||||
return new Response(JSON.stringify({ response_code: "error_token", response_text: "Token error, please log-in" }), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
return new Response("not-found", { status: 404 });
|
||||
}) as typeof fetch;
|
||||
|
||||
const service = new DebridService(settings);
|
||||
await service.unrestrictLink("https://rapidgator.net/file/token-collision.rar.html").then(() => null, (e: unknown) => e);
|
||||
|
||||
const cooldown = getMegaDebridAccountCooldownState(`${getMegaDebridAccountId("user")}:api`);
|
||||
if (cooldown) {
|
||||
expect(cooldown.category).not.toBe("invalid");
|
||||
expect(cooldown.remainingMs).toBeLessThan(60 * 1000);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses Mega Web only when it is configured as a separate fallback provider", async () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
|
||||
@ -6721,6 +6721,160 @@ describe("download manager", () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
});
|
||||
|
||||
it("serializes Mega-Debrid API conversions to one per account (no single-token hammering)", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
let getLinkCalls = 0;
|
||||
const pendingRejectors = new Set<(error: Error) => void>();
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : (input as Request).url;
|
||||
if (url.includes("action=connectUser")) {
|
||||
return new Response(JSON.stringify({ response_code: "ok", token: `tok-${getLinkCalls}-${url.length}` }), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
if (url.includes("action=getLink")) {
|
||||
getLinkCalls += 1;
|
||||
const signal = init?.signal;
|
||||
return await new Promise<Response>((_resolve, reject) => {
|
||||
const rejector = (error: Error): void => {
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
pendingRejectors.delete(rejector);
|
||||
reject(error);
|
||||
};
|
||||
const onAbort = (): void => { rejector(new Error("aborted:test-api")); };
|
||||
if (signal?.aborted) { onAbort(); return; }
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
pendingRejectors.add(rejector);
|
||||
});
|
||||
}
|
||||
return originalFetch(input, init);
|
||||
}) as typeof fetch;
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
megaCredentials: "mega-user-a:pass-a\nmega-user-b:pass-b",
|
||||
megaDebridApiEnabled: true,
|
||||
megaDebridWebEnabled: false,
|
||||
megaDebridPreferApi: true,
|
||||
providerOrder: [],
|
||||
providerPrimary: "megadebrid",
|
||||
providerSecondary: "none",
|
||||
providerTertiary: "none",
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir: path.join(root, "extract"),
|
||||
autoExtract: false,
|
||||
autoReconnect: false,
|
||||
enableIntegrityCheck: false,
|
||||
maxParallel: 6
|
||||
},
|
||||
emptySession(),
|
||||
createStoragePaths(path.join(root, "state")),
|
||||
{}
|
||||
);
|
||||
|
||||
manager.addPackages([{
|
||||
name: "mega-api-serialized",
|
||||
links: [
|
||||
"https://rapidgator.net/file/api-1.part1.rar.html",
|
||||
"https://rapidgator.net/file/api-2.part2.rar.html",
|
||||
"https://rapidgator.net/file/api-3.part3.rar.html"
|
||||
]
|
||||
}]);
|
||||
|
||||
await manager.start();
|
||||
await waitFor(() => getLinkCalls === 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(getLinkCalls).toBe(2);
|
||||
|
||||
manager.stop();
|
||||
for (const reject of Array.from(pendingRejectors)) {
|
||||
reject(new Error("aborted:test-api"));
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
});
|
||||
|
||||
it("limits Mega-Debrid API conversions to one at a time with a single account", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
let getLinkCalls = 0;
|
||||
const pendingRejectors = new Set<(error: Error) => void>();
|
||||
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : (input as Request).url;
|
||||
if (url.includes("action=connectUser")) {
|
||||
return new Response(JSON.stringify({ response_code: "ok", token: "tok-single" }), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
if (url.includes("action=getLink")) {
|
||||
getLinkCalls += 1;
|
||||
const signal = init?.signal;
|
||||
return await new Promise<Response>((_resolve, reject) => {
|
||||
const rejector = (error: Error): void => {
|
||||
signal?.removeEventListener("abort", onAbort);
|
||||
pendingRejectors.delete(rejector);
|
||||
reject(error);
|
||||
};
|
||||
const onAbort = (): void => { rejector(new Error("aborted:test-api")); };
|
||||
if (signal?.aborted) { onAbort(); return; }
|
||||
signal?.addEventListener("abort", onAbort, { once: true });
|
||||
pendingRejectors.add(rejector);
|
||||
});
|
||||
}
|
||||
return originalFetch(input, init);
|
||||
}) as typeof fetch;
|
||||
|
||||
const manager = new DownloadManager(
|
||||
{
|
||||
...defaultSettings(),
|
||||
megaCredentials: "mega-user:mega-pass",
|
||||
megaDebridApiEnabled: true,
|
||||
megaDebridWebEnabled: false,
|
||||
megaDebridPreferApi: true,
|
||||
providerOrder: [],
|
||||
providerPrimary: "megadebrid",
|
||||
providerSecondary: "none",
|
||||
providerTertiary: "none",
|
||||
outputDir: path.join(root, "downloads"),
|
||||
extractDir: path.join(root, "extract"),
|
||||
autoExtract: false,
|
||||
autoReconnect: false,
|
||||
enableIntegrityCheck: false,
|
||||
maxParallel: 6
|
||||
},
|
||||
emptySession(),
|
||||
createStoragePaths(path.join(root, "state")),
|
||||
{}
|
||||
);
|
||||
|
||||
manager.addPackages([{
|
||||
name: "mega-api-single",
|
||||
links: [
|
||||
"https://rapidgator.net/file/single-1.part1.rar.html",
|
||||
"https://rapidgator.net/file/single-2.part2.rar.html",
|
||||
"https://rapidgator.net/file/single-3.part3.rar.html"
|
||||
]
|
||||
}]);
|
||||
|
||||
await manager.start();
|
||||
await waitFor(() => getLinkCalls === 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(getLinkCalls).toBe(1);
|
||||
|
||||
manager.stop();
|
||||
for (const reject of Array.from(pendingRejectors)) {
|
||||
reject(new Error("aborted:test-api"));
|
||||
}
|
||||
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);
|
||||
|
||||
67
tests/support-bundle.test.ts
Normal file
67
tests/support-bundle.test.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import AdmZip from "adm-zip";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { buildSupportBundle } from "../src/main/support-bundle";
|
||||
import type { DownloadManager } from "../src/main/download-manager";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { }
|
||||
}
|
||||
});
|
||||
|
||||
function fakeManager(): DownloadManager {
|
||||
const snapshot = {
|
||||
stats: {},
|
||||
session: { packages: {}, items: {}, packageOrder: [] },
|
||||
speedText: "",
|
||||
etaText: "",
|
||||
canStart: false,
|
||||
canStop: false,
|
||||
canPause: false
|
||||
};
|
||||
return {
|
||||
getSnapshot: () => snapshot,
|
||||
getPackageLogPath: () => null,
|
||||
getItemLogPath: () => null
|
||||
} as unknown as DownloadManager;
|
||||
}
|
||||
|
||||
describe("buildSupportBundle (async, non-blocking)", () => {
|
||||
it("returns a Promise and produces a valid zip with overview + a real on-disk file", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-bundle-"));
|
||||
tempDirs.push(root);
|
||||
fs.writeFileSync(path.join(root, "debug_host.txt"), "host-info-test", "utf8");
|
||||
|
||||
const promise = buildSupportBundle(fakeManager(), root, { hostDiagnosticsMode: "none" });
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
|
||||
const buffer = await promise;
|
||||
expect(Buffer.isBuffer(buffer)).toBe(true);
|
||||
expect(buffer.length).toBeGreaterThan(0);
|
||||
|
||||
const entries = new AdmZip(buffer).getEntries().map((e) => e.entryName);
|
||||
expect(entries).toContain("overview/meta.json");
|
||||
expect(entries).toContain("overview/settings.json");
|
||||
expect(entries).toContain("runtime/debug_host.txt");
|
||||
|
||||
const hostEntry = new AdmZip(buffer).getEntry("runtime/debug_host.txt");
|
||||
expect(hostEntry?.getData().toString("utf8")).toBe("host-info-test");
|
||||
});
|
||||
|
||||
it("does not block the event loop while building (a concurrent timer still fires)", async () => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-bundle-"));
|
||||
tempDirs.push(root);
|
||||
|
||||
let timerFired = false;
|
||||
const timer = setTimeout(() => { timerFired = true; }, 0);
|
||||
await buildSupportBundle(fakeManager(), root, { hostDiagnosticsMode: "none" });
|
||||
clearTimeout(timer);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
expect(timerFired).toBe(true);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user