Harden Mega web flow and smooth download runtime
Build and Release / build (push) Has been cancelled
Build and Release / build (push) Has been cancelled
This commit is contained in:
@@ -3,7 +3,7 @@ import os from "node:os";
|
||||
import { AppSettings } from "../shared/types";
|
||||
|
||||
export const APP_NAME = "Debrid Download Manager";
|
||||
export const APP_VERSION = "1.1.21";
|
||||
export const APP_VERSION = "1.1.22";
|
||||
export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0";
|
||||
|
||||
export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload";
|
||||
|
||||
@@ -755,6 +755,19 @@ export class DownloadManager extends EventEmitter {
|
||||
let windowBytes = 0;
|
||||
let windowStarted = nowMs();
|
||||
|
||||
const waitDrain = (): Promise<void> => new Promise((resolve, reject) => {
|
||||
const onDrain = (): void => {
|
||||
stream.off("error", onError);
|
||||
resolve();
|
||||
};
|
||||
const onError = (error: Error): void => {
|
||||
stream.off("drain", onDrain);
|
||||
reject(error);
|
||||
};
|
||||
stream.once("drain", onDrain);
|
||||
stream.once("error", onError);
|
||||
});
|
||||
|
||||
try {
|
||||
const body = response.body;
|
||||
if (!body) {
|
||||
@@ -784,7 +797,9 @@ export class DownloadManager extends EventEmitter {
|
||||
|
||||
const buffer = Buffer.from(chunk);
|
||||
await this.applySpeedLimit(buffer.length, windowBytes, windowStarted);
|
||||
stream.write(buffer);
|
||||
if (!stream.write(buffer)) {
|
||||
await waitDrain();
|
||||
}
|
||||
written += buffer.length;
|
||||
windowBytes += buffer.length;
|
||||
this.session.totalDownloadedBytes += buffer.length;
|
||||
@@ -806,8 +821,18 @@ export class DownloadManager extends EventEmitter {
|
||||
this.emitState();
|
||||
}
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => {
|
||||
stream.end(() => resolve());
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const onFinish = (): void => {
|
||||
stream.off("error", onError);
|
||||
resolve();
|
||||
};
|
||||
const onError = (error: Error): void => {
|
||||
stream.off("finish", onFinish);
|
||||
reject(error);
|
||||
};
|
||||
stream.once("finish", onFinish);
|
||||
stream.once("error", onError);
|
||||
stream.end();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -83,8 +83,9 @@ async function hashFile(filePath: string, algorithm: "crc32" | "md5" | "sha1"):
|
||||
const stream = fs.createReadStream(filePath, { highWaterMark: 1024 * 1024 });
|
||||
return await new Promise<string>((resolve, reject) => {
|
||||
let crc = 0;
|
||||
stream.on("data", (chunk: Buffer) => {
|
||||
crc = crc32Buffer(chunk, crc);
|
||||
stream.on("data", (chunk: string | Buffer) => {
|
||||
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
||||
crc = crc32Buffer(buffer, crc);
|
||||
});
|
||||
stream.on("error", reject);
|
||||
stream.on("end", () => resolve(((crc >>> 0).toString(16)).padStart(8, "0").toLowerCase()));
|
||||
|
||||
@@ -20,9 +20,23 @@ function normalizeLink(link: string): string {
|
||||
return link.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function parseSetCookie(raw: string): string {
|
||||
function parseSetCookieFromHeaders(headers: Headers): string {
|
||||
const getSetCookie = (headers as unknown as { getSetCookie?: () => string[] }).getSetCookie;
|
||||
if (typeof getSetCookie === "function") {
|
||||
const values = getSetCookie.call(headers)
|
||||
.map((entry) => entry.split(";")[0].trim())
|
||||
.filter(Boolean);
|
||||
if (values.length > 0) {
|
||||
return values.join("; ");
|
||||
}
|
||||
}
|
||||
|
||||
const raw = headers.get("set-cookie") || "";
|
||||
if (!raw) {
|
||||
return "";
|
||||
}
|
||||
return raw
|
||||
.split(",")
|
||||
.split(/,(?=[^;=]+?=)/g)
|
||||
.map((chunk) => chunk.split(";")[0].trim())
|
||||
.filter(Boolean)
|
||||
.join("; ");
|
||||
@@ -144,10 +158,25 @@ export class MegaWebFallback {
|
||||
redirect: "manual"
|
||||
});
|
||||
|
||||
const cookie = parseSetCookie(response.headers.get("set-cookie") || "");
|
||||
const cookie = parseSetCookieFromHeaders(response.headers);
|
||||
if (!cookie) {
|
||||
throw new Error("Mega-Web Login liefert kein Session-Cookie");
|
||||
}
|
||||
|
||||
const verify = await fetch(DEBRID_REFERER, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
Cookie: cookie,
|
||||
Referer: DEBRID_REFERER
|
||||
}
|
||||
});
|
||||
const verifyHtml = await verify.text();
|
||||
const hasDebridForm = /id=["']debridForm["']/i.test(verifyHtml) || /name=["']links["']/i.test(verifyHtml);
|
||||
if (!hasDebridForm) {
|
||||
throw new Error("Mega-Web Login ungültig oder Session blockiert");
|
||||
}
|
||||
|
||||
this.cookie = cookie;
|
||||
this.cookieSetAt = Date.now();
|
||||
}
|
||||
|
||||
+51
-25
@@ -170,26 +170,36 @@ export function App(): ReactElement {
|
||||
};
|
||||
|
||||
const onAddLinks = async (): Promise<void> => {
|
||||
await window.rd.updateSettings(settingsDraft);
|
||||
const result = await window.rd.addLinks({ rawText: linksRaw, packageName: settingsDraft.packageName });
|
||||
if (result.addedLinks > 0) {
|
||||
setStatusToast(`${result.addedPackages} Paket(e), ${result.addedLinks} Link(s) hinzugefügt`);
|
||||
setLinksRaw("");
|
||||
} else {
|
||||
setStatusToast("Keine gültigen Links gefunden");
|
||||
try {
|
||||
await window.rd.updateSettings(settingsDraft);
|
||||
const result = await window.rd.addLinks({ rawText: linksRaw, packageName: settingsDraft.packageName });
|
||||
if (result.addedLinks > 0) {
|
||||
setStatusToast(`${result.addedPackages} Paket(e), ${result.addedLinks} Link(s) hinzugefügt`);
|
||||
setLinksRaw("");
|
||||
} else {
|
||||
setStatusToast("Keine gültigen Links gefunden");
|
||||
}
|
||||
setTimeout(() => setStatusToast(""), 2200);
|
||||
} catch (error) {
|
||||
setStatusToast(`Fehler beim Hinzufügen: ${String(error)}`);
|
||||
setTimeout(() => setStatusToast(""), 2600);
|
||||
}
|
||||
setTimeout(() => setStatusToast(""), 2200);
|
||||
};
|
||||
|
||||
const onImportDlc = async (): Promise<void> => {
|
||||
const files = await window.rd.pickContainers();
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
try {
|
||||
const files = await window.rd.pickContainers();
|
||||
if (files.length === 0) {
|
||||
return;
|
||||
}
|
||||
await window.rd.updateSettings(settingsDraft);
|
||||
const result = await window.rd.addContainers(files);
|
||||
setStatusToast(`DLC importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
||||
setTimeout(() => setStatusToast(""), 2200);
|
||||
} catch (error) {
|
||||
setStatusToast(`Fehler beim DLC-Import: ${String(error)}`);
|
||||
setTimeout(() => setStatusToast(""), 2600);
|
||||
}
|
||||
await window.rd.updateSettings(settingsDraft);
|
||||
const result = await window.rd.addContainers(files);
|
||||
setStatusToast(`DLC importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
||||
setTimeout(() => setStatusToast(""), 2200);
|
||||
};
|
||||
|
||||
const onDrop = async (event: DragEvent<HTMLTextAreaElement>): Promise<void> => {
|
||||
@@ -202,10 +212,15 @@ export function App(): ReactElement {
|
||||
if (dlc.length === 0) {
|
||||
return;
|
||||
}
|
||||
await window.rd.updateSettings(settingsDraft);
|
||||
const result = await window.rd.addContainers(dlc);
|
||||
setStatusToast(`Drag-and-Drop: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
||||
setTimeout(() => setStatusToast(""), 2200);
|
||||
try {
|
||||
await window.rd.updateSettings(settingsDraft);
|
||||
const result = await window.rd.addContainers(dlc);
|
||||
setStatusToast(`Drag-and-Drop: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
||||
setTimeout(() => setStatusToast(""), 2200);
|
||||
} catch (error) {
|
||||
setStatusToast(`Fehler bei Drag-and-Drop: ${String(error)}`);
|
||||
setTimeout(() => setStatusToast(""), 2600);
|
||||
}
|
||||
};
|
||||
|
||||
const setBool = (key: keyof AppSettings, value: boolean): void => {
|
||||
@@ -220,6 +235,15 @@ export function App(): ReactElement {
|
||||
setSettingsDraft((prev: AppSettings) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const performQuickAction = async (action: () => Promise<unknown>): Promise<void> => {
|
||||
try {
|
||||
await action();
|
||||
} catch (error) {
|
||||
setStatusToast(`Fehler: ${String(error)}`);
|
||||
setTimeout(() => setStatusToast(""), 2600);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<header className="top-header">
|
||||
@@ -239,15 +263,17 @@ export function App(): ReactElement {
|
||||
className="btn accent"
|
||||
disabled={!snapshot.canStart}
|
||||
onClick={async () => {
|
||||
await window.rd.updateSettings(settingsDraft);
|
||||
await window.rd.start();
|
||||
await performQuickAction(async () => {
|
||||
await window.rd.updateSettings(settingsDraft);
|
||||
await window.rd.start();
|
||||
});
|
||||
}}
|
||||
>Start</button>
|
||||
<button className="btn" disabled={!snapshot.canPause} onClick={() => window.rd.togglePause()}>
|
||||
<button className="btn" disabled={!snapshot.canPause} onClick={() => { void performQuickAction(() => window.rd.togglePause()); }}>
|
||||
{snapshot.session.paused ? "Resume" : "Pause"}
|
||||
</button>
|
||||
<button className="btn" disabled={!snapshot.canStop} onClick={() => window.rd.stop()}>Stop</button>
|
||||
<button className="btn" onClick={() => window.rd.clearAll()}>Alles leeren</button>
|
||||
<button className="btn" disabled={!snapshot.canStop} onClick={() => { void performQuickAction(() => window.rd.stop()); }}>Stop</button>
|
||||
<button className="btn" onClick={() => { void performQuickAction(() => window.rd.clearAll()); }}>Alles leeren</button>
|
||||
</div>
|
||||
<div className="speed-config">
|
||||
<label>
|
||||
@@ -310,7 +336,7 @@ export function App(): ReactElement {
|
||||
key={pkg.id}
|
||||
pkg={pkg}
|
||||
items={pkg.itemIds.map((id) => snapshot.session.items[id]).filter(Boolean)}
|
||||
onCancel={() => window.rd.cancelPackage(pkg.id)}
|
||||
onCancel={() => { void performQuickAction(() => window.rd.cancelPackage(pkg.id)); }}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user