Release v1.6.90

This commit is contained in:
Sucukdeluxe 2026-03-06 20:43:15 +01:00
parent 0eb3403e40
commit 0003d786d8
10 changed files with 354 additions and 79 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.6.89", "version": "1.6.90",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.6.89", "version": "1.6.90",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.6.89", "version": "1.6.90",
"description": "Desktop downloader", "description": "Desktop downloader",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -44,6 +44,8 @@ export function defaultSettings(): AppSettings {
realDebridUseWebLogin: false, realDebridUseWebLogin: false,
megaLogin: "", megaLogin: "",
megaPassword: "", megaPassword: "",
megaDebridApiEnabled: false,
megaDebridWebEnabled: false,
megaDebridPreferApi: true, megaDebridPreferApi: true,
bestToken: "", bestToken: "",
bestDebridUseWebLogin: false, bestDebridUseWebLogin: false,
@ -58,7 +60,7 @@ export function defaultSettings(): AppSettings {
archivePasswordList: "", archivePasswordList: "",
rememberToken: true, rememberToken: true,
providerPrimary: "realdebrid", providerPrimary: "realdebrid",
providerSecondary: "megadebrid", providerSecondary: "megadebrid-api",
providerTertiary: "bestdebrid", providerTertiary: "bestdebrid",
autoProviderFallback: true, autoProviderFallback: true,
outputDir: baseDir, outputDir: baseDir,

View File

@ -25,6 +25,8 @@ const LINKSNAPPY_API_BASE = "https://linksnappy.com/api";
const PROVIDER_LABELS: Record<DebridProvider, string> = { const PROVIDER_LABELS: Record<DebridProvider, string> = {
realdebrid: "Real-Debrid", realdebrid: "Real-Debrid",
megadebrid: "Mega-Debrid", megadebrid: "Mega-Debrid",
"megadebrid-api": "Mega-Debrid API",
"megadebrid-web": "Mega-Debrid Web",
bestdebrid: "BestDebrid", bestdebrid: "BestDebrid",
alldebrid: "AllDebrid", alldebrid: "AllDebrid",
ddownload: "DDownload", ddownload: "DDownload",
@ -67,6 +69,32 @@ function cloneSettings(settings: AppSettings): AppSettings {
}; };
} }
function hasMegaDebridCredentials(settings: AppSettings): boolean {
return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim());
}
function isMegaDebridModeEnabled(settings: AppSettings, mode: "api" | "web"): boolean {
if (mode === "api") {
return settings.megaDebridApiEnabled
|| (hasMegaDebridCredentials(settings) && !settings.megaDebridApiEnabled && !settings.megaDebridWebEnabled && settings.megaDebridPreferApi);
}
return settings.megaDebridWebEnabled
|| (hasMegaDebridCredentials(settings) && !settings.megaDebridApiEnabled && !settings.megaDebridWebEnabled && !settings.megaDebridPreferApi);
}
function resolveMegaDebridProvider(settings: AppSettings, provider: DebridProvider): DebridProvider {
if (provider !== "megadebrid") {
return provider;
}
if (isMegaDebridModeEnabled(settings, "api") && !isMegaDebridModeEnabled(settings, "web")) {
return "megadebrid-api";
}
if (isMegaDebridModeEnabled(settings, "web") && !isMegaDebridModeEnabled(settings, "api")) {
return "megadebrid-web";
}
return settings.megaDebridPreferApi ? "megadebrid-api" : "megadebrid-web";
}
type BestDebridRequest = { type BestDebridRequest = {
url: string; url: string;
useAuthHeader: boolean; useAuthHeader: boolean;
@ -692,7 +720,9 @@ class MegaDebridClient {
private password: string; private password: string;
private preferApi: boolean; private mode: "api" | "web";
private allowApiFallback: boolean;
private static cachedApiToken = ""; private static cachedApiToken = "";
@ -700,10 +730,11 @@ class MegaDebridClient {
private static pendingConnect: Promise<string | null> | null = null; private static pendingConnect: Promise<string | null> | null = null;
public constructor(login: string, password: string, preferApi: boolean, megaWebUnrestrict?: MegaWebUnrestrictor) { public constructor(login: string, password: string, mode: "api" | "web", allowApiFallback: boolean, megaWebUnrestrict?: MegaWebUnrestrictor) {
this.login = login; this.login = login;
this.password = password; this.password = password;
this.preferApi = preferApi; this.mode = mode;
this.allowApiFallback = allowApiFallback;
this.megaWebUnrestrict = megaWebUnrestrict; this.megaWebUnrestrict = megaWebUnrestrict;
} }
@ -839,25 +870,27 @@ class MegaDebridClient {
} }
public async unrestrictLink(link: string, signal?: AbortSignal): Promise<UnrestrictedLink> { public async unrestrictLink(link: string, signal?: AbortSignal): Promise<UnrestrictedLink> {
if (this.preferApi && this.login.trim() && this.password.trim()) { if (this.mode === "api" && this.login.trim() && this.password.trim()) {
// API mode: try API first, fall back to web on failure
try { try {
const apiResult = await this.unrestrictViaApi(link, signal); const apiResult = await this.unrestrictViaApi(link, signal);
if (apiResult) { if (apiResult) {
logger.info(`Mega-Debrid (API) unrestrict OK: ${apiResult.fileName}`); logger.info(`Mega-Debrid (API) unrestrict OK: ${apiResult.fileName}`);
return apiResult; return apiResult;
} }
throw new Error("Mega-Debrid API: Login oder Unrestrict fehlgeschlagen");
} catch (error) { } catch (error) {
const errorText = compactErrorText(error); const errorText = compactErrorText(error);
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) { if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
throw error; throw error;
} }
if (!this.allowApiFallback) {
throw error;
}
logger.warn(`Mega-Debrid API fehlgeschlagen, versuche Web-Fallback: ${errorText}`); logger.warn(`Mega-Debrid API fehlgeschlagen, versuche Web-Fallback: ${errorText}`);
} }
return this.unrestrictViaWeb(link, signal); return this.unrestrictViaWeb(link, signal);
} }
// Web mode only
return this.unrestrictViaWeb(link, signal); return this.unrestrictViaWeb(link, signal);
} }
} }
@ -2036,33 +2069,38 @@ export class DebridService {
} }
private isProviderConfiguredFor(settings: AppSettings, provider: DebridProvider): boolean { private isProviderConfiguredFor(settings: AppSettings, provider: DebridProvider): boolean {
if ((settings.disabledProviders || []).includes(provider)) return false; const effectiveProvider = resolveMegaDebridProvider(settings, provider);
if (provider === "realdebrid") { if ((settings.disabledProviders || []).includes(provider) || (settings.disabledProviders || []).includes(effectiveProvider)) return false;
if (effectiveProvider === "realdebrid") {
return Boolean(this.shouldUseRealDebridWeb(settings) || settings.token.trim()); return Boolean(this.shouldUseRealDebridWeb(settings) || settings.token.trim());
} }
if (provider === "megadebrid") { if (effectiveProvider === "megadebrid-api") {
return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim()); return Boolean(hasMegaDebridCredentials(settings) && isMegaDebridModeEnabled(settings, "api"));
} }
if (provider === "alldebrid") { if (effectiveProvider === "megadebrid-web") {
return Boolean(hasMegaDebridCredentials(settings) && isMegaDebridModeEnabled(settings, "web") && this.options.megaWebUnrestrict);
}
if (effectiveProvider === "alldebrid") {
return Boolean(this.shouldUseAllDebridWeb(settings) || settings.allDebridToken.trim()); return Boolean(this.shouldUseAllDebridWeb(settings) || settings.allDebridToken.trim());
} }
if (provider === "ddownload") { if (effectiveProvider === "ddownload") {
return Boolean(settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim()); return Boolean(settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim());
} }
if (provider === "onefichier") { if (effectiveProvider === "onefichier") {
return Boolean(settings.oneFichierApiKey.trim()); return Boolean(settings.oneFichierApiKey.trim());
} }
if (provider === "debridlink") { if (effectiveProvider === "debridlink") {
return Boolean(settings.debridLinkApiKeys.trim()); return Boolean(settings.debridLinkApiKeys.trim());
} }
if (provider === "linksnappy") { if (effectiveProvider === "linksnappy") {
return Boolean(settings.linkSnappyLogin.trim() && settings.linkSnappyPassword.trim()); return Boolean(settings.linkSnappyLogin.trim() && settings.linkSnappyPassword.trim());
} }
return Boolean(this.shouldUseBestDebridWeb(settings) || settings.bestToken.trim()); return Boolean(this.shouldUseBestDebridWeb(settings) || settings.bestToken.trim());
} }
private async unrestrictViaProvider(settings: AppSettings, provider: DebridProvider, link: string, signal?: AbortSignal): Promise<UnrestrictedLink> { private async unrestrictViaProvider(settings: AppSettings, provider: DebridProvider, link: string, signal?: AbortSignal): Promise<UnrestrictedLink> {
if (provider === "realdebrid") { const effectiveProvider = resolveMegaDebridProvider(settings, provider);
if (effectiveProvider === "realdebrid") {
if (this.shouldUseRealDebridWeb(settings) && this.options.realDebridWebUnrestrict) { if (this.shouldUseRealDebridWeb(settings) && this.options.realDebridWebUnrestrict) {
const result = await this.options.realDebridWebUnrestrict(link, signal); const result = await this.options.realDebridWebUnrestrict(link, signal);
if (!result) { if (!result) {
@ -2075,10 +2113,13 @@ export class DebridService {
result.sourceLabel = "API"; result.sourceLabel = "API";
return result; return result;
} }
if (provider === "megadebrid") { if (effectiveProvider === "megadebrid-api") {
return new MegaDebridClient(settings.megaLogin, settings.megaPassword, settings.megaDebridPreferApi, this.options.megaWebUnrestrict).unrestrictLink(link, signal); return new MegaDebridClient(settings.megaLogin, settings.megaPassword, "api", provider === "megadebrid" && settings.megaDebridPreferApi, this.options.megaWebUnrestrict).unrestrictLink(link, signal);
} }
if (provider === "alldebrid") { if (effectiveProvider === "megadebrid-web") {
return new MegaDebridClient(settings.megaLogin, settings.megaPassword, "web", false, this.options.megaWebUnrestrict).unrestrictLink(link, signal);
}
if (effectiveProvider === "alldebrid") {
if (this.shouldUseAllDebridWeb(settings) && this.options.allDebridWebUnrestrict) { if (this.shouldUseAllDebridWeb(settings) && this.options.allDebridWebUnrestrict) {
const result = await this.options.allDebridWebUnrestrict(link, signal); const result = await this.options.allDebridWebUnrestrict(link, signal);
if (!result) { if (!result) {
@ -2091,18 +2132,18 @@ export class DebridService {
adResult.sourceLabel = "API"; adResult.sourceLabel = "API";
return adResult; return adResult;
} }
if (provider === "ddownload") { if (effectiveProvider === "ddownload") {
return this.getDdownloadClient(settings.ddownloadLogin, settings.ddownloadPassword).unrestrictLink(link, signal); return this.getDdownloadClient(settings.ddownloadLogin, settings.ddownloadPassword).unrestrictLink(link, signal);
} }
if (provider === "onefichier") { if (effectiveProvider === "onefichier") {
return new OneFichierClient(settings.oneFichierApiKey).unrestrictLink(link, signal); return new OneFichierClient(settings.oneFichierApiKey).unrestrictLink(link, signal);
} }
if (provider === "debridlink") { if (effectiveProvider === "debridlink") {
const dlResult = await this.getDebridLinkClient(settings.debridLinkApiKeys).unrestrictLink(link, signal); const dlResult = await this.getDebridLinkClient(settings.debridLinkApiKeys).unrestrictLink(link, signal);
dlResult.sourceLabel = dlResult.sourceLabel || "API"; dlResult.sourceLabel = dlResult.sourceLabel || "API";
return dlResult; return dlResult;
} }
if (provider === "linksnappy") { if (effectiveProvider === "linksnappy") {
return this.getLinkSnappyClient(settings.linkSnappyLogin, settings.linkSnappyPassword).unrestrictLink(link, signal); return this.getLinkSnappyClient(settings.linkSnappyLogin, settings.linkSnappyPassword).unrestrictLink(link, signal);
} }
if (this.shouldUseBestDebridWeb(settings) && this.options.bestDebridWebUnrestrict) { if (this.shouldUseBestDebridWeb(settings) && this.options.bestDebridWebUnrestrict) {

View File

@ -371,6 +371,12 @@ function providerLabel(provider: DownloadItem["provider"]): string {
if (provider === "megadebrid") { if (provider === "megadebrid") {
return "Mega-Debrid"; return "Mega-Debrid";
} }
if (provider === "megadebrid-api") {
return "Mega-Debrid API";
}
if (provider === "megadebrid-web") {
return "Mega-Debrid Web";
}
if (provider === "bestdebrid") { if (provider === "bestdebrid") {
return "BestDebrid"; return "BestDebrid";
} }
@ -383,6 +389,23 @@ function providerLabel(provider: DownloadItem["provider"]): string {
return "Debrid"; return "Debrid";
} }
function resolveMegaDebridProvider(settings: AppSettings, provider: DebridProvider | null): DebridProvider | null {
if (provider !== "megadebrid") {
return provider;
}
const apiEnabled = settings.megaDebridApiEnabled
|| (settings.megaLogin.trim() && settings.megaPassword.trim() && !settings.megaDebridApiEnabled && !settings.megaDebridWebEnabled && settings.megaDebridPreferApi);
const webEnabled = settings.megaDebridWebEnabled
|| (settings.megaLogin.trim() && settings.megaPassword.trim() && !settings.megaDebridApiEnabled && !settings.megaDebridWebEnabled && !settings.megaDebridPreferApi);
if (apiEnabled && !webEnabled) {
return "megadebrid-api";
}
if (webEnabled && !apiEnabled) {
return "megadebrid-web";
}
return settings.megaDebridPreferApi ? "megadebrid-api" : "megadebrid-web";
}
function pathKey(filePath: string): string { function pathKey(filePath: string): string {
const resolved = path.resolve(filePath); const resolved = path.resolve(filePath);
return process.platform === "win32" ? resolved.toLowerCase() : resolved; return process.platform === "win32" ? resolved.toLowerCase() : resolved;
@ -3462,7 +3485,16 @@ export class DownloadManager extends EventEmitter {
this.session.reconnectReason = ""; this.session.reconnectReason = "";
for (const item of Object.values(this.session.items)) { for (const item of Object.values(this.session.items)) {
if (item.provider !== "realdebrid" && item.provider !== "megadebrid" && item.provider !== "bestdebrid" && item.provider !== "alldebrid" && item.provider !== "ddownload") { if (item.provider === "megadebrid") {
item.provider = resolveMegaDebridProvider(this.settings, item.provider);
}
if (item.provider !== "realdebrid"
&& item.provider !== "megadebrid"
&& item.provider !== "megadebrid-api"
&& item.provider !== "megadebrid-web"
&& item.provider !== "bestdebrid"
&& item.provider !== "alldebrid"
&& item.provider !== "ddownload") {
item.provider = null; item.provider = null;
} }
if (item.status === "cancelled" && item.fullStatus === "Gestoppt") { if (item.status === "cancelled" && item.fullStatus === "Gestoppt") {
@ -4302,7 +4334,7 @@ export class DownloadManager extends EventEmitter {
entry.cooldownUntil = now + cooldownMs; entry.cooldownUntil = now + cooldownMs;
logger.warn(`Provider Circuit-Breaker: ${key} ${entry.count} konsekutive Fehler, Cooldown ${cooldownMs / 1000}s`); logger.warn(`Provider Circuit-Breaker: ${key} ${entry.count} konsekutive Fehler, Cooldown ${cooldownMs / 1000}s`);
// Invalidate mega-debrid session on cooldown to force fresh login // Invalidate mega-debrid session on cooldown to force fresh login
if (key === "megadebrid" && this.invalidateMegaSessionFn) { if ((key === "megadebrid" || key === "megadebrid-api" || key === "megadebrid-web") && this.invalidateMegaSessionFn) {
try { try {
this.invalidateMegaSessionFn(); this.invalidateMegaSessionFn();
} catch { /* ignore */ } } catch { /* ignore */ }
@ -4344,28 +4376,34 @@ export class DownloadManager extends EventEmitter {
} }
private isProviderConfigured(provider: DebridProvider): boolean { private isProviderConfigured(provider: DebridProvider): boolean {
if ((this.settings.disabledProviders || []).includes(provider)) { const effectiveProvider = resolveMegaDebridProvider(this.settings, provider) || provider;
if ((this.settings.disabledProviders || []).includes(provider) || (this.settings.disabledProviders || []).includes(effectiveProvider)) {
return false; return false;
} }
if (provider === "realdebrid") { if (effectiveProvider === "realdebrid") {
return Boolean(this.settings.realDebridUseWebLogin || this.settings.token.trim()); return Boolean(this.settings.realDebridUseWebLogin || this.settings.token.trim());
} }
if (provider === "megadebrid") { if (effectiveProvider === "megadebrid-api") {
return Boolean(this.settings.megaLogin.trim() && this.settings.megaPassword.trim()); return Boolean(resolveMegaDebridProvider(this.settings, "megadebrid") === "megadebrid-api" && this.settings.megaLogin.trim() && this.settings.megaPassword.trim()
|| this.settings.megaDebridApiEnabled && this.settings.megaLogin.trim() && this.settings.megaPassword.trim());
} }
if (provider === "bestdebrid") { if (effectiveProvider === "megadebrid-web") {
return Boolean(resolveMegaDebridProvider(this.settings, "megadebrid") === "megadebrid-web" && this.settings.megaLogin.trim() && this.settings.megaPassword.trim()
|| this.settings.megaDebridWebEnabled && this.settings.megaLogin.trim() && this.settings.megaPassword.trim());
}
if (effectiveProvider === "bestdebrid") {
return Boolean(this.settings.bestDebridUseWebLogin || this.settings.bestToken.trim()); return Boolean(this.settings.bestDebridUseWebLogin || this.settings.bestToken.trim());
} }
if (provider === "alldebrid") { if (effectiveProvider === "alldebrid") {
return Boolean(this.settings.allDebridUseWebLogin || this.settings.allDebridToken.trim()); return Boolean(this.settings.allDebridUseWebLogin || this.settings.allDebridToken.trim());
} }
if (provider === "ddownload") { if (effectiveProvider === "ddownload") {
return Boolean(this.settings.ddownloadLogin.trim() && this.settings.ddownloadPassword.trim()); return Boolean(this.settings.ddownloadLogin.trim() && this.settings.ddownloadPassword.trim());
} }
if (provider === "onefichier") { if (effectiveProvider === "onefichier") {
return Boolean(this.settings.oneFichierApiKey.trim()); return Boolean(this.settings.oneFichierApiKey.trim());
} }
if (provider === "debridlink") { if (effectiveProvider === "debridlink") {
return Boolean(this.settings.debridLinkApiKeys.trim()); return Boolean(this.settings.debridLinkApiKeys.trim());
} }
if (provider === "linksnappy") { if (provider === "linksnappy") {
@ -4376,7 +4414,7 @@ export class DownloadManager extends EventEmitter {
private getExpectedProviderForItem(item: DownloadItem): DebridProvider | null { private getExpectedProviderForItem(item: DownloadItem): DebridProvider | null {
if (item.provider) { if (item.provider) {
return item.provider; return resolveMegaDebridProvider(this.settings, item.provider);
} }
const hosterKey = extractHosterKey(item.url); const hosterKey = extractHosterKey(item.url);

View File

@ -1,12 +1,12 @@
import fs from "node:fs"; import fs from "node:fs";
import fsp from "node:fs/promises"; import fsp from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { AppSettings, BandwidthScheduleEntry, DebridProvider, DownloadItem, DownloadStatus, HistoryEntry, PackageEntry, PackagePriority, SessionState } from "../shared/types"; import { AppSettings, BandwidthScheduleEntry, DebridFallbackProvider, DebridProvider, DownloadItem, DownloadStatus, HistoryEntry, PackageEntry, PackagePriority, SessionState } from "../shared/types";
import { defaultSettings } from "./constants"; import { defaultSettings } from "./constants";
import { logger } from "./logger"; import { logger } from "./logger";
const VALID_PRIMARY_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink", "linksnappy"]); const VALID_PRIMARY_PROVIDERS = new Set(["realdebrid", "megadebrid-api", "megadebrid-web", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink", "linksnappy"]);
const VALID_FALLBACK_PROVIDERS = new Set(["none", "realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink", "linksnappy"]); const VALID_FALLBACK_PROVIDERS = new Set(["none", "realdebrid", "megadebrid-api", "megadebrid-web", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink", "linksnappy"]);
const VALID_CLEANUP_MODES = new Set(["none", "trash", "delete"]); const VALID_CLEANUP_MODES = new Set(["none", "trash", "delete"]);
const VALID_CONFLICT_MODES = new Set(["overwrite", "skip", "rename", "ask"]); const VALID_CONFLICT_MODES = new Set(["overwrite", "skip", "rename", "ask"]);
const VALID_FINISHED_POLICIES = new Set(["never", "immediate", "on_start", "package_done"]); const VALID_FINISHED_POLICIES = new Set(["never", "immediate", "on_start", "package_done"]);
@ -17,7 +17,7 @@ const VALID_PACKAGE_PRIORITIES = new Set<string>(["high", "normal", "low"]);
const VALID_DOWNLOAD_STATUSES = new Set<DownloadStatus>([ const VALID_DOWNLOAD_STATUSES = new Set<DownloadStatus>([
"queued", "validating", "downloading", "paused", "reconnect_wait", "extracting", "integrity_check", "completed", "failed", "cancelled" "queued", "validating", "downloading", "paused", "reconnect_wait", "extracting", "integrity_check", "completed", "failed", "cancelled"
]); ]);
const VALID_ITEM_PROVIDERS = new Set<DebridProvider>(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink"]); const VALID_ITEM_PROVIDERS = new Set<DebridProvider>(["realdebrid", "megadebrid", "megadebrid-api", "megadebrid-web", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink"]);
const VALID_ONLINE_STATUSES = new Set(["online", "offline", "checking"]); const VALID_ONLINE_STATUSES = new Set(["online", "offline", "checking"]);
function asText(value: unknown): string { function asText(value: unknown): string {
@ -91,14 +91,66 @@ function normalizeColumnOrder(raw: unknown): string[] {
return result; return result;
} }
function normalizeHosterRouting(raw: unknown): Record<string, DebridProvider> { function getPreferredMegaDebridProvider(megaDebridPreferApi: boolean, megaDebridApiEnabled: boolean, megaDebridWebEnabled: boolean): DebridProvider {
if (megaDebridApiEnabled && !megaDebridWebEnabled) {
return "megadebrid-api";
}
if (megaDebridWebEnabled && !megaDebridApiEnabled) {
return "megadebrid-web";
}
return megaDebridPreferApi ? "megadebrid-api" : "megadebrid-web";
}
function normalizeConfiguredProvider(raw: unknown, megaDebridPreferApi: boolean, megaDebridApiEnabled: boolean, megaDebridWebEnabled: boolean): DebridProvider | null {
const provider = String(raw ?? "").trim();
if (!provider) {
return null;
}
if (provider === "megadebrid") {
return getPreferredMegaDebridProvider(megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled);
}
return VALID_PRIMARY_PROVIDERS.has(provider) ? provider as DebridProvider : null;
}
function normalizeFallbackProvider(raw: unknown, megaDebridPreferApi: boolean, megaDebridApiEnabled: boolean, megaDebridWebEnabled: boolean): DebridFallbackProvider {
const provider = String(raw ?? "").trim();
if (!provider || provider === "none") {
return "none";
}
const normalized = normalizeConfiguredProvider(provider, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled);
return normalized || "none";
}
function normalizeDisabledProviders(raw: unknown): DebridProvider[] {
if (!Array.isArray(raw)) {
return [];
}
const seen = new Set<DebridProvider>();
const result: DebridProvider[] = [];
for (const entry of raw) {
const provider = String(entry ?? "").trim();
const candidates: DebridProvider[] = provider === "megadebrid"
? ["megadebrid-api", "megadebrid-web"]
: (VALID_PRIMARY_PROVIDERS.has(provider) ? [provider as DebridProvider] : []);
for (const candidate of candidates) {
if (seen.has(candidate)) {
continue;
}
seen.add(candidate);
result.push(candidate);
}
}
return result;
}
function normalizeHosterRouting(raw: unknown, megaDebridPreferApi: boolean, megaDebridApiEnabled: boolean, megaDebridWebEnabled: boolean): Record<string, DebridProvider> {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}; if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
const result: Record<string, DebridProvider> = {}; const result: Record<string, DebridProvider> = {};
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) { for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
const hoster = String(key).trim().toLowerCase(); const hoster = String(key).trim().toLowerCase();
const provider = String(value ?? "").trim(); const provider = normalizeConfiguredProvider(value, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled);
if (hoster && VALID_PRIMARY_PROVIDERS.has(provider)) { if (hoster && provider) {
result[hoster] = provider as DebridProvider; result[hoster] = provider;
} }
} }
return result; return result;
@ -118,12 +170,24 @@ function migrateUpdateRepo(raw: string, fallback: string): string {
export function normalizeSettings(settings: AppSettings): AppSettings { export function normalizeSettings(settings: AppSettings): AppSettings {
const defaults = defaultSettings(); const defaults = defaultSettings();
const megaLogin = asText(settings.megaLogin);
const megaPassword = asText(settings.megaPassword);
const megaDebridPreferApi = settings.megaDebridPreferApi !== undefined ? Boolean(settings.megaDebridPreferApi) : true;
const hasMegaCreds = Boolean(megaLogin && megaPassword);
const megaDebridApiEnabled = settings.megaDebridApiEnabled !== undefined
? Boolean(settings.megaDebridApiEnabled)
: (hasMegaCreds ? megaDebridPreferApi : defaults.megaDebridApiEnabled);
const megaDebridWebEnabled = settings.megaDebridWebEnabled !== undefined
? Boolean(settings.megaDebridWebEnabled)
: (hasMegaCreds ? !megaDebridPreferApi : defaults.megaDebridWebEnabled);
const normalized: AppSettings = { const normalized: AppSettings = {
token: asText(settings.token), token: asText(settings.token),
realDebridUseWebLogin: Boolean(settings.realDebridUseWebLogin), realDebridUseWebLogin: Boolean(settings.realDebridUseWebLogin),
megaLogin: asText(settings.megaLogin), megaLogin,
megaPassword: asText(settings.megaPassword), megaPassword,
megaDebridPreferApi: settings.megaDebridPreferApi !== undefined ? Boolean(settings.megaDebridPreferApi) : true, megaDebridApiEnabled,
megaDebridWebEnabled,
megaDebridPreferApi,
bestToken: asText(settings.bestToken), bestToken: asText(settings.bestToken),
bestDebridUseWebLogin: Boolean(settings.bestDebridUseWebLogin), bestDebridUseWebLogin: Boolean(settings.bestDebridUseWebLogin),
allDebridToken: asText(settings.allDebridToken), allDebridToken: asText(settings.allDebridToken),
@ -136,9 +200,9 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
linkSnappyPassword: asText(settings.linkSnappyPassword), linkSnappyPassword: asText(settings.linkSnappyPassword),
archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n|\r/g, "\n"), archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n|\r/g, "\n"),
rememberToken: Boolean(settings.rememberToken), rememberToken: Boolean(settings.rememberToken),
providerPrimary: settings.providerPrimary, providerPrimary: normalizeConfiguredProvider(settings.providerPrimary, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled) || defaults.providerPrimary,
providerSecondary: settings.providerSecondary, providerSecondary: normalizeFallbackProvider(settings.providerSecondary, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled),
providerTertiary: settings.providerTertiary, providerTertiary: normalizeFallbackProvider(settings.providerTertiary, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled),
autoProviderFallback: Boolean(settings.autoProviderFallback), autoProviderFallback: Boolean(settings.autoProviderFallback),
outputDir: normalizeAbsoluteDir(settings.outputDir, defaults.outputDir), outputDir: normalizeAbsoluteDir(settings.outputDir, defaults.outputDir),
packageName: asText(settings.packageName), packageName: asText(settings.packageName),
@ -177,8 +241,8 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
columnOrder: normalizeColumnOrder(settings.columnOrder), columnOrder: normalizeColumnOrder(settings.columnOrder),
extractCpuPriority: settings.extractCpuPriority, extractCpuPriority: settings.extractCpuPriority,
autoExtractWhenStopped: settings.autoExtractWhenStopped !== undefined ? Boolean(settings.autoExtractWhenStopped) : defaults.autoExtractWhenStopped, autoExtractWhenStopped: settings.autoExtractWhenStopped !== undefined ? Boolean(settings.autoExtractWhenStopped) : defaults.autoExtractWhenStopped,
disabledProviders: Array.isArray(settings.disabledProviders) ? settings.disabledProviders.filter((p: unknown) => VALID_PRIMARY_PROVIDERS.has(p as string)) as DebridProvider[] : [], disabledProviders: normalizeDisabledProviders(settings.disabledProviders),
hosterRouting: normalizeHosterRouting(settings.hosterRouting) hosterRouting: normalizeHosterRouting(settings.hosterRouting, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled)
}; };
if (!VALID_PRIMARY_PROVIDERS.has(normalized.providerPrimary)) { if (!VALID_PRIMARY_PROVIDERS.has(normalized.providerPrimary)) {

View File

@ -53,7 +53,7 @@ interface LinkPopupState {
isPackage: boolean; isPackage: boolean;
} }
type AccountService = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink" | "linksnappy"; type AccountService = "realdebrid" | "megadebrid-api" | "megadebrid-web" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink" | "linksnappy";
type AccountKind = type AccountKind =
| "realdebrid-api" | "realdebrid-api"
| "realdebrid-web" | "realdebrid-web"
@ -121,20 +121,20 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
}, },
{ {
kind: "megadebrid-api", kind: "megadebrid-api",
service: "megadebrid", service: "megadebrid-api",
serviceLabel: "Mega-Debrid", serviceLabel: "Mega-Debrid",
title: "Mega-Debrid API", title: "Mega-Debrid API",
modeLabel: "API", modeLabel: "API",
pickerDescription: "Login mit API-Präferenz und Web-Fallback.", pickerDescription: "Login nur über die API, ohne Web-Fallback.",
needsCredentials: true needsCredentials: true
}, },
{ {
kind: "megadebrid-web", kind: "megadebrid-web",
service: "megadebrid", service: "megadebrid-web",
serviceLabel: "Mega-Debrid", serviceLabel: "Mega-Debrid",
title: "Mega-Debrid Web", title: "Mega-Debrid Web",
modeLabel: "Web", modeLabel: "Web",
pickerDescription: "Login mit Web-Präferenz über Nutzername und Passwort.", pickerDescription: "Login nur über Web, ohne API-Fallback.",
needsCredentials: true needsCredentials: true
}, },
{ {
@ -209,7 +209,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
} }
]; ];
const ACCOUNT_SERVICES: AccountService[] = ["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink", "linksnappy"]; const ACCOUNT_SERVICES: AccountService[] = ["realdebrid", "megadebrid-api", "megadebrid-web", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink", "linksnappy"];
const ACCOUNT_COLUMN_STORAGE_KEY = "rd-account-column-widths"; const ACCOUNT_COLUMN_STORAGE_KEY = "rd-account-column-widths";
const ACCOUNT_COLUMN_DEFAULT_WIDTHS: Record<AccountColumnKey, number> = { const ACCOUNT_COLUMN_DEFAULT_WIDTHS: Record<AccountColumnKey, number> = {
service: 220, service: 220,
@ -277,13 +277,20 @@ function getAccountPickerFunctionLabel(option: AccountOption): string {
} }
} }
function hasMegaDebridCredentials(settings: AppSettings): boolean {
return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim());
}
function getConfiguredProvidersFromSettings(settings: AppSettings): DebridProvider[] { function getConfiguredProvidersFromSettings(settings: AppSettings): DebridProvider[] {
const list: DebridProvider[] = []; const list: DebridProvider[] = [];
if (settings.token.trim() || settings.realDebridUseWebLogin) { if (settings.token.trim() || settings.realDebridUseWebLogin) {
list.push("realdebrid"); list.push("realdebrid");
} }
if (settings.megaLogin.trim() && settings.megaPassword.trim()) { if (hasMegaDebridCredentials(settings) && settings.megaDebridApiEnabled) {
list.push("megadebrid"); list.push("megadebrid-api");
}
if (hasMegaDebridCredentials(settings) && settings.megaDebridWebEnabled) {
list.push("megadebrid-web");
} }
if (settings.bestDebridUseWebLogin || settings.bestToken.trim()) { if (settings.bestDebridUseWebLogin || settings.bestToken.trim()) {
list.push("bestdebrid"); list.push("bestdebrid");
@ -330,9 +337,10 @@ function getConfiguredAccountKind(settings: AppSettings, service: AccountService
case "realdebrid": case "realdebrid":
if (settings.realDebridUseWebLogin) return "realdebrid-web"; if (settings.realDebridUseWebLogin) return "realdebrid-web";
return settings.token.trim() ? "realdebrid-api" : null; return settings.token.trim() ? "realdebrid-api" : null;
case "megadebrid": case "megadebrid-api":
if (!settings.megaLogin.trim() || !settings.megaPassword.trim()) return null; return hasMegaDebridCredentials(settings) && settings.megaDebridApiEnabled ? "megadebrid-api" : null;
return settings.megaDebridPreferApi ? "megadebrid-api" : "megadebrid-web"; case "megadebrid-web":
return hasMegaDebridCredentials(settings) && settings.megaDebridWebEnabled ? "megadebrid-web" : null;
case "bestdebrid": case "bestdebrid":
if (settings.bestDebridUseWebLogin) return "bestdebrid-web"; if (settings.bestDebridUseWebLogin) return "bestdebrid-web";
return settings.bestToken.trim() ? "bestdebrid-api" : null; return settings.bestToken.trim() ? "bestdebrid-api" : null;
@ -446,9 +454,9 @@ function applyAccountDialogToSettings(settings: AppSettings, dialog: AccountDial
case "realdebrid-web": case "realdebrid-web":
return { ...settings, token: "", realDebridUseWebLogin: true }; return { ...settings, token: "", realDebridUseWebLogin: true };
case "megadebrid-api": case "megadebrid-api":
return { ...settings, megaLogin: login, megaPassword: password, megaDebridPreferApi: true }; return { ...settings, megaLogin: login, megaPassword: password, megaDebridApiEnabled: true, megaDebridPreferApi: true };
case "megadebrid-web": case "megadebrid-web":
return { ...settings, megaLogin: login, megaPassword: password, megaDebridPreferApi: false }; return { ...settings, megaLogin: login, megaPassword: password, megaDebridWebEnabled: true, megaDebridPreferApi: false };
case "bestdebrid-api": case "bestdebrid-api":
return { ...settings, bestToken: token, bestDebridUseWebLogin: false }; return { ...settings, bestToken: token, bestDebridUseWebLogin: false };
case "bestdebrid-web": case "bestdebrid-web":
@ -474,8 +482,14 @@ function clearAccountServiceFromSettings(settings: AppSettings, service: Account
switch (service) { switch (service) {
case "realdebrid": case "realdebrid":
return { ...settings, token: "", realDebridUseWebLogin: false }; return { ...settings, token: "", realDebridUseWebLogin: false };
case "megadebrid": case "megadebrid-api":
return { ...settings, megaLogin: "", megaPassword: "" }; return settings.megaDebridWebEnabled
? { ...settings, megaDebridApiEnabled: false }
: { ...settings, megaLogin: "", megaPassword: "", megaDebridApiEnabled: false };
case "megadebrid-web":
return settings.megaDebridApiEnabled
? { ...settings, megaDebridWebEnabled: false }
: { ...settings, megaLogin: "", megaPassword: "", megaDebridWebEnabled: false };
case "bestdebrid": case "bestdebrid":
return { ...settings, bestToken: "", bestDebridUseWebLogin: false }; return { ...settings, bestToken: "", bestDebridUseWebLogin: false };
case "alldebrid": case "alldebrid":
@ -522,9 +536,9 @@ const emptyStats = (): DownloadStats => ({
const emptySnapshot = (): UiSnapshot => ({ const emptySnapshot = (): UiSnapshot => ({
settings: { settings: {
token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaDebridPreferApi: true, bestToken: "", bestDebridUseWebLogin: false, allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "", token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaDebridApiEnabled: false, megaDebridWebEnabled: false, megaDebridPreferApi: true, bestToken: "", bestDebridUseWebLogin: false, allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "",
archivePasswordList: "", archivePasswordList: "",
rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid", rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid-api",
providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "", providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "",
autoExtract: true, autoRename4sf4sj: false, extractDir: "", createExtractSubfolder: true, hybridExtract: true, autoExtract: true, autoRename4sf4sj: false, extractDir: "", createExtractSubfolder: true, hybridExtract: true,
collectMkvToLibrary: false, mkvLibraryDir: "", collectMkvToLibrary: false, mkvLibraryDir: "",
@ -556,7 +570,16 @@ const cleanupLabels: Record<string, string> = {
const AUTO_RENDER_PACKAGE_LIMIT = 260; const AUTO_RENDER_PACKAGE_LIMIT = 260;
const providerLabels: Record<DebridProvider, string> = { const providerLabels: Record<DebridProvider, string> = {
realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid", ddownload: "DDownload", onefichier: "1Fichier", debridlink: "Debrid-Link", linksnappy: "LinkSnappy" realdebrid: "Real-Debrid",
megadebrid: "Mega-Debrid",
"megadebrid-api": "Mega-Debrid API",
"megadebrid-web": "Mega-Debrid Web",
bestdebrid: "BestDebrid",
alldebrid: "AllDebrid",
ddownload: "DDownload",
onefichier: "1Fichier",
debridlink: "Debrid-Link",
linksnappy: "LinkSnappy"
}; };
const KNOWN_HOSTERS: { id: string; label: string }[] = [ const KNOWN_HOSTERS: { id: string; label: string }[] = [
@ -591,7 +614,10 @@ const KNOWN_HOSTERS: { id: string; label: string }[] = [
function providerLabelWithMode(provider: DebridProvider, settings: AppSettings): string { function providerLabelWithMode(provider: DebridProvider, settings: AppSettings): string {
const base = providerLabels[provider]; const base = providerLabels[provider];
const kind = getConfiguredAccountKind(settings, provider); if (provider === "megadebrid" || provider === "megadebrid-api" || provider === "megadebrid-web") {
return base;
}
const kind = getConfiguredAccountKind(settings, provider as AccountService);
if (!kind) return base; if (!kind) return base;
const opt = ACCOUNT_OPTIONS.find((o) => o.kind === kind); const opt = ACCOUNT_OPTIONS.find((o) => o.kind === kind);
return opt?.modeLabel ? `${base} (${opt.modeLabel})` : base; return opt?.modeLabel ? `${base} (${opt.modeLabel})` : base;
@ -1573,9 +1599,9 @@ export function App(): ReactElement {
let statusLabel = "Konfiguriert"; let statusLabel = "Konfiguriert";
let note = ""; let note = "";
if (kind === "megadebrid-api") { if (kind === "megadebrid-api") {
note = "API wird bevorzugt, Web bleibt als Fallback aktiv."; note = "Nur API aktiv. Kein Web-Fallback.";
} else if (kind === "megadebrid-web") { } else if (kind === "megadebrid-web") {
note = "Web wird bevorzugt, API bleibt als Fallback aktiv."; note = "Nur Web aktiv. Kein API-Fallback.";
} else if (kind === "realdebrid-web") { } else if (kind === "realdebrid-web") {
note = "Login kann bei Bedarf direkt aus der Liste geöffnet werden."; note = "Login kann bei Bedarf direkt aus der Liste geöffnet werden.";
} else if (kind === "bestdebrid-web") { } else if (kind === "bestdebrid-web") {
@ -4285,10 +4311,10 @@ export function App(): ReactElement {
<div className="account-modal-note">Der Web-Login nutzt ein echtes Browserfenster, damit reCAPTCHA sauber laeuft.</div> <div className="account-modal-note">Der Web-Login nutzt ein echtes Browserfenster, damit reCAPTCHA sauber laeuft.</div>
)} )}
{accountDialog.kind === "megadebrid-api" && ( {accountDialog.kind === "megadebrid-api" && (
<div className="account-modal-note">Mega-Debrid versucht zuerst die API und faellt bei Bedarf auf Web zurueck.</div> <div className="account-modal-note">Dieser Account nutzt nur die Mega-Debrid API. Kein Web-Fallback.</div>
)} )}
{accountDialog.kind === "megadebrid-web" && ( {accountDialog.kind === "megadebrid-web" && (
<div className="account-modal-note">Mega-Debrid bevorzugt Web. Die API bleibt als Fallback erhalten.</div> <div className="account-modal-note">Dieser Account nutzt nur Mega-Debrid Web. Kein API-Fallback.</div>
)} )}
{accountDialogOption.service === "alldebrid" && allDebridHostInfo && ( {accountDialogOption.service === "alldebrid" && allDebridHostInfo && (

View File

@ -14,7 +14,17 @@ export type CleanupMode = "none" | "trash" | "delete";
export type ConflictMode = "overwrite" | "skip" | "rename" | "ask"; export type ConflictMode = "overwrite" | "skip" | "rename" | "ask";
export type SpeedMode = "global" | "per_download"; export type SpeedMode = "global" | "per_download";
export type FinishedCleanupPolicy = "never" | "immediate" | "on_start" | "package_done"; export type FinishedCleanupPolicy = "never" | "immediate" | "on_start" | "package_done";
export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink" | "linksnappy"; export type DebridProvider =
| "realdebrid"
| "megadebrid"
| "megadebrid-api"
| "megadebrid-web"
| "bestdebrid"
| "alldebrid"
| "ddownload"
| "onefichier"
| "debridlink"
| "linksnappy";
export type DebridFallbackProvider = DebridProvider | "none"; export type DebridFallbackProvider = DebridProvider | "none";
export type AppTheme = "dark" | "light"; export type AppTheme = "dark" | "light";
export type PackagePriority = "high" | "normal" | "low"; export type PackagePriority = "high" | "normal" | "low";
@ -41,6 +51,8 @@ export interface AppSettings {
realDebridUseWebLogin: boolean; realDebridUseWebLogin: boolean;
megaLogin: string; megaLogin: string;
megaPassword: string; megaPassword: string;
megaDebridApiEnabled: boolean;
megaDebridWebEnabled: boolean;
megaDebridPreferApi: boolean; megaDebridPreferApi: boolean;
bestToken: string; bestToken: string;
bestDebridUseWebLogin: boolean; bestDebridUseWebLogin: boolean;

View File

@ -444,6 +444,68 @@ describe("debrid service", () => {
expect(megaWeb).toHaveBeenCalledTimes(1); expect(megaWeb).toHaveBeenCalledTimes(1);
}); });
it("does not fallback from Mega API to Mega Web unless Mega Web is a separate provider in the order", async () => {
const settings = {
...defaultSettings(),
token: "",
bestToken: "",
allDebridToken: "",
megaLogin: "user",
megaPassword: "pass",
megaDebridApiEnabled: true,
megaDebridWebEnabled: true,
providerPrimary: "megadebrid-api" as const,
providerSecondary: "none" as const,
providerTertiary: "none" as const,
autoProviderFallback: true
};
globalThis.fetch = (async () => new Response("not-found", { status: 404 })) as typeof fetch;
const megaWeb = vi.fn(async () => ({
fileName: "should-not-run.rar",
directUrl: "https://unused",
fileSize: null,
retriesUsed: 0
}));
const service = new DebridService(settings, { megaWebUnrestrict: megaWeb });
await expect(service.unrestrictLink("https://rapidgator.net/file/mega-api-only.rar.html")).rejects.toThrow(/mega-debrid api/i);
expect(megaWeb).toHaveBeenCalledTimes(0);
});
it("uses Mega Web only when it is configured as a separate fallback provider", async () => {
const settings = {
...defaultSettings(),
token: "",
bestToken: "",
allDebridToken: "",
megaLogin: "user",
megaPassword: "pass",
megaDebridApiEnabled: true,
megaDebridWebEnabled: true,
providerPrimary: "megadebrid-api" as const,
providerSecondary: "megadebrid-web" as const,
providerTertiary: "none" as const,
autoProviderFallback: true
};
globalThis.fetch = (async () => new Response("not-found", { status: 404 })) as typeof fetch;
const megaWeb = vi.fn(async () => ({
fileName: "from-separate-web.rar",
directUrl: "https://mega-web.example/from-separate-web.rar",
fileSize: null,
retriesUsed: 0
}));
const service = new DebridService(settings, { megaWebUnrestrict: megaWeb });
const result = await service.unrestrictLink("https://rapidgator.net/file/from-separate-web.rar.html");
expect(result.provider).toBe("megadebrid-web");
expect(result.directUrl).toBe("https://mega-web.example/from-separate-web.rar");
expect(megaWeb).toHaveBeenCalledTimes(1);
});
it("aborts Mega web unrestrict when caller signal is cancelled", async () => { it("aborts Mega web unrestrict when caller signal is cancelled", async () => {
const settings = { const settings = {
...defaultSettings(), ...defaultSettings(),

View File

@ -146,6 +146,36 @@ describe("settings storage", () => {
expect(normalized.providerTertiary).toBe("none"); expect(normalized.providerTertiary).toBe("none");
}); });
it("migrates legacy MegaDebrid provider selections to explicit API/Web providers", () => {
const apiNormalized = normalizeSettings({
...defaultSettings(),
megaLogin: "mega-user",
megaPassword: "mega-pass",
megaDebridPreferApi: true,
providerPrimary: "megadebrid" as unknown as AppSettings["providerPrimary"],
providerSecondary: "megadebrid" as unknown as AppSettings["providerSecondary"],
disabledProviders: ["megadebrid" as unknown as AppSettings["providerPrimary"]]
});
expect(apiNormalized.providerPrimary).toBe("megadebrid-api");
expect(apiNormalized.providerSecondary).toBe("none");
expect(apiNormalized.disabledProviders).toEqual(["megadebrid-api", "megadebrid-web"]);
const webNormalized = normalizeSettings({
...defaultSettings(),
megaLogin: "mega-user",
megaPassword: "mega-pass",
megaDebridPreferApi: false,
megaDebridApiEnabled: false,
megaDebridWebEnabled: true,
providerPrimary: "megadebrid" as unknown as AppSettings["providerPrimary"],
hosterRouting: { rapidgator: "megadebrid" as unknown as AppSettings["providerPrimary"] }
});
expect(webNormalized.providerPrimary).toBe("megadebrid-web");
expect(webNormalized.hosterRouting.rapidgator).toBe("megadebrid-web");
});
it("normalizes archive password list line endings", () => { it("normalizes archive password list line endings", () => {
const normalized = normalizeSettings({ const normalized = normalizeSettings({
...defaultSettings(), ...defaultSettings(),