Switch MegaDebrid to web-only flow and reduce UI lag
Some checks are pending
Build and Release / build (push) Waiting to run
Some checks are pending
Build and Release / build (push) Waiting to run
This commit is contained in:
parent
b1b8ed4180
commit
40bfda2ad7
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.1.20",
|
"version": "1.1.21",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.1.20",
|
"version": "1.1.21",
|
||||||
"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.1.20",
|
"version": "1.1.21",
|
||||||
"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",
|
||||||
|
|||||||
@ -10,7 +10,8 @@ const links = [
|
|||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
token: process.env.RD_TOKEN || "",
|
token: process.env.RD_TOKEN || "",
|
||||||
megaToken: process.env.MEGA_TOKEN || "",
|
megaLogin: process.env.MEGA_LOGIN || "",
|
||||||
|
megaPassword: process.env.MEGA_PASSWORD || "",
|
||||||
bestToken: process.env.BEST_TOKEN || "",
|
bestToken: process.env.BEST_TOKEN || "",
|
||||||
allDebridToken: process.env.ALLDEBRID_TOKEN || "",
|
allDebridToken: process.env.ALLDEBRID_TOKEN || "",
|
||||||
providerPrimary: "alldebrid" as const,
|
providerPrimary: "alldebrid" as const,
|
||||||
@ -19,8 +20,8 @@ const settings = {
|
|||||||
autoProviderFallback: true
|
autoProviderFallback: true
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!settings.token && !settings.megaToken && !settings.bestToken && !settings.allDebridToken) {
|
if (!settings.token && !(settings.megaLogin && settings.megaPassword) && !settings.bestToken && !settings.allDebridToken) {
|
||||||
console.error("No provider tokens set. Use RD_TOKEN/MEGA_TOKEN/BEST_TOKEN/ALLDEBRID_TOKEN.");
|
console.error("No provider credentials set. Use RD_TOKEN or MEGA_LOGIN+MEGA_PASSWORD or BEST_TOKEN or ALLDEBRID_TOKEN.");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -46,7 +46,12 @@ export class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private hasAnyProviderToken(settings: AppSettings): boolean {
|
private hasAnyProviderToken(settings: AppSettings): boolean {
|
||||||
return Boolean(settings.token.trim() || settings.megaToken.trim() || settings.bestToken.trim() || settings.allDebridToken.trim());
|
return Boolean(
|
||||||
|
settings.token.trim()
|
||||||
|
|| (settings.megaLogin.trim() && settings.megaPassword.trim())
|
||||||
|
|| settings.bestToken.trim()
|
||||||
|
|| settings.allDebridToken.trim()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onState: ((snapshot: UiSnapshot) => void) | null = null;
|
public onState: ((snapshot: UiSnapshot) => void) | null = null;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import os from "node:os";
|
|||||||
import { AppSettings } from "../shared/types";
|
import { AppSettings } from "../shared/types";
|
||||||
|
|
||||||
export const APP_NAME = "Debrid Download Manager";
|
export const APP_NAME = "Debrid Download Manager";
|
||||||
export const APP_VERSION = "1.1.20";
|
export const APP_VERSION = "1.1.21";
|
||||||
export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0";
|
export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0";
|
||||||
|
|
||||||
export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload";
|
export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload";
|
||||||
@ -28,7 +28,6 @@ export function defaultSettings(): AppSettings {
|
|||||||
const baseDir = path.join(os.homedir(), "Downloads", "RealDebrid");
|
const baseDir = path.join(os.homedir(), "Downloads", "RealDebrid");
|
||||||
return {
|
return {
|
||||||
token: "",
|
token: "",
|
||||||
megaToken: "",
|
|
||||||
megaLogin: "",
|
megaLogin: "",
|
||||||
megaPassword: "",
|
megaPassword: "",
|
||||||
bestToken: "",
|
bestToken: "",
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
import { AppSettings, DebridProvider } from "../shared/types";
|
import { AppSettings, DebridProvider } from "../shared/types";
|
||||||
import { createHash } from "node:crypto";
|
|
||||||
import { REQUEST_RETRIES } from "./constants";
|
import { REQUEST_RETRIES } from "./constants";
|
||||||
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 MEGA_DEBRID_API = "https://www.mega-debrid.eu/api.php";
|
|
||||||
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";
|
||||||
|
|
||||||
@ -195,135 +193,31 @@ function buildBestDebridRequests(link: string, token: string): BestDebridRequest
|
|||||||
}
|
}
|
||||||
|
|
||||||
class MegaDebridClient {
|
class MegaDebridClient {
|
||||||
private token: string;
|
|
||||||
|
|
||||||
private megaWebUnrestrict?: MegaWebUnrestrictor;
|
private megaWebUnrestrict?: MegaWebUnrestrictor;
|
||||||
|
|
||||||
public constructor(token: string, megaWebUnrestrict?: MegaWebUnrestrictor) {
|
public constructor(megaWebUnrestrict?: MegaWebUnrestrictor) {
|
||||||
this.token = token;
|
|
||||||
this.megaWebUnrestrict = megaWebUnrestrict;
|
this.megaWebUnrestrict = megaWebUnrestrict;
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeMegaCandidates(link: string): string[] {
|
|
||||||
const result = new Set<string>();
|
|
||||||
const trimmed = link.trim();
|
|
||||||
if (trimmed) {
|
|
||||||
result.add(trimmed);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = new URL(trimmed);
|
|
||||||
const host = parsed.hostname.toLowerCase();
|
|
||||||
if (host.includes("rapidgator.net")) {
|
|
||||||
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
||||||
const fileIdx = parts.findIndex((part) => part.toLowerCase() === "file");
|
|
||||||
if (fileIdx >= 0 && parts[fileIdx + 1]) {
|
|
||||||
const hash = parts[fileIdx + 1];
|
|
||||||
result.add(`https://rapidgator.net/file/${hash}`);
|
|
||||||
result.add(`http://rapidgator.net/file/${hash}`);
|
|
||||||
if (parts[fileIdx + 2]) {
|
|
||||||
const name = parts[fileIdx + 2].replace(/\.html$/i, "");
|
|
||||||
result.add(`https://rapidgator.net/file/${hash}/${name}.html`);
|
|
||||||
result.add(`http://rapidgator.net/file/${hash}/${name}.html`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore malformed URL
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...result];
|
|
||||||
}
|
|
||||||
|
|
||||||
private async requestMega(link: string, includePasswordField: boolean, useGetLinkParam: boolean): Promise<UnrestrictedLink> {
|
|
||||||
const url = `${MEGA_DEBRID_API}?action=getLink&token=${encodeURIComponent(this.token)}${useGetLinkParam ? `&link=${encodeURIComponent(link)}` : ""}`;
|
|
||||||
const body = new URLSearchParams();
|
|
||||||
if (!useGetLinkParam) {
|
|
||||||
body.set("link", link);
|
|
||||||
}
|
|
||||||
if (includePasswordField) {
|
|
||||||
body.set("password", createHash("md5").update("").digest("hex"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
"User-Agent": "RD-Node-Downloader/1.1.17"
|
|
||||||
},
|
|
||||||
body
|
|
||||||
});
|
|
||||||
const text = await response.text();
|
|
||||||
const payload = asRecord(parseJson(text));
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(parseError(response.status, text, payload));
|
|
||||||
}
|
|
||||||
|
|
||||||
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: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unrestrictLink(link: string): Promise<UnrestrictedLink> {
|
public async unrestrictLink(link: string): Promise<UnrestrictedLink> {
|
||||||
|
if (!this.megaWebUnrestrict) {
|
||||||
|
throw new Error("Mega-Web-Fallback nicht verfügbar");
|
||||||
|
}
|
||||||
let lastError = "";
|
let lastError = "";
|
||||||
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
|
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
|
||||||
try {
|
const web = await this.megaWebUnrestrict(link).catch((error) => {
|
||||||
const candidates = this.normalizeMegaCandidates(link);
|
|
||||||
const variants = [
|
|
||||||
{ includePasswordField: false, useGetLinkParam: false },
|
|
||||||
{ includePasswordField: true, useGetLinkParam: false },
|
|
||||||
{ includePasswordField: false, useGetLinkParam: true },
|
|
||||||
{ includePasswordField: true, useGetLinkParam: true }
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
|
||||||
for (const variant of variants) {
|
|
||||||
try {
|
|
||||||
const out = await this.requestMega(candidate, variant.includePasswordField, variant.useGetLinkParam);
|
|
||||||
out.retriesUsed = attempt - 1;
|
|
||||||
return out;
|
|
||||||
} catch (error) {
|
|
||||||
lastError = compactErrorText(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/token error|vip_end/i.test(lastError)) {
|
|
||||||
throw new Error(lastError);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/UNRESTRICTING_ERROR_1/i.test(lastError) && this.megaWebUnrestrict) {
|
|
||||||
const web = await this.megaWebUnrestrict(link);
|
|
||||||
if (web?.directUrl) {
|
|
||||||
web.retriesUsed = attempt - 1;
|
|
||||||
return web;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
lastError = compactErrorText(error);
|
lastError = compactErrorText(error);
|
||||||
if (attempt >= REQUEST_RETRIES) {
|
return null;
|
||||||
break;
|
});
|
||||||
}
|
if (web?.directUrl) {
|
||||||
|
web.retriesUsed = attempt - 1;
|
||||||
|
return web;
|
||||||
|
}
|
||||||
|
if (attempt < REQUEST_RETRIES) {
|
||||||
await sleep(retryDelay(attempt));
|
await sleep(retryDelay(attempt));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error(lastError || "Mega-Debrid Unrestrict fehlgeschlagen");
|
throw new Error(lastError || "Mega-Web Unrestrict fehlgeschlagen");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -608,8 +502,7 @@ export class DebridService {
|
|||||||
const attempts: string[] = [];
|
const attempts: string[] = [];
|
||||||
|
|
||||||
for (const provider of order) {
|
for (const provider of order) {
|
||||||
const token = this.getProviderToken(provider).trim();
|
if (!this.isProviderConfigured(provider)) {
|
||||||
if (!token) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
configuredFound = true;
|
configuredFound = true;
|
||||||
@ -618,7 +511,7 @@ export class DebridService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.unrestrictViaProvider(provider, link, token);
|
const result = await this.unrestrictViaProvider(provider, link);
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
provider,
|
provider,
|
||||||
@ -630,35 +523,35 @@ export class DebridService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!configuredFound) {
|
if (!configuredFound) {
|
||||||
throw new Error("Kein Debrid-Provider konfiguriert (API-Key fehlt)");
|
throw new Error("Kein Debrid-Provider konfiguriert");
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Unrestrict fehlgeschlagen: ${attempts.join(" | ")}`);
|
throw new Error(`Unrestrict fehlgeschlagen: ${attempts.join(" | ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getProviderToken(provider: DebridProvider): string {
|
private isProviderConfigured(provider: DebridProvider): boolean {
|
||||||
if (provider === "realdebrid") {
|
if (provider === "realdebrid") {
|
||||||
return this.settings.token;
|
return Boolean(this.settings.token.trim());
|
||||||
}
|
}
|
||||||
if (provider === "megadebrid") {
|
if (provider === "megadebrid") {
|
||||||
return this.settings.megaToken;
|
return Boolean(this.settings.megaLogin.trim() && this.settings.megaPassword.trim());
|
||||||
}
|
}
|
||||||
if (provider === "alldebrid") {
|
if (provider === "alldebrid") {
|
||||||
return this.settings.allDebridToken;
|
return Boolean(this.settings.allDebridToken.trim());
|
||||||
}
|
}
|
||||||
return this.settings.bestToken;
|
return Boolean(this.settings.bestToken.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async unrestrictViaProvider(provider: DebridProvider, link: string, token: string): Promise<UnrestrictedLink> {
|
private async unrestrictViaProvider(provider: DebridProvider, link: string): Promise<UnrestrictedLink> {
|
||||||
if (provider === "realdebrid") {
|
if (provider === "realdebrid") {
|
||||||
return this.realDebridClient.unrestrictLink(link);
|
return this.realDebridClient.unrestrictLink(link);
|
||||||
}
|
}
|
||||||
if (provider === "megadebrid") {
|
if (provider === "megadebrid") {
|
||||||
return new MegaDebridClient(token, this.options.megaWebUnrestrict).unrestrictLink(link);
|
return new MegaDebridClient(this.options.megaWebUnrestrict).unrestrictLink(link);
|
||||||
}
|
}
|
||||||
if (provider === "alldebrid") {
|
if (provider === "alldebrid") {
|
||||||
return this.allDebridClient.unrestrictLink(link);
|
return this.allDebridClient.unrestrictLink(link);
|
||||||
}
|
}
|
||||||
return new BestDebridClient(token).unrestrictLink(link);
|
return new BestDebridClient(this.settings.bestToken).unrestrictLink(link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -417,7 +417,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.stateEmitTimer = setTimeout(() => {
|
this.stateEmitTimer = setTimeout(() => {
|
||||||
this.stateEmitTimer = null;
|
this.stateEmitTimer = null;
|
||||||
this.emit("state", this.getSnapshot());
|
this.emit("state", this.getSnapshot());
|
||||||
}, 140);
|
}, 260);
|
||||||
}
|
}
|
||||||
|
|
||||||
private pruneSpeedEvents(now: number): void {
|
private pruneSpeedEvents(now: number): void {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { DragEvent, ReactElement, useEffect, useMemo, useState } from "react";
|
import { DragEvent, ReactElement, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { AppSettings, DebridProvider, DownloadItem, PackageEntry, UiSnapshot, UpdateCheckResult } from "../shared/types";
|
import type { AppSettings, DebridProvider, DownloadItem, PackageEntry, UiSnapshot, UpdateCheckResult } from "../shared/types";
|
||||||
|
|
||||||
type Tab = "collector" | "downloads" | "settings";
|
type Tab = "collector" | "downloads" | "settings";
|
||||||
@ -6,7 +6,6 @@ type Tab = "collector" | "downloads" | "settings";
|
|||||||
const emptySnapshot = (): UiSnapshot => ({
|
const emptySnapshot = (): UiSnapshot => ({
|
||||||
settings: {
|
settings: {
|
||||||
token: "",
|
token: "",
|
||||||
megaToken: "",
|
|
||||||
megaLogin: "",
|
megaLogin: "",
|
||||||
megaPassword: "",
|
megaPassword: "",
|
||||||
bestToken: "",
|
bestToken: "",
|
||||||
@ -80,6 +79,8 @@ export function App(): ReactElement {
|
|||||||
const [linksRaw, setLinksRaw] = useState("");
|
const [linksRaw, setLinksRaw] = useState("");
|
||||||
const [statusToast, setStatusToast] = useState("");
|
const [statusToast, setStatusToast] = useState("");
|
||||||
const [settingsDraft, setSettingsDraft] = useState<AppSettings>(emptySnapshot().settings);
|
const [settingsDraft, setSettingsDraft] = useState<AppSettings>(emptySnapshot().settings);
|
||||||
|
const latestStateRef = useRef<UiSnapshot | null>(null);
|
||||||
|
const stateFlushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unsubscribe: (() => void) | null = null;
|
let unsubscribe: (() => void) | null = null;
|
||||||
@ -93,9 +94,23 @@ export function App(): ReactElement {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
unsubscribe = window.rd.onStateUpdate((state) => {
|
unsubscribe = window.rd.onStateUpdate((state) => {
|
||||||
setSnapshot(state);
|
latestStateRef.current = state;
|
||||||
|
if (stateFlushTimerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stateFlushTimerRef.current = setTimeout(() => {
|
||||||
|
stateFlushTimerRef.current = null;
|
||||||
|
if (latestStateRef.current) {
|
||||||
|
setSnapshot(latestStateRef.current);
|
||||||
|
latestStateRef.current = null;
|
||||||
|
}
|
||||||
|
}, 220);
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
|
if (stateFlushTimerRef.current) {
|
||||||
|
clearTimeout(stateFlushTimerRef.current);
|
||||||
|
stateFlushTimerRef.current = null;
|
||||||
|
}
|
||||||
if (unsubscribe) {
|
if (unsubscribe) {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
}
|
}
|
||||||
@ -171,6 +186,7 @@ export function App(): ReactElement {
|
|||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await window.rd.updateSettings(settingsDraft);
|
||||||
const result = await window.rd.addContainers(files);
|
const result = await window.rd.addContainers(files);
|
||||||
setStatusToast(`DLC importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
setStatusToast(`DLC importiert: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
||||||
setTimeout(() => setStatusToast(""), 2200);
|
setTimeout(() => setStatusToast(""), 2200);
|
||||||
@ -186,6 +202,7 @@ export function App(): ReactElement {
|
|||||||
if (dlc.length === 0) {
|
if (dlc.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
await window.rd.updateSettings(settingsDraft);
|
||||||
const result = await window.rd.addContainers(dlc);
|
const result = await window.rd.addContainers(dlc);
|
||||||
setStatusToast(`Drag-and-Drop: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
setStatusToast(`Drag-and-Drop: ${result.addedPackages} Paket(e), ${result.addedLinks} Link(s)`);
|
||||||
setTimeout(() => setStatusToast(""), 2200);
|
setTimeout(() => setStatusToast(""), 2200);
|
||||||
@ -309,18 +326,12 @@ export function App(): ReactElement {
|
|||||||
value={settingsDraft.token}
|
value={settingsDraft.token}
|
||||||
onChange={(event) => setText("token", event.target.value)}
|
onChange={(event) => setText("token", event.target.value)}
|
||||||
/>
|
/>
|
||||||
<label>Mega-Debrid API Token</label>
|
<label>Mega-Debrid Login</label>
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
value={settingsDraft.megaToken}
|
|
||||||
onChange={(event) => setText("megaToken", event.target.value)}
|
|
||||||
/>
|
|
||||||
<label>Mega-Debrid Login (Web Fallback)</label>
|
|
||||||
<input
|
<input
|
||||||
value={settingsDraft.megaLogin}
|
value={settingsDraft.megaLogin}
|
||||||
onChange={(event) => setText("megaLogin", event.target.value)}
|
onChange={(event) => setText("megaLogin", event.target.value)}
|
||||||
/>
|
/>
|
||||||
<label>Mega-Debrid Passwort (Web Fallback)</label>
|
<label>Mega-Debrid Passwort</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={settingsDraft.megaPassword}
|
value={settingsDraft.megaPassword}
|
||||||
|
|||||||
@ -18,7 +18,6 @@ export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "allde
|
|||||||
|
|
||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
token: string;
|
token: string;
|
||||||
megaToken: string;
|
|
||||||
megaLogin: string;
|
megaLogin: string;
|
||||||
megaPassword: string;
|
megaPassword: string;
|
||||||
bestToken: string;
|
bestToken: string;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import { defaultSettings } from "../src/main/constants";
|
import { defaultSettings } from "../src/main/constants";
|
||||||
import { DebridService } from "../src/main/debrid";
|
import { DebridService } from "../src/main/debrid";
|
||||||
|
|
||||||
@ -6,14 +6,16 @@ const originalFetch = globalThis.fetch;
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
globalThis.fetch = originalFetch;
|
globalThis.fetch = originalFetch;
|
||||||
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("debrid service", () => {
|
describe("debrid service", () => {
|
||||||
it("falls back to Mega-Debrid when Real-Debrid fails", async () => {
|
it("falls back to Mega web when Real-Debrid fails", async () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
token: "rd-token",
|
token: "rd-token",
|
||||||
megaToken: "mega-token",
|
megaLogin: "user",
|
||||||
|
megaPassword: "pass",
|
||||||
bestToken: "",
|
bestToken: "",
|
||||||
providerPrimary: "realdebrid" as const,
|
providerPrimary: "realdebrid" as const,
|
||||||
providerSecondary: "megadebrid" as const,
|
providerSecondary: "megadebrid" as const,
|
||||||
@ -29,26 +31,29 @@ describe("debrid service", () => {
|
|||||||
headers: { "Content-Type": "application/json" }
|
headers: { "Content-Type": "application/json" }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (url.includes("mega-debrid.eu/api.php?action=getLink")) {
|
|
||||||
return new Response(JSON.stringify({ response_code: "ok", debridLink: "https://mega.example/file.bin" }), {
|
|
||||||
status: 200,
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return new Response("not-found", { status: 404 });
|
return new Response("not-found", { status: 404 });
|
||||||
}) as typeof fetch;
|
}) as typeof fetch;
|
||||||
|
|
||||||
const service = new DebridService(settings);
|
const megaWeb = vi.fn(async () => ({
|
||||||
|
fileName: "file.bin",
|
||||||
|
directUrl: "https://mega-web.example/file.bin",
|
||||||
|
fileSize: null,
|
||||||
|
retriesUsed: 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
const service = new DebridService(settings, { megaWebUnrestrict: megaWeb });
|
||||||
const result = await service.unrestrictLink("https://rapidgator.net/file/example.part1.rar.html");
|
const result = await service.unrestrictLink("https://rapidgator.net/file/example.part1.rar.html");
|
||||||
expect(result.provider).toBe("megadebrid");
|
expect(result.provider).toBe("megadebrid");
|
||||||
expect(result.directUrl).toBe("https://mega.example/file.bin");
|
expect(result.directUrl).toBe("https://mega-web.example/file.bin");
|
||||||
|
expect(megaWeb).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not fallback when auto fallback is disabled", async () => {
|
it("does not fallback when auto fallback is disabled", async () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
token: "rd-token",
|
token: "rd-token",
|
||||||
megaToken: "mega-token",
|
megaLogin: "user",
|
||||||
|
megaPassword: "pass",
|
||||||
providerPrimary: "realdebrid" as const,
|
providerPrimary: "realdebrid" as const,
|
||||||
providerSecondary: "megadebrid" as const,
|
providerSecondary: "megadebrid" as const,
|
||||||
providerTertiary: "bestdebrid" as const,
|
providerTertiary: "bestdebrid" as const,
|
||||||
@ -60,21 +65,25 @@ describe("debrid service", () => {
|
|||||||
if (url.includes("api.real-debrid.com/rest/1.0/unrestrict/link")) {
|
if (url.includes("api.real-debrid.com/rest/1.0/unrestrict/link")) {
|
||||||
return new Response("traffic exhausted", { status: 429 });
|
return new Response("traffic exhausted", { status: 429 });
|
||||||
}
|
}
|
||||||
return new Response(JSON.stringify({ response_code: "ok", debridLink: "https://mega.example/file.bin" }), {
|
return new Response("not-found", { status: 404 });
|
||||||
status: 200,
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
});
|
|
||||||
}) as typeof fetch;
|
}) as typeof fetch;
|
||||||
|
|
||||||
const service = new DebridService(settings);
|
const megaWeb = vi.fn(async () => ({
|
||||||
|
fileName: "unused.bin",
|
||||||
|
directUrl: "https://unused",
|
||||||
|
fileSize: null,
|
||||||
|
retriesUsed: 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
const service = new DebridService(settings, { megaWebUnrestrict: megaWeb });
|
||||||
await expect(service.unrestrictLink("https://rapidgator.net/file/example.part2.rar.html")).rejects.toThrow();
|
await expect(service.unrestrictLink("https://rapidgator.net/file/example.part2.rar.html")).rejects.toThrow();
|
||||||
|
expect(megaWeb).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("supports BestDebrid auth query fallback", async () => {
|
it("supports BestDebrid auth query fallback", async () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
token: "",
|
token: "",
|
||||||
megaToken: "",
|
|
||||||
bestToken: "best-token",
|
bestToken: "best-token",
|
||||||
providerPrimary: "bestdebrid" as const,
|
providerPrimary: "bestdebrid" as const,
|
||||||
providerSecondary: "realdebrid" as const,
|
providerSecondary: "realdebrid" as const,
|
||||||
@ -109,7 +118,6 @@ describe("debrid service", () => {
|
|||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
token: "",
|
token: "",
|
||||||
megaToken: "",
|
|
||||||
bestToken: "",
|
bestToken: "",
|
||||||
allDebridToken: "ad-token",
|
allDebridToken: "ad-token",
|
||||||
providerPrimary: "alldebrid" as const,
|
providerPrimary: "alldebrid" as const,
|
||||||
@ -143,52 +151,46 @@ describe("debrid service", () => {
|
|||||||
expect(result.fileSize).toBe(4096);
|
expect(result.fileSize).toBe(4096);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("retries Mega-Debrid with alternate request variants", async () => {
|
it("uses Mega web path exclusively", async () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
token: "",
|
token: "",
|
||||||
megaToken: "mega-token",
|
|
||||||
bestToken: "",
|
bestToken: "",
|
||||||
allDebridToken: "",
|
allDebridToken: "",
|
||||||
|
megaLogin: "user",
|
||||||
|
megaPassword: "pass",
|
||||||
providerPrimary: "megadebrid" as const,
|
providerPrimary: "megadebrid" as const,
|
||||||
providerSecondary: "megadebrid" as const,
|
providerSecondary: "megadebrid" as const,
|
||||||
providerTertiary: "megadebrid" as const,
|
providerTertiary: "megadebrid" as const,
|
||||||
autoProviderFallback: true
|
autoProviderFallback: true
|
||||||
};
|
};
|
||||||
|
|
||||||
let calls = 0;
|
const fetchSpy = vi.fn(async () => new Response("not-found", { status: 404 }));
|
||||||
globalThis.fetch = (async (input: RequestInfo | URL): Promise<Response> => {
|
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
||||||
calls += 1;
|
|
||||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
||||||
if (url.includes("mega-debrid.eu/api.php?action=getLink") && !url.includes("&link=")) {
|
|
||||||
return new Response(JSON.stringify({ response_code: "UNRESTRICTING_ERROR_1", response_text: "UNRESTRICTING_ERROR_1" }), {
|
|
||||||
status: 200,
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (url.includes("mega-debrid.eu/api.php?action=getLink") && url.includes("&link=")) {
|
|
||||||
return new Response(JSON.stringify({ response_code: "ok", debridLink: "https://mega.example/file2.bin", filename: "file2.bin" }), {
|
|
||||||
status: 200,
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return new Response("not-found", { status: 404 });
|
|
||||||
}) as typeof fetch;
|
|
||||||
|
|
||||||
const service = new DebridService(settings);
|
const megaWeb = vi.fn(async () => ({
|
||||||
const result = await service.unrestrictLink("https://rapidgator.net/file/abc/name.part1.rar.html");
|
fileName: "from-web.rar",
|
||||||
|
directUrl: "https://www11.unrestrict.link/download/file/abc/from-web.rar",
|
||||||
|
fileSize: null,
|
||||||
|
retriesUsed: 0
|
||||||
|
}));
|
||||||
|
|
||||||
|
const service = new DebridService(settings, { megaWebUnrestrict: megaWeb });
|
||||||
|
const result = await service.unrestrictLink("https://rapidgator.net/file/abc/from-web.rar.html");
|
||||||
expect(result.provider).toBe("megadebrid");
|
expect(result.provider).toBe("megadebrid");
|
||||||
expect(result.fileName).toBe("file2.bin");
|
expect(result.directUrl).toContain("unrestrict.link/download/file/");
|
||||||
expect(calls).toBeGreaterThan(1);
|
expect(megaWeb).toHaveBeenCalledTimes(1);
|
||||||
|
expect(fetchSpy).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("respects provider selection and does not append hidden fallback providers", async () => {
|
it("respects provider selection and does not append hidden providers", async () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
token: "",
|
token: "",
|
||||||
megaToken: "mega-token",
|
|
||||||
bestToken: "",
|
bestToken: "",
|
||||||
allDebridToken: "ad-token",
|
allDebridToken: "ad-token",
|
||||||
|
megaLogin: "user",
|
||||||
|
megaPassword: "pass",
|
||||||
providerPrimary: "megadebrid" as const,
|
providerPrimary: "megadebrid" as const,
|
||||||
providerSecondary: "megadebrid" as const,
|
providerSecondary: "megadebrid" as const,
|
||||||
providerTertiary: "megadebrid" as const,
|
providerTertiary: "megadebrid" as const,
|
||||||
@ -198,12 +200,6 @@ describe("debrid service", () => {
|
|||||||
let allDebridCalls = 0;
|
let allDebridCalls = 0;
|
||||||
globalThis.fetch = (async (input: RequestInfo | URL): Promise<Response> => {
|
globalThis.fetch = (async (input: RequestInfo | URL): Promise<Response> => {
|
||||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||||
if (url.includes("mega-debrid.eu/api.php?action=getLink")) {
|
|
||||||
return new Response(JSON.stringify({ response_code: "error", response_text: "host unavailable" }), {
|
|
||||||
status: 200,
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (url.includes("api.alldebrid.com/v4/link/unlock")) {
|
if (url.includes("api.alldebrid.com/v4/link/unlock")) {
|
||||||
allDebridCalls += 1;
|
allDebridCalls += 1;
|
||||||
return new Response(JSON.stringify({ status: "success", data: { link: "https://alldebrid.example/file.bin" } }), {
|
return new Response(JSON.stringify({ status: "success", data: { link: "https://alldebrid.example/file.bin" } }), {
|
||||||
@ -214,7 +210,8 @@ describe("debrid service", () => {
|
|||||||
return new Response("not-found", { status: 404 });
|
return new Response("not-found", { status: 404 });
|
||||||
}) as typeof fetch;
|
}) as typeof fetch;
|
||||||
|
|
||||||
const service = new DebridService(settings);
|
const megaWeb = vi.fn(async () => null);
|
||||||
|
const service = new DebridService(settings, { megaWebUnrestrict: megaWeb });
|
||||||
await expect(service.unrestrictLink("https://rapidgator.net/file/example.part5.rar.html")).rejects.toThrow();
|
await expect(service.unrestrictLink("https://rapidgator.net/file/example.part5.rar.html")).rejects.toThrow();
|
||||||
expect(allDebridCalls).toBe(0);
|
expect(allDebridCalls).toBe(0);
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user