Add multi-provider fallback with AllDebrid and fix packaged UI path
Build and Release / build (push) Has been cancelled

This commit is contained in:
Sucukdeluxe
2026-02-27 04:02:31 +01:00
parent f27584d6ee
commit cbc1ffa18b
15 changed files with 975 additions and 27 deletions
+5 -1
View File
@@ -28,13 +28,17 @@ export class AppController {
if (this.settings.autoResumeOnStart) {
const snapshot = this.manager.getSnapshot();
const hasPending = Object.values(snapshot.session.items).some((item) => item.status === "queued" || item.status === "reconnect_wait");
if (hasPending && this.settings.token.trim()) {
if (hasPending && this.hasAnyProviderToken(this.settings)) {
this.manager.start();
logger.info("Auto-Resume beim Start aktiviert");
}
}
}
private hasAnyProviderToken(settings: AppSettings): boolean {
return Boolean(settings.token.trim() || settings.megaToken.trim() || settings.bestToken.trim() || settings.allDebridToken.trim());
}
public onState: ((snapshot: UiSnapshot) => void) | null = null;
public getSnapshot(): UiSnapshot {
+9 -2
View File
@@ -2,8 +2,8 @@ import path from "node:path";
import os from "node:os";
import { AppSettings } from "../shared/types";
export const APP_NAME = "Real-Debrid Download Manager";
export const APP_VERSION = "1.1.11";
export const APP_NAME = "Debrid Download Manager";
export const APP_VERSION = "1.1.12";
export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0";
export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload";
@@ -28,7 +28,14 @@ export function defaultSettings(): AppSettings {
const baseDir = path.join(os.homedir(), "Downloads", "RealDebrid");
return {
token: "",
megaToken: "",
bestToken: "",
allDebridToken: "",
rememberToken: true,
providerPrimary: "realdebrid",
providerSecondary: "megadebrid",
providerTertiary: "bestdebrid",
autoProviderFallback: true,
outputDir: baseDir,
packageName: "",
autoExtract: true,
+408
View File
@@ -0,0 +1,408 @@
import { AppSettings, DebridProvider } from "../shared/types";
import { REQUEST_RETRIES } from "./constants";
import { RealDebridClient, UnrestrictedLink } from "./realdebrid";
import { compactErrorText, filenameFromUrl, sleep } from "./utils";
const MEGA_DEBRID_API = "https://www.mega-debrid.eu/api.php";
const BEST_DEBRID_API_BASE = "https://bestdebrid.com/api/v1";
const ALL_DEBRID_API_BASE = "https://api.alldebrid.com/v4";
const PROVIDER_LABELS: Record<DebridProvider, string> = {
realdebrid: "Real-Debrid",
megadebrid: "Mega-Debrid",
bestdebrid: "BestDebrid",
alldebrid: "AllDebrid"
};
interface ProviderUnrestrictedLink extends UnrestrictedLink {
provider: DebridProvider;
providerLabel: string;
}
type BestDebridRequest = {
url: string;
useAuthHeader: boolean;
};
function shouldRetryStatus(status: number): boolean {
return status === 429 || status >= 500;
}
function retryDelay(attempt: number): number {
return Math.min(5000, 400 * 2 ** attempt);
}
function asRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
function parseJson(text: string): unknown {
try {
return JSON.parse(text) as unknown;
} catch {
return null;
}
}
function pickString(payload: Record<string, unknown> | null, keys: string[]): string {
if (!payload) {
return "";
}
for (const key of keys) {
const value = payload[key];
if (typeof value === "string" && value.trim()) {
return value.trim();
}
}
return "";
}
function pickNumber(payload: Record<string, unknown> | null, keys: string[]): number | null {
if (!payload) {
return null;
}
for (const key of keys) {
const value = Number(payload[key] ?? NaN);
if (Number.isFinite(value) && value > 0) {
return Math.floor(value);
}
}
return null;
}
function parseError(status: number, responseText: string, payload: Record<string, unknown> | null): string {
const fromPayload = pickString(payload, ["response_text", "error", "message", "detail", "error_description"]);
if (fromPayload) {
return fromPayload;
}
const compact = compactErrorText(responseText);
if (compact && compact !== "Unbekannter Fehler") {
return compact;
}
return `HTTP ${status}`;
}
function uniqueProviderOrder(order: DebridProvider[]): DebridProvider[] {
const seen = new Set<DebridProvider>();
const result: DebridProvider[] = [];
for (const provider of order) {
if (seen.has(provider)) {
continue;
}
seen.add(provider);
result.push(provider);
}
return result;
}
function buildBestDebridRequests(link: string, token: string): BestDebridRequest[] {
const linkParam = encodeURIComponent(link);
const authParam = encodeURIComponent(token);
return [
{
url: `${BEST_DEBRID_API_BASE}/generateLink?link=${linkParam}`,
useAuthHeader: true
},
{
url: `${BEST_DEBRID_API_BASE}/generateLink?auth=${authParam}&link=${linkParam}`,
useAuthHeader: false
}
];
}
class MegaDebridClient {
private token: string;
public constructor(token: string) {
this.token = token;
}
public async unrestrictLink(link: string): Promise<UnrestrictedLink> {
let lastError = "";
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
try {
const body = new URLSearchParams({ link });
const response = await fetch(`${MEGA_DEBRID_API}?action=getLink&token=${encodeURIComponent(this.token)}`, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "RD-Node-Downloader/1.1.12"
},
body
});
const text = await response.text();
const payload = asRecord(parseJson(text));
if (!response.ok) {
const reason = parseError(response.status, text, payload);
if (shouldRetryStatus(response.status) && attempt < REQUEST_RETRIES) {
await sleep(retryDelay(attempt));
continue;
}
throw new Error(reason);
}
const responseCode = pickString(payload, ["response_code"]);
if (responseCode && responseCode.toLowerCase() !== "ok") {
throw new Error(pickString(payload, ["response_text"]) || responseCode);
}
const directUrl = pickString(payload, ["debridLink", "download", "link"]);
if (!directUrl) {
throw new Error("Mega-Debrid Antwort ohne debridLink");
}
const fileName = pickString(payload, ["filename", "fileName"]) || filenameFromUrl(link);
const fileSize = pickNumber(payload, ["filesize", "size"]);
return {
fileName,
directUrl,
fileSize,
retriesUsed: attempt - 1
};
} catch (error) {
lastError = compactErrorText(error);
if (attempt >= REQUEST_RETRIES) {
break;
}
await sleep(retryDelay(attempt));
}
}
throw new Error(lastError || "Mega-Debrid Unrestrict fehlgeschlagen");
}
}
class BestDebridClient {
private token: string;
public constructor(token: string) {
this.token = token;
}
public async unrestrictLink(link: string): Promise<UnrestrictedLink> {
const requests = buildBestDebridRequests(link, this.token);
let lastError = "";
for (const request of requests) {
try {
return await this.tryRequest(request, link);
} catch (error) {
lastError = compactErrorText(error);
}
}
throw new Error(lastError || "BestDebrid Unrestrict fehlgeschlagen");
}
private async tryRequest(request: BestDebridRequest, originalLink: string): Promise<UnrestrictedLink> {
let lastError = "";
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
try {
const headers: Record<string, string> = {
"User-Agent": "RD-Node-Downloader/1.1.12"
};
if (request.useAuthHeader) {
headers.Authorization = this.token;
}
const response = await fetch(request.url, {
method: "GET",
headers
});
const text = await response.text();
const parsed = parseJson(text);
const payload = Array.isArray(parsed) ? asRecord(parsed[0]) : asRecord(parsed);
if (!response.ok) {
const reason = parseError(response.status, text, payload);
if (shouldRetryStatus(response.status) && attempt < REQUEST_RETRIES) {
await sleep(retryDelay(attempt));
continue;
}
throw new Error(reason);
}
const directUrl = pickString(payload, ["download", "debridLink", "link"]);
if (directUrl) {
const fileName = pickString(payload, ["filename", "fileName"]) || filenameFromUrl(originalLink);
const fileSize = pickNumber(payload, ["filesize", "size", "bytes"]);
return {
fileName,
directUrl,
fileSize,
retriesUsed: attempt - 1
};
}
const message = pickString(payload, ["response_text", "message", "error"]);
if (message) {
throw new Error(message);
}
throw new Error("BestDebrid Antwort ohne Download-Link");
} catch (error) {
lastError = compactErrorText(error);
if (attempt >= REQUEST_RETRIES) {
break;
}
await sleep(retryDelay(attempt));
}
}
throw new Error(lastError || "BestDebrid Request fehlgeschlagen");
}
}
class AllDebridClient {
private token: string;
public constructor(token: string) {
this.token = token;
}
public async unrestrictLink(link: string): Promise<UnrestrictedLink> {
let lastError = "";
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
try {
const response = await fetch(`${ALL_DEBRID_API_BASE}/link/unlock`, {
method: "POST",
headers: {
Authorization: `Bearer ${this.token}`,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "RD-Node-Downloader/1.1.12"
},
body: new URLSearchParams({ link })
});
const text = await response.text();
const payload = asRecord(parseJson(text));
if (!response.ok) {
const reason = parseError(response.status, text, payload);
if (shouldRetryStatus(response.status) && attempt < REQUEST_RETRIES) {
await sleep(retryDelay(attempt));
continue;
}
throw new Error(reason);
}
const status = pickString(payload, ["status"]);
if (status && status.toLowerCase() === "error") {
const errorObj = asRecord(payload?.error);
throw new Error(pickString(errorObj, ["message", "code"]) || "AllDebrid API error");
}
const data = asRecord(payload?.data);
const directUrl = pickString(data, ["link"]);
if (!directUrl) {
throw new Error("AllDebrid Antwort ohne Download-Link");
}
return {
fileName: pickString(data, ["filename"]) || filenameFromUrl(link),
directUrl,
fileSize: pickNumber(data, ["filesize"]),
retriesUsed: attempt - 1
};
} catch (error) {
lastError = compactErrorText(error);
if (attempt >= REQUEST_RETRIES) {
break;
}
await sleep(retryDelay(attempt));
}
}
throw new Error(lastError || "AllDebrid Unrestrict fehlgeschlagen");
}
}
export class DebridService {
private settings: AppSettings;
private realDebridClient: RealDebridClient;
private allDebridClient: AllDebridClient;
public constructor(settings: AppSettings) {
this.settings = settings;
this.realDebridClient = new RealDebridClient(settings.token);
this.allDebridClient = new AllDebridClient(settings.allDebridToken);
}
public setSettings(next: AppSettings): void {
this.settings = next;
this.realDebridClient = new RealDebridClient(next.token);
this.allDebridClient = new AllDebridClient(next.allDebridToken);
}
public async unrestrictLink(link: string): Promise<ProviderUnrestrictedLink> {
const order = uniqueProviderOrder([
this.settings.providerPrimary,
this.settings.providerSecondary,
this.settings.providerTertiary,
"realdebrid",
"megadebrid",
"bestdebrid",
"alldebrid"
]);
let configuredFound = false;
const attempts: string[] = [];
for (const provider of order) {
const token = this.getProviderToken(provider).trim();
if (!token) {
continue;
}
configuredFound = true;
if (!this.settings.autoProviderFallback && attempts.length > 0) {
break;
}
try {
const result = await this.unrestrictViaProvider(provider, link, token);
return {
...result,
provider,
providerLabel: PROVIDER_LABELS[provider]
};
} catch (error) {
attempts.push(`${PROVIDER_LABELS[provider]}: ${compactErrorText(error)}`);
}
}
if (!configuredFound) {
throw new Error("Kein Debrid-Provider konfiguriert (API-Key fehlt)");
}
throw new Error(`Unrestrict fehlgeschlagen: ${attempts.join(" | ")}`);
}
private getProviderToken(provider: DebridProvider): string {
if (provider === "realdebrid") {
return this.settings.token;
}
if (provider === "megadebrid") {
return this.settings.megaToken;
}
if (provider === "alldebrid") {
return this.settings.allDebridToken;
}
return this.settings.bestToken;
}
private async unrestrictViaProvider(provider: DebridProvider, link: string, token: string): Promise<UnrestrictedLink> {
if (provider === "realdebrid") {
return this.realDebridClient.unrestrictLink(link);
}
if (provider === "megadebrid") {
return new MegaDebridClient(token).unrestrictLink(link);
}
if (provider === "alldebrid") {
return this.allDebridClient.unrestrictLink(link);
}
return new BestDebridClient(token).unrestrictLink(link);
}
}
+29 -11
View File
@@ -6,10 +6,10 @@ import { v4 as uuidv4 } from "uuid";
import { AppSettings, DownloadItem, DownloadSummary, DownloadStatus, PackageEntry, ParsedPackageInput, SessionState, UiSnapshot } from "../shared/types";
import { CHUNK_SIZE, REQUEST_RETRIES } from "./constants";
import { cleanupCancelledPackageArtifacts, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
import { DebridService } from "./debrid";
import { extractPackageArchives } from "./extractor";
import { validateFileAgainstManifest } from "./integrity";
import { logger } from "./logger";
import { RealDebridClient } from "./realdebrid";
import { StoragePaths, saveSession } from "./storage";
import { compactErrorText, ensureDirPath, filenameFromUrl, formatEta, humanSize, nowMs, sanitizeFilename, sleep } from "./utils";
@@ -47,6 +47,22 @@ function isFinishedStatus(status: DownloadStatus): boolean {
return status === "completed" || status === "failed" || status === "cancelled";
}
function providerLabel(provider: DownloadItem["provider"]): string {
if (provider === "realdebrid") {
return "Real-Debrid";
}
if (provider === "megadebrid") {
return "Mega-Debrid";
}
if (provider === "bestdebrid") {
return "BestDebrid";
}
if (provider === "alldebrid") {
return "AllDebrid";
}
return "Debrid";
}
function nextAvailablePath(targetPath: string): string {
if (!fs.existsSync(targetPath)) {
return targetPath;
@@ -69,7 +85,7 @@ export class DownloadManager extends EventEmitter {
private storagePaths: StoragePaths;
private rdClient: RealDebridClient;
private debridService: DebridService;
private activeTasks = new Map<string, ActiveTask>();
@@ -88,17 +104,14 @@ export class DownloadManager extends EventEmitter {
this.settings = settings;
this.session = cloneSession(session);
this.storagePaths = storagePaths;
this.rdClient = new RealDebridClient(settings.token);
this.debridService = new DebridService(settings);
this.applyOnStartCleanupPolicy();
this.normalizeSessionStatuses();
}
public setSettings(next: AppSettings): void {
const tokenChanged = next.token !== this.settings.token;
this.settings = next;
if (tokenChanged) {
this.rdClient = new RealDebridClient(next.token);
}
this.debridService.setSettings(next);
this.emitState();
}
@@ -180,6 +193,7 @@ export class DownloadManager extends EventEmitter {
id: itemId,
packageId,
url: link,
provider: null,
status: "queued",
retries: 0,
speedBps: 0,
@@ -278,6 +292,9 @@ export class DownloadManager extends EventEmitter {
private normalizeSessionStatuses(): void {
for (const item of Object.values(this.session.items)) {
if (item.provider !== "realdebrid" && item.provider !== "megadebrid" && item.provider !== "bestdebrid" && item.provider !== "alldebrid") {
item.provider = null;
}
if (item.status === "downloading" || item.status === "validating" || item.status === "extracting" || item.status === "integrity_check") {
item.status = "queued";
item.speedBps = 0;
@@ -440,7 +457,7 @@ export class DownloadManager extends EventEmitter {
}
item.status = "validating";
item.fullStatus = "Link wird via Real-Debrid umgewandelt";
item.fullStatus = "Link wird umgewandelt";
item.updatedAt = nowMs();
pkg.status = "downloading";
pkg.updatedAt = nowMs();
@@ -475,14 +492,15 @@ export class DownloadManager extends EventEmitter {
}
try {
const unrestricted = await this.rdClient.unrestrictLink(item.url);
const unrestricted = await this.debridService.unrestrictLink(item.url);
item.provider = unrestricted.provider;
item.retries = unrestricted.retriesUsed;
item.fileName = sanitizeFilename(unrestricted.fileName || filenameFromUrl(item.url));
fs.mkdirSync(pkg.outputDir, { recursive: true });
item.targetPath = nextAvailablePath(path.join(pkg.outputDir, item.fileName));
item.totalBytes = unrestricted.fileSize;
item.status = "downloading";
item.fullStatus = "Download läuft";
item.fullStatus = `Download läuft (${unrestricted.providerLabel})`;
item.updatedAt = nowMs();
this.emitState();
@@ -693,7 +711,7 @@ export class DownloadManager extends EventEmitter {
item.speedBps = Math.max(0, Math.floor(speed));
item.downloadedBytes = written;
item.progressPercent = item.totalBytes ? Math.max(0, Math.min(100, Math.floor((written / item.totalBytes) * 100))) : 0;
item.fullStatus = "Download läuft";
item.fullStatus = `Download läuft (${providerLabel(item.provider)})`;
item.updatedAt = nowMs();
this.emitState();
}
+3 -2
View File
@@ -4,6 +4,7 @@ import { AddLinksPayload, AppSettings } from "../shared/types";
import { AppController } from "./app-controller";
import { IPC_CHANNELS } from "../shared/ipc";
import { logger } from "./logger";
import { APP_NAME } from "./constants";
let mainWindow: BrowserWindow | null = null;
const controller = new AppController();
@@ -19,7 +20,7 @@ function createWindow(): BrowserWindow {
minWidth: 1120,
minHeight: 760,
backgroundColor: "#070b14",
title: `Real-Debrid Download Manager v${controller.getVersion()}`,
title: `${APP_NAME} v${controller.getVersion()}`,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
@@ -30,7 +31,7 @@ function createWindow(): BrowserWindow {
if (isDevMode()) {
void window.loadURL("http://localhost:5173");
} else {
void window.loadFile(path.join(__dirname, "../renderer/index.html"));
void window.loadFile(path.join(app.getAppPath(), "build", "renderer", "index.html"));
}
return window;
+1 -1
View File
@@ -38,7 +38,7 @@ export class RealDebridClient {
headers: {
Authorization: `Bearer ${this.token}`,
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "RD-Node-Downloader/1.1.9"
"User-Agent": "RD-Node-Downloader/1.1.12"
},
body
});
+12
View File
@@ -4,6 +4,8 @@ import { AppSettings, SessionState } from "../shared/types";
import { defaultSettings } from "./constants";
import { logger } from "./logger";
const VALID_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid"]);
export interface StoragePaths {
baseDir: string;
configFile: string;
@@ -33,6 +35,16 @@ export function loadSettings(paths: StoragePaths): AppSettings {
...defaultSettings(),
...parsed
};
if (!VALID_PROVIDERS.has(merged.providerPrimary)) {
merged.providerPrimary = "realdebrid";
}
if (!VALID_PROVIDERS.has(merged.providerSecondary)) {
merged.providerSecondary = "megadebrid";
}
if (!VALID_PROVIDERS.has(merged.providerTertiary)) {
merged.providerTertiary = "bestdebrid";
}
merged.autoProviderFallback = Boolean(merged.autoProviderFallback);
merged.maxParallel = Math.max(1, Math.min(50, Number(merged.maxParallel) || 4));
merged.speedLimitKbps = Math.max(0, Math.min(500000, Number(merged.speedLimitKbps) || 0));
merged.reconnectWaitSeconds = Math.max(10, Math.min(600, Number(merged.reconnectWaitSeconds) || 45));
+66 -6
View File
@@ -1,12 +1,19 @@
import { DragEvent, ReactElement, useEffect, useMemo, useState } from "react";
import type { AppSettings, DownloadItem, PackageEntry, UiSnapshot } from "../shared/types";
import type { AppSettings, DebridProvider, DownloadItem, PackageEntry, UiSnapshot } from "../shared/types";
type Tab = "collector" | "downloads" | "settings";
const emptySnapshot = (): UiSnapshot => ({
settings: {
token: "",
megaToken: "",
bestToken: "",
allDebridToken: "",
rememberToken: true,
providerPrimary: "realdebrid",
providerSecondary: "megadebrid",
providerTertiary: "bestdebrid",
autoProviderFallback: true,
outputDir: "",
packageName: "",
autoExtract: true,
@@ -58,6 +65,13 @@ const cleanupLabels: Record<string, string> = {
package_done: "Sobald Paket fertig ist"
};
const providerLabels: Record<DebridProvider, string> = {
realdebrid: "Real-Debrid",
megadebrid: "Mega-Debrid",
bestdebrid: "BestDebrid",
alldebrid: "AllDebrid"
};
export function App(): ReactElement {
const [snapshot, setSnapshot] = useState<UiSnapshot>(emptySnapshot);
const [tab, setTab] = useState<Tab>("collector");
@@ -144,8 +158,8 @@ export function App(): ReactElement {
<div className="app-shell">
<header className="top-header">
<div className="title-block">
<h1>Real-Debrid Download Manager</h1>
<span>JDownloader-Style Workflow</span>
<h1>Debrid Download Manager</h1>
<span>Multi-Provider Workflow</span>
</div>
<div className="metrics">
<div>{snapshot.speedText}</div>
@@ -197,20 +211,64 @@ export function App(): ReactElement {
{tab === "collector" && (
<section className="grid-two">
<article className="card">
<h3>Authentifizierung</h3>
<label>API Token</label>
<h3>Debrid Provider</h3>
<label>Real-Debrid API Token</label>
<input
type="password"
value={settingsDraft.token}
onChange={(event) => setText("token", event.target.value)}
/>
<label>Mega-Debrid API Token</label>
<input
type="password"
value={settingsDraft.megaToken}
onChange={(event) => setText("megaToken", event.target.value)}
/>
<label>BestDebrid API Token</label>
<input
type="password"
value={settingsDraft.bestToken}
onChange={(event) => setText("bestToken", event.target.value)}
/>
<label>AllDebrid API Key</label>
<input
type="password"
value={settingsDraft.allDebridToken}
onChange={(event) => setText("allDebridToken", event.target.value)}
/>
<label>Primärer Provider</label>
<select value={settingsDraft.providerPrimary} onChange={(event) => setText("providerPrimary", event.target.value)}>
{Object.entries(providerLabels).map(([key, label]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
<label>Sekundärer Provider</label>
<select value={settingsDraft.providerSecondary} onChange={(event) => setText("providerSecondary", event.target.value)}>
{Object.entries(providerLabels).map(([key, label]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
<label>Tertiärer Provider</label>
<select value={settingsDraft.providerTertiary} onChange={(event) => setText("providerTertiary", event.target.value)}>
{Object.entries(providerLabels).map(([key, label]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
<label>
<input
type="checkbox"
checked={settingsDraft.autoProviderFallback}
onChange={(event) => setBool("autoProviderFallback", event.target.checked)}
/>
Bei Fehler/Fair-Use automatisch zum nächsten Provider wechseln
</label>
<label>
<input
type="checkbox"
checked={settingsDraft.rememberToken}
onChange={(event) => setBool("rememberToken", event.target.checked)}
/>
Token lokal speichern
API Keys lokal speichern
</label>
<label>GitHub Repo</label>
<input value={settingsDraft.updateRepo} onChange={(event) => setText("updateRepo", event.target.value)} />
@@ -363,6 +421,7 @@ function PackageCard({ pkg, items, onCancel }: { pkg: PackageEntry; items: Downl
<thead>
<tr>
<th>Datei</th>
<th>Provider</th>
<th>Status</th>
<th>Fortschritt</th>
<th>Speed</th>
@@ -373,6 +432,7 @@ function PackageCard({ pkg, items, onCancel }: { pkg: PackageEntry; items: Downl
{items.map((item) => (
<tr key={item.id}>
<td>{item.fileName}</td>
<td>{item.provider ? providerLabels[item.provider] : "-"}</td>
<td title={item.fullStatus}>{item.fullStatus}</td>
<td>{item.progressPercent}%</td>
<td>{item.speedBps > 0 ? `${Math.floor(item.speedBps / 1024)} KB/s` : "0 KB/s"}</td>
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Real-Debrid Download Manager</title>
<title>Debrid Download Manager</title>
</head>
<body>
<div id="root"></div>
+9
View File
@@ -14,10 +14,18 @@ export type CleanupMode = "none" | "trash" | "delete";
export type ConflictMode = "overwrite" | "skip" | "rename" | "ask";
export type SpeedMode = "global" | "per_download";
export type FinishedCleanupPolicy = "never" | "immediate" | "on_start" | "package_done";
export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid";
export interface AppSettings {
token: string;
megaToken: string;
bestToken: string;
allDebridToken: string;
rememberToken: boolean;
providerPrimary: DebridProvider;
providerSecondary: DebridProvider;
providerTertiary: DebridProvider;
autoProviderFallback: boolean;
outputDir: string;
packageName: string;
autoExtract: boolean;
@@ -45,6 +53,7 @@ export interface DownloadItem {
id: string;
packageId: string;
url: string;
provider: DebridProvider | null;
status: DownloadStatus;
retries: number;
speedBps: number;