Release v1.4.16 with crash prevention and hang protection
- Add 30s fetch timeouts to ALL API calls (Real-Debrid, BestDebrid, AllDebrid, Mega-Web) - Fix race condition in concurrent worker indexing (runWithConcurrency) - Guard JSON.parse in RealDebrid response with try-catch - Add try-catch to fs.mkdirSync in download pipeline (handles permission denied) - Convert MD5/SHA1 hashing to streaming (prevents OOM on large files) - Add error handling for hash manifest file reading - Prevent infinite hangs on unresponsive API endpoints Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
147269849d
commit
ea6301d326
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.15",
|
"version": "1.4.16",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.15",
|
"version": "1.4.16",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.15",
|
"version": "1.4.16",
|
||||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import { AppSettings, DebridFallbackProvider, DebridProvider } from "../shared/types";
|
import { AppSettings, DebridFallbackProvider, DebridProvider } from "../shared/types";
|
||||||
import { REQUEST_RETRIES } from "./constants";
|
import { REQUEST_RETRIES } from "./constants";
|
||||||
|
import { logger } from "./logger";
|
||||||
import { RealDebridClient, UnrestrictedLink } from "./realdebrid";
|
import { RealDebridClient, UnrestrictedLink } from "./realdebrid";
|
||||||
import { compactErrorText, filenameFromUrl, looksLikeOpaqueFilename, sleep } from "./utils";
|
import { compactErrorText, filenameFromUrl, looksLikeOpaqueFilename, sleep } from "./utils";
|
||||||
|
|
||||||
|
const API_TIMEOUT_MS = 30000;
|
||||||
|
|
||||||
const BEST_DEBRID_API_BASE = "https://bestdebrid.com/api/v1";
|
const BEST_DEBRID_API_BASE = "https://bestdebrid.com/api/v1";
|
||||||
const ALL_DEBRID_API_BASE = "https://api.alldebrid.com/v4";
|
const ALL_DEBRID_API_BASE = "https://api.alldebrid.com/v4";
|
||||||
|
|
||||||
@ -216,11 +219,19 @@ async function runWithConcurrency<T>(items: T[], concurrency: number, worker: (i
|
|||||||
}
|
}
|
||||||
const size = Math.max(1, Math.min(concurrency, items.length));
|
const size = Math.max(1, Math.min(concurrency, items.length));
|
||||||
let index = 0;
|
let index = 0;
|
||||||
|
const next = (): T | undefined => {
|
||||||
|
if (index >= items.length) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const item = items[index];
|
||||||
|
index += 1;
|
||||||
|
return item;
|
||||||
|
};
|
||||||
const runners = Array.from({ length: size }, async () => {
|
const runners = Array.from({ length: size }, async () => {
|
||||||
while (index < items.length) {
|
let current = next();
|
||||||
const current = items[index];
|
while (current !== undefined) {
|
||||||
index += 1;
|
|
||||||
await worker(current);
|
await worker(current);
|
||||||
|
current = next();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await Promise.all(runners);
|
await Promise.all(runners);
|
||||||
@ -243,7 +254,8 @@ async function resolveRapidgatorFilename(link: string): Promise<string> {
|
|||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
||||||
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
"Accept-Language": "en-US,en;q=0.9,de;q=0.8"
|
"Accept-Language": "en-US,en;q=0.9,de;q=0.8"
|
||||||
}
|
},
|
||||||
|
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (shouldRetryStatus(response.status) && attempt < REQUEST_RETRIES + 2) {
|
if (shouldRetryStatus(response.status) && attempt < REQUEST_RETRIES + 2) {
|
||||||
@ -348,7 +360,8 @@ class BestDebridClient {
|
|||||||
|
|
||||||
const response = await fetch(request.url, {
|
const response = await fetch(request.url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers
|
headers,
|
||||||
|
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
||||||
});
|
});
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
const parsed = parseJson(text);
|
const parsed = parseJson(text);
|
||||||
@ -432,7 +445,8 @@ class AllDebridClient {
|
|||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
"User-Agent": "RD-Node-Downloader/1.1.15"
|
"User-Agent": "RD-Node-Downloader/1.1.15"
|
||||||
},
|
},
|
||||||
body
|
body,
|
||||||
|
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
@ -484,7 +498,8 @@ class AllDebridClient {
|
|||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
"User-Agent": "RD-Node-Downloader/1.1.12"
|
"User-Agent": "RD-Node-Downloader/1.1.12"
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({ link })
|
body: new URLSearchParams({ link }),
|
||||||
|
signal: AbortSignal.timeout(API_TIMEOUT_MS)
|
||||||
});
|
});
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
const payload = asRecord(parseJson(text));
|
const payload = asRecord(parseJson(text));
|
||||||
|
|||||||
@ -1851,7 +1851,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
item.provider = unrestricted.provider;
|
item.provider = unrestricted.provider;
|
||||||
item.retries += unrestricted.retriesUsed;
|
item.retries += unrestricted.retriesUsed;
|
||||||
item.fileName = sanitizeFilename(unrestricted.fileName || filenameFromUrl(item.url));
|
item.fileName = sanitizeFilename(unrestricted.fileName || filenameFromUrl(item.url));
|
||||||
fs.mkdirSync(pkg.outputDir, { recursive: true });
|
try {
|
||||||
|
fs.mkdirSync(pkg.outputDir, { recursive: true });
|
||||||
|
} catch (mkdirError) {
|
||||||
|
throw new Error(`Zielordner kann nicht erstellt werden: ${compactErrorText(mkdirError)}`);
|
||||||
|
}
|
||||||
const existingTargetPath = String(item.targetPath || "").trim();
|
const existingTargetPath = String(item.targetPath || "").trim();
|
||||||
const canReuseExistingTarget = existingTargetPath
|
const canReuseExistingTarget = existingTargetPath
|
||||||
&& isPathInsideDir(existingTargetPath, pkg.outputDir)
|
&& isPathInsideDir(existingTargetPath, pkg.outputDir)
|
||||||
|
|||||||
@ -50,7 +50,12 @@ export function readHashManifest(packageDir: string): Map<string, ParsedHashEntr
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const filePath = path.join(packageDir, entry.name);
|
const filePath = path.join(packageDir, entry.name);
|
||||||
const lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/);
|
let lines: string[];
|
||||||
|
try {
|
||||||
|
lines = fs.readFileSync(filePath, "utf8").split(/\r?\n/);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const parsed = parseHashLine(line);
|
const parsed = parseHashLine(line);
|
||||||
if (!parsed) {
|
if (!parsed) {
|
||||||
@ -93,9 +98,12 @@ async function hashFile(filePath: string, algorithm: "crc32" | "md5" | "sha1"):
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hash = crypto.createHash(algorithm);
|
const hash = crypto.createHash(algorithm);
|
||||||
const data = fs.readFileSync(filePath);
|
const stream = fs.createReadStream(filePath, { highWaterMark: 1024 * 1024 });
|
||||||
hash.update(data);
|
return await new Promise<string>((resolve, reject) => {
|
||||||
return hash.digest("hex").toLowerCase();
|
stream.on("data", (chunk: string | Buffer) => hash.update(typeof chunk === "string" ? Buffer.from(chunk) : chunk));
|
||||||
|
stream.on("error", reject);
|
||||||
|
stream.on("end", () => resolve(hash.digest("hex").toLowerCase()));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateFileAgainstManifest(filePath: string, packageDir: string): Promise<{ ok: boolean; message: string }> {
|
export async function validateFileAgainstManifest(filePath: string, packageDir: string): Promise<{ ok: boolean; message: string }> {
|
||||||
|
|||||||
@ -155,7 +155,8 @@ export class MegaWebFallback {
|
|||||||
password,
|
password,
|
||||||
remember: "on"
|
remember: "on"
|
||||||
}),
|
}),
|
||||||
redirect: "manual"
|
redirect: "manual",
|
||||||
|
signal: AbortSignal.timeout(30000)
|
||||||
});
|
});
|
||||||
|
|
||||||
const cookie = parseSetCookieFromHeaders(response.headers);
|
const cookie = parseSetCookieFromHeaders(response.headers);
|
||||||
@ -169,7 +170,8 @@ export class MegaWebFallback {
|
|||||||
"User-Agent": "Mozilla/5.0",
|
"User-Agent": "Mozilla/5.0",
|
||||||
Cookie: cookie,
|
Cookie: cookie,
|
||||||
Referer: DEBRID_REFERER
|
Referer: DEBRID_REFERER
|
||||||
}
|
},
|
||||||
|
signal: AbortSignal.timeout(30000)
|
||||||
});
|
});
|
||||||
const verifyHtml = await verify.text();
|
const verifyHtml = await verify.text();
|
||||||
const hasDebridForm = /id=["']debridForm["']/i.test(verifyHtml) || /name=["']links["']/i.test(verifyHtml);
|
const hasDebridForm = /id=["']debridForm["']/i.test(verifyHtml) || /name=["']links["']/i.test(verifyHtml);
|
||||||
@ -194,7 +196,8 @@ export class MegaWebFallback {
|
|||||||
links: link,
|
links: link,
|
||||||
password: "",
|
password: "",
|
||||||
showLinks: "1"
|
showLinks: "1"
|
||||||
})
|
}),
|
||||||
|
signal: AbortSignal.timeout(30000)
|
||||||
});
|
});
|
||||||
|
|
||||||
const html = await page.text();
|
const html = await page.text();
|
||||||
@ -215,7 +218,8 @@ export class MegaWebFallback {
|
|||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
code,
|
code,
|
||||||
autodl: "0"
|
autodl: "0"
|
||||||
})
|
}),
|
||||||
|
signal: AbortSignal.timeout(15000)
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = (await res.text()).trim();
|
const text = (await res.text()).trim();
|
||||||
|
|||||||
@ -40,7 +40,8 @@ export class RealDebridClient {
|
|||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
"User-Agent": "RD-Node-Downloader/1.1.12"
|
"User-Agent": "RD-Node-Downloader/1.1.12"
|
||||||
},
|
},
|
||||||
body
|
body,
|
||||||
|
signal: AbortSignal.timeout(30000)
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
@ -53,7 +54,12 @@ export class RealDebridClient {
|
|||||||
throw new Error(parsed);
|
throw new Error(parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = JSON.parse(text) as Record<string, unknown>;
|
let payload: Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
payload = JSON.parse(text) as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Ungültige JSON-Antwort: ${text.slice(0, 120)}`);
|
||||||
|
}
|
||||||
const directUrl = String(payload.download || payload.link || "").trim();
|
const directUrl = String(payload.download || payload.link || "").trim();
|
||||||
if (!directUrl) {
|
if (!directUrl) {
|
||||||
throw new Error("Unrestrict ohne Download-URL");
|
throw new Error("Unrestrict ohne Download-URL");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user