From 0003d786d85987428b68f75a5ab5c57ba198c786 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 6 Mar 2026 20:43:15 +0100 Subject: [PATCH] Release v1.6.90 --- package-lock.json | 4 +- package.json | 2 +- src/main/constants.ts | 4 +- src/main/debrid.ts | 87 +++++++++++++++++++++++--------- src/main/download-manager.ts | 62 ++++++++++++++++++----- src/main/storage.ts | 96 ++++++++++++++++++++++++++++++------ src/renderer/App.tsx | 72 ++++++++++++++++++--------- src/shared/types.ts | 14 +++++- tests/debrid.test.ts | 62 +++++++++++++++++++++++ tests/storage.test.ts | 30 +++++++++++ 10 files changed, 354 insertions(+), 79 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6854ce6..3fed182 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "1.6.89", + "version": "1.6.90", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "1.6.89", + "version": "1.6.90", "license": "MIT", "dependencies": { "adm-zip": "^0.5.16", diff --git a/package.json b/package.json index 2180379..e37e037 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.6.89", + "version": "1.6.90", "description": "Desktop downloader", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/constants.ts b/src/main/constants.ts index fddcea3..6760109 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -44,6 +44,8 @@ export function defaultSettings(): AppSettings { realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", + megaDebridApiEnabled: false, + megaDebridWebEnabled: false, megaDebridPreferApi: true, bestToken: "", bestDebridUseWebLogin: false, @@ -58,7 +60,7 @@ export function defaultSettings(): AppSettings { archivePasswordList: "", rememberToken: true, providerPrimary: "realdebrid", - providerSecondary: "megadebrid", + providerSecondary: "megadebrid-api", providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: baseDir, diff --git a/src/main/debrid.ts b/src/main/debrid.ts index dc37cb7..84a3cbe 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -25,6 +25,8 @@ const LINKSNAPPY_API_BASE = "https://linksnappy.com/api"; const PROVIDER_LABELS: Record = { realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", + "megadebrid-api": "Mega-Debrid API", + "megadebrid-web": "Mega-Debrid Web", bestdebrid: "BestDebrid", alldebrid: "AllDebrid", 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 = { url: string; useAuthHeader: boolean; @@ -692,7 +720,9 @@ class MegaDebridClient { private password: string; - private preferApi: boolean; + private mode: "api" | "web"; + + private allowApiFallback: boolean; private static cachedApiToken = ""; @@ -700,10 +730,11 @@ class MegaDebridClient { private static pendingConnect: Promise | 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.password = password; - this.preferApi = preferApi; + this.mode = mode; + this.allowApiFallback = allowApiFallback; this.megaWebUnrestrict = megaWebUnrestrict; } @@ -839,25 +870,27 @@ class MegaDebridClient { } public async unrestrictLink(link: string, signal?: AbortSignal): Promise { - if (this.preferApi && this.login.trim() && this.password.trim()) { - // API mode: try API first, fall back to web on failure + if (this.mode === "api" && this.login.trim() && this.password.trim()) { try { const apiResult = await this.unrestrictViaApi(link, signal); if (apiResult) { logger.info(`Mega-Debrid (API) unrestrict OK: ${apiResult.fileName}`); return apiResult; } + throw new Error("Mega-Debrid API: Login oder Unrestrict fehlgeschlagen"); } catch (error) { const errorText = compactErrorText(error); if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) { throw error; } + if (!this.allowApiFallback) { + throw error; + } logger.warn(`Mega-Debrid API fehlgeschlagen, versuche Web-Fallback: ${errorText}`); } return this.unrestrictViaWeb(link, signal); } - // Web mode only return this.unrestrictViaWeb(link, signal); } } @@ -2036,33 +2069,38 @@ export class DebridService { } private isProviderConfiguredFor(settings: AppSettings, provider: DebridProvider): boolean { - if ((settings.disabledProviders || []).includes(provider)) return false; - if (provider === "realdebrid") { + const effectiveProvider = resolveMegaDebridProvider(settings, provider); + if ((settings.disabledProviders || []).includes(provider) || (settings.disabledProviders || []).includes(effectiveProvider)) return false; + if (effectiveProvider === "realdebrid") { return Boolean(this.shouldUseRealDebridWeb(settings) || settings.token.trim()); } - if (provider === "megadebrid") { - return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim()); + if (effectiveProvider === "megadebrid-api") { + 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()); } - if (provider === "ddownload") { + if (effectiveProvider === "ddownload") { return Boolean(settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim()); } - if (provider === "onefichier") { + if (effectiveProvider === "onefichier") { return Boolean(settings.oneFichierApiKey.trim()); } - if (provider === "debridlink") { + if (effectiveProvider === "debridlink") { return Boolean(settings.debridLinkApiKeys.trim()); } - if (provider === "linksnappy") { + if (effectiveProvider === "linksnappy") { return Boolean(settings.linkSnappyLogin.trim() && settings.linkSnappyPassword.trim()); } return Boolean(this.shouldUseBestDebridWeb(settings) || settings.bestToken.trim()); } private async unrestrictViaProvider(settings: AppSettings, provider: DebridProvider, link: string, signal?: AbortSignal): Promise { - if (provider === "realdebrid") { + const effectiveProvider = resolveMegaDebridProvider(settings, provider); + if (effectiveProvider === "realdebrid") { if (this.shouldUseRealDebridWeb(settings) && this.options.realDebridWebUnrestrict) { const result = await this.options.realDebridWebUnrestrict(link, signal); if (!result) { @@ -2075,10 +2113,13 @@ export class DebridService { result.sourceLabel = "API"; return result; } - if (provider === "megadebrid") { - return new MegaDebridClient(settings.megaLogin, settings.megaPassword, settings.megaDebridPreferApi, this.options.megaWebUnrestrict).unrestrictLink(link, signal); + if (effectiveProvider === "megadebrid-api") { + 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) { const result = await this.options.allDebridWebUnrestrict(link, signal); if (!result) { @@ -2091,18 +2132,18 @@ export class DebridService { adResult.sourceLabel = "API"; return adResult; } - if (provider === "ddownload") { + if (effectiveProvider === "ddownload") { return this.getDdownloadClient(settings.ddownloadLogin, settings.ddownloadPassword).unrestrictLink(link, signal); } - if (provider === "onefichier") { + if (effectiveProvider === "onefichier") { return new OneFichierClient(settings.oneFichierApiKey).unrestrictLink(link, signal); } - if (provider === "debridlink") { + if (effectiveProvider === "debridlink") { const dlResult = await this.getDebridLinkClient(settings.debridLinkApiKeys).unrestrictLink(link, signal); dlResult.sourceLabel = dlResult.sourceLabel || "API"; return dlResult; } - if (provider === "linksnappy") { + if (effectiveProvider === "linksnappy") { return this.getLinkSnappyClient(settings.linkSnappyLogin, settings.linkSnappyPassword).unrestrictLink(link, signal); } if (this.shouldUseBestDebridWeb(settings) && this.options.bestDebridWebUnrestrict) { diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index b8675da..86ba7d3 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -371,6 +371,12 @@ function providerLabel(provider: DownloadItem["provider"]): string { if (provider === "megadebrid") { return "Mega-Debrid"; } + if (provider === "megadebrid-api") { + return "Mega-Debrid API"; + } + if (provider === "megadebrid-web") { + return "Mega-Debrid Web"; + } if (provider === "bestdebrid") { return "BestDebrid"; } @@ -383,6 +389,23 @@ function providerLabel(provider: DownloadItem["provider"]): string { 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 { const resolved = path.resolve(filePath); return process.platform === "win32" ? resolved.toLowerCase() : resolved; @@ -3462,7 +3485,16 @@ export class DownloadManager extends EventEmitter { this.session.reconnectReason = ""; 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; } if (item.status === "cancelled" && item.fullStatus === "Gestoppt") { @@ -4302,7 +4334,7 @@ export class DownloadManager extends EventEmitter { entry.cooldownUntil = now + cooldownMs; logger.warn(`Provider Circuit-Breaker: ${key} ${entry.count} konsekutive Fehler, Cooldown ${cooldownMs / 1000}s`); // 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 { this.invalidateMegaSessionFn(); } catch { /* ignore */ } @@ -4344,28 +4376,34 @@ export class DownloadManager extends EventEmitter { } 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; } - if (provider === "realdebrid") { + if (effectiveProvider === "realdebrid") { return Boolean(this.settings.realDebridUseWebLogin || this.settings.token.trim()); } - if (provider === "megadebrid") { - return Boolean(this.settings.megaLogin.trim() && this.settings.megaPassword.trim()); + if (effectiveProvider === "megadebrid-api") { + 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()); } - if (provider === "alldebrid") { + if (effectiveProvider === "alldebrid") { 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()); } - if (provider === "onefichier") { + if (effectiveProvider === "onefichier") { return Boolean(this.settings.oneFichierApiKey.trim()); } - if (provider === "debridlink") { + if (effectiveProvider === "debridlink") { return Boolean(this.settings.debridLinkApiKeys.trim()); } if (provider === "linksnappy") { @@ -4376,7 +4414,7 @@ export class DownloadManager extends EventEmitter { private getExpectedProviderForItem(item: DownloadItem): DebridProvider | null { if (item.provider) { - return item.provider; + return resolveMegaDebridProvider(this.settings, item.provider); } const hosterKey = extractHosterKey(item.url); diff --git a/src/main/storage.ts b/src/main/storage.ts index 3334805..42cc02f 100644 --- a/src/main/storage.ts +++ b/src/main/storage.ts @@ -1,12 +1,12 @@ import fs from "node:fs"; import fsp from "node:fs/promises"; 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 { logger } from "./logger"; -const VALID_PRIMARY_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink", "linksnappy"]); -const VALID_FALLBACK_PROVIDERS = new Set(["none", "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-api", "megadebrid-web", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink", "linksnappy"]); const VALID_CLEANUP_MODES = new Set(["none", "trash", "delete"]); const VALID_CONFLICT_MODES = new Set(["overwrite", "skip", "rename", "ask"]); const VALID_FINISHED_POLICIES = new Set(["never", "immediate", "on_start", "package_done"]); @@ -17,7 +17,7 @@ const VALID_PACKAGE_PRIORITIES = new Set(["high", "normal", "low"]); const VALID_DOWNLOAD_STATUSES = new Set([ "queued", "validating", "downloading", "paused", "reconnect_wait", "extracting", "integrity_check", "completed", "failed", "cancelled" ]); -const VALID_ITEM_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink"]); +const VALID_ITEM_PROVIDERS = new Set(["realdebrid", "megadebrid", "megadebrid-api", "megadebrid-web", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink"]); const VALID_ONLINE_STATUSES = new Set(["online", "offline", "checking"]); function asText(value: unknown): string { @@ -91,14 +91,66 @@ function normalizeColumnOrder(raw: unknown): string[] { return result; } -function normalizeHosterRouting(raw: unknown): Record { +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(); + 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 { if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}; const result: Record = {}; for (const [key, value] of Object.entries(raw as Record)) { const hoster = String(key).trim().toLowerCase(); - const provider = String(value ?? "").trim(); - if (hoster && VALID_PRIMARY_PROVIDERS.has(provider)) { - result[hoster] = provider as DebridProvider; + const provider = normalizeConfiguredProvider(value, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled); + if (hoster && provider) { + result[hoster] = provider; } } return result; @@ -118,12 +170,24 @@ function migrateUpdateRepo(raw: string, fallback: string): string { export function normalizeSettings(settings: AppSettings): AppSettings { 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 = { token: asText(settings.token), realDebridUseWebLogin: Boolean(settings.realDebridUseWebLogin), - megaLogin: asText(settings.megaLogin), - megaPassword: asText(settings.megaPassword), - megaDebridPreferApi: settings.megaDebridPreferApi !== undefined ? Boolean(settings.megaDebridPreferApi) : true, + megaLogin, + megaPassword, + megaDebridApiEnabled, + megaDebridWebEnabled, + megaDebridPreferApi, bestToken: asText(settings.bestToken), bestDebridUseWebLogin: Boolean(settings.bestDebridUseWebLogin), allDebridToken: asText(settings.allDebridToken), @@ -136,9 +200,9 @@ export function normalizeSettings(settings: AppSettings): AppSettings { linkSnappyPassword: asText(settings.linkSnappyPassword), archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n|\r/g, "\n"), rememberToken: Boolean(settings.rememberToken), - providerPrimary: settings.providerPrimary, - providerSecondary: settings.providerSecondary, - providerTertiary: settings.providerTertiary, + providerPrimary: normalizeConfiguredProvider(settings.providerPrimary, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled) || defaults.providerPrimary, + providerSecondary: normalizeFallbackProvider(settings.providerSecondary, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled), + providerTertiary: normalizeFallbackProvider(settings.providerTertiary, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled), autoProviderFallback: Boolean(settings.autoProviderFallback), outputDir: normalizeAbsoluteDir(settings.outputDir, defaults.outputDir), packageName: asText(settings.packageName), @@ -177,8 +241,8 @@ export function normalizeSettings(settings: AppSettings): AppSettings { columnOrder: normalizeColumnOrder(settings.columnOrder), extractCpuPriority: settings.extractCpuPriority, 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[] : [], - hosterRouting: normalizeHosterRouting(settings.hosterRouting) + disabledProviders: normalizeDisabledProviders(settings.disabledProviders), + hosterRouting: normalizeHosterRouting(settings.hosterRouting, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled) }; if (!VALID_PRIMARY_PROVIDERS.has(normalized.providerPrimary)) { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index c26afda..512a8a7 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -53,7 +53,7 @@ interface LinkPopupState { 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 = | "realdebrid-api" | "realdebrid-web" @@ -121,20 +121,20 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ }, { kind: "megadebrid-api", - service: "megadebrid", + service: "megadebrid-api", serviceLabel: "Mega-Debrid", title: "Mega-Debrid API", modeLabel: "API", - pickerDescription: "Login mit API-Präferenz und Web-Fallback.", + pickerDescription: "Login nur über die API, ohne Web-Fallback.", needsCredentials: true }, { kind: "megadebrid-web", - service: "megadebrid", + service: "megadebrid-web", serviceLabel: "Mega-Debrid", title: "Mega-Debrid Web", modeLabel: "Web", - pickerDescription: "Login mit Web-Präferenz über Nutzername und Passwort.", + pickerDescription: "Login nur über Web, ohne API-Fallback.", 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_DEFAULT_WIDTHS: Record = { 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[] { const list: DebridProvider[] = []; if (settings.token.trim() || settings.realDebridUseWebLogin) { list.push("realdebrid"); } - if (settings.megaLogin.trim() && settings.megaPassword.trim()) { - list.push("megadebrid"); + if (hasMegaDebridCredentials(settings) && settings.megaDebridApiEnabled) { + list.push("megadebrid-api"); + } + if (hasMegaDebridCredentials(settings) && settings.megaDebridWebEnabled) { + list.push("megadebrid-web"); } if (settings.bestDebridUseWebLogin || settings.bestToken.trim()) { list.push("bestdebrid"); @@ -330,9 +337,10 @@ function getConfiguredAccountKind(settings: AppSettings, service: AccountService case "realdebrid": if (settings.realDebridUseWebLogin) return "realdebrid-web"; return settings.token.trim() ? "realdebrid-api" : null; - case "megadebrid": - if (!settings.megaLogin.trim() || !settings.megaPassword.trim()) return null; - return settings.megaDebridPreferApi ? "megadebrid-api" : "megadebrid-web"; + case "megadebrid-api": + return hasMegaDebridCredentials(settings) && settings.megaDebridApiEnabled ? "megadebrid-api" : null; + case "megadebrid-web": + return hasMegaDebridCredentials(settings) && settings.megaDebridWebEnabled ? "megadebrid-web" : null; case "bestdebrid": if (settings.bestDebridUseWebLogin) return "bestdebrid-web"; return settings.bestToken.trim() ? "bestdebrid-api" : null; @@ -446,9 +454,9 @@ function applyAccountDialogToSettings(settings: AppSettings, dialog: AccountDial case "realdebrid-web": return { ...settings, token: "", realDebridUseWebLogin: true }; case "megadebrid-api": - return { ...settings, megaLogin: login, megaPassword: password, megaDebridPreferApi: true }; + return { ...settings, megaLogin: login, megaPassword: password, megaDebridApiEnabled: true, megaDebridPreferApi: true }; case "megadebrid-web": - return { ...settings, megaLogin: login, megaPassword: password, megaDebridPreferApi: false }; + return { ...settings, megaLogin: login, megaPassword: password, megaDebridWebEnabled: true, megaDebridPreferApi: false }; case "bestdebrid-api": return { ...settings, bestToken: token, bestDebridUseWebLogin: false }; case "bestdebrid-web": @@ -474,8 +482,14 @@ function clearAccountServiceFromSettings(settings: AppSettings, service: Account switch (service) { case "realdebrid": return { ...settings, token: "", realDebridUseWebLogin: false }; - case "megadebrid": - return { ...settings, megaLogin: "", megaPassword: "" }; + case "megadebrid-api": + 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": return { ...settings, bestToken: "", bestDebridUseWebLogin: false }; case "alldebrid": @@ -522,9 +536,9 @@ const emptyStats = (): DownloadStats => ({ const emptySnapshot = (): UiSnapshot => ({ 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: "", - rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid", + rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid-api", providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "", autoExtract: true, autoRename4sf4sj: false, extractDir: "", createExtractSubfolder: true, hybridExtract: true, collectMkvToLibrary: false, mkvLibraryDir: "", @@ -556,7 +570,16 @@ const cleanupLabels: Record = { const AUTO_RENDER_PACKAGE_LIMIT = 260; const providerLabels: Record = { - 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 }[] = [ @@ -591,7 +614,10 @@ const KNOWN_HOSTERS: { id: string; label: string }[] = [ function providerLabelWithMode(provider: DebridProvider, settings: AppSettings): string { 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; const opt = ACCOUNT_OPTIONS.find((o) => o.kind === kind); return opt?.modeLabel ? `${base} (${opt.modeLabel})` : base; @@ -1573,9 +1599,9 @@ export function App(): ReactElement { let statusLabel = "Konfiguriert"; let note = ""; 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") { - note = "Web wird bevorzugt, API bleibt als Fallback aktiv."; + note = "Nur Web aktiv. Kein API-Fallback."; } else if (kind === "realdebrid-web") { note = "Login kann bei Bedarf direkt aus der Liste geöffnet werden."; } else if (kind === "bestdebrid-web") { @@ -4285,10 +4311,10 @@ export function App(): ReactElement {
Der Web-Login nutzt ein echtes Browserfenster, damit reCAPTCHA sauber laeuft.
)} {accountDialog.kind === "megadebrid-api" && ( -
Mega-Debrid versucht zuerst die API und faellt bei Bedarf auf Web zurueck.
+
Dieser Account nutzt nur die Mega-Debrid API. Kein Web-Fallback.
)} {accountDialog.kind === "megadebrid-web" && ( -
Mega-Debrid bevorzugt Web. Die API bleibt als Fallback erhalten.
+
Dieser Account nutzt nur Mega-Debrid Web. Kein API-Fallback.
)} {accountDialogOption.service === "alldebrid" && allDebridHostInfo && ( diff --git a/src/shared/types.ts b/src/shared/types.ts index 4a4041f..83a8243 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -14,7 +14,17 @@ 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" | "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 AppTheme = "dark" | "light"; export type PackagePriority = "high" | "normal" | "low"; @@ -41,6 +51,8 @@ export interface AppSettings { realDebridUseWebLogin: boolean; megaLogin: string; megaPassword: string; + megaDebridApiEnabled: boolean; + megaDebridWebEnabled: boolean; megaDebridPreferApi: boolean; bestToken: string; bestDebridUseWebLogin: boolean; diff --git a/tests/debrid.test.ts b/tests/debrid.test.ts index 75f406b..861178e 100644 --- a/tests/debrid.test.ts +++ b/tests/debrid.test.ts @@ -444,6 +444,68 @@ describe("debrid service", () => { 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 () => { const settings = { ...defaultSettings(), diff --git a/tests/storage.test.ts b/tests/storage.test.ts index cc23bdd..83e523b 100644 --- a/tests/storage.test.ts +++ b/tests/storage.test.ts @@ -146,6 +146,36 @@ describe("settings storage", () => { 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", () => { const normalized = normalizeSettings({ ...defaultSettings(),