Release v1.6.90
This commit is contained in:
parent
0eb3403e40
commit
0003d786d8
4
package-lock.json
generated
4
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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<AccountColumnKey, number> = {
|
||||
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<string, string> = {
|
||||
const AUTO_RENDER_PACKAGE_LIMIT = 260;
|
||||
|
||||
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 }[] = [
|
||||
@ -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 {
|
||||
<div className="account-modal-note">Der Web-Login nutzt ein echtes Browserfenster, damit reCAPTCHA sauber laeuft.</div>
|
||||
)}
|
||||
{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" && (
|
||||
<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 && (
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user