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
+3 -1
View File
@@ -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,
+64 -23
View File
@@ -25,6 +25,8 @@ const LINKSNAPPY_API_BASE = "https://linksnappy.com/api";
const PROVIDER_LABELS: Record<DebridProvider, string> = {
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<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.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<UnrestrictedLink> {
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<UnrestrictedLink> {
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) {
+50 -12
View File
@@ -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);
+80 -16
View File
@@ -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<string>(["high", "normal", "low"]);
const VALID_DOWNLOAD_STATUSES = new Set<DownloadStatus>([
"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"]);
function asText(value: unknown): string {
@@ -91,14 +91,66 @@ function normalizeColumnOrder(raw: unknown): string[] {
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 {};
const result: Record<string, DebridProvider> = {};
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
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)) {