release: ship v2.0.23 queue and account reliability fixes
Normalize RapidGator aliases, stabilize active queue ordering, preserve user-controlled package expansion, and align compact queue status presentation. Separate Mega-Debrid API and Web credential pools, migrate legacy credentials and disabled states safely, refresh live account availability without restart, honor disabled credential persistence, and invalidate Web sessions without allowing stale in-flight, retry, or queued logins to restore old cookies. Expand regression coverage for account isolation, legacy migration, session races, live scheduler refresh, update notes, and responsive queue behavior.
This commit is contained in:
@@ -2,6 +2,41 @@
|
|||||||
|
|
||||||
All notable changes to Multi-Debrid Downloader are documented in this file.
|
All notable changes to Multi-Debrid Downloader are documented in this file.
|
||||||
|
|
||||||
|
## [2.0.23] - 2026-08-11
|
||||||
|
|
||||||
|
### Update experience
|
||||||
|
|
||||||
|
- Changed the available-update header action to a dedicated light-blue treatment with dark high-contrast text.
|
||||||
|
- Added cumulative release notes for every stable version newer than the installed application, ordered from newest to oldest.
|
||||||
|
- Excluded draft releases, prereleases, the installed version, and older versions from cumulative update notes.
|
||||||
|
- Added a bounded vertical scroll area so long multi-version changelogs remain usable without overflowing the update dialog.
|
||||||
|
|
||||||
|
### Download queue
|
||||||
|
|
||||||
|
- Unified RapidGator main and short-link domains under one host identity for icons, host counts, routing, limits, and cooldowns.
|
||||||
|
- Centered service and status values beneath their corresponding column headings.
|
||||||
|
- Removed archive filenames from visible password-cracking progress while retaining full technical details in the status tooltip.
|
||||||
|
- Kept active packages in their activation order and appended newly active packages behind downloads that were already running.
|
||||||
|
- Removed status-driven automatic package expansion so collapse state changes only through explicit user actions.
|
||||||
|
|
||||||
|
### Account handling
|
||||||
|
|
||||||
|
- Separated Mega-Debrid API and Web Login credential pools so adding, editing, disabling, or removing one mode no longer changes the other mode.
|
||||||
|
- Migrated legacy shared Mega-Debrid credentials and disabled-account states into the explicitly enabled access mode, falling back to the preferred mode only for ambiguous legacy configurations.
|
||||||
|
- Kept mode-specific account availability and provider selection synchronized with live settings changes.
|
||||||
|
- Applied added, edited, disabled, and re-enabled Mega-Debrid accounts to the active scheduler without requiring an application restart.
|
||||||
|
- Invalidated cached Mega-Debrid Web Login sessions immediately when Web credentials change, prevented older in-flight, retried, and queued requests from restoring invalidated cookies, and left API-only changes isolated.
|
||||||
|
- Released only Mega-Debrid reset-parked queue items when a usable account pool becomes available while preserving unrelated retry delays.
|
||||||
|
- Prevented new Mega-Debrid account forms from exposing stored credentials in an unrelated token field.
|
||||||
|
- Removed both mode-specific Mega-Debrid credential pools from persisted settings when credential storage is disabled.
|
||||||
|
- Synchronized item and package status immediately when a provider retry is queued.
|
||||||
|
|
||||||
|
### Reliability and testing
|
||||||
|
|
||||||
|
- Kept the latest release notes as a fallback when the release history cannot be loaded.
|
||||||
|
- Added regression coverage for cumulative version filtering, ordering, update colors, changelog scrolling, RapidGator aliases, centered queue cells, compact password progress, independent Mega-Debrid API and Web Login pools, credential persistence, legacy migration, live session invalidation, live account-pool refresh, stable active ordering, and user-controlled package expansion.
|
||||||
|
- Verified the update dialog with twelve version sections at a 1120 by 760 pixel viewport.
|
||||||
|
|
||||||
## [2.0.22] - 2026-08-11
|
## [2.0.22] - 2026-08-11
|
||||||
|
|
||||||
### History management
|
### History management
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "2.0.22",
|
"version": "2.0.23",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "2.0.22",
|
"version": "2.0.23",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "0.6.0",
|
"adm-zip": "0.6.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "2.0.22",
|
"version": "2.0.23",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@@ -46,9 +46,11 @@ export function defaultSettings(): AppSettings {
|
|||||||
language: "en",
|
language: "en",
|
||||||
token: "",
|
token: "",
|
||||||
realDebridUseWebLogin: false,
|
realDebridUseWebLogin: false,
|
||||||
megaLogin: "",
|
megaLogin: "",
|
||||||
megaPassword: "",
|
megaPassword: "",
|
||||||
megaCredentials: "",
|
megaCredentials: "",
|
||||||
|
megaDebridApiCredentials: "",
|
||||||
|
megaDebridWebCredentials: "",
|
||||||
megaDebridApiEnabled: false,
|
megaDebridApiEnabled: false,
|
||||||
megaDebridWebEnabled: false,
|
megaDebridWebEnabled: false,
|
||||||
megaDebridPreferApi: true,
|
megaDebridPreferApi: true,
|
||||||
@@ -133,7 +135,9 @@ export function defaultSettings(): AppSettings {
|
|||||||
debridLinkApiKeyDailyLimitBytes: {},
|
debridLinkApiKeyDailyLimitBytes: {},
|
||||||
debridLinkApiKeyDailyUsageBytes: {},
|
debridLinkApiKeyDailyUsageBytes: {},
|
||||||
debridLinkApiKeyTotalUsageBytes: {},
|
debridLinkApiKeyTotalUsageBytes: {},
|
||||||
megaDebridDisabledAccountIds: [],
|
megaDebridDisabledAccountIds: [],
|
||||||
|
megaDebridApiDisabledAccountIds: [],
|
||||||
|
megaDebridWebDisabledAccountIds: [],
|
||||||
megaDebridAccountDailyLimitBytes: {},
|
megaDebridAccountDailyLimitBytes: {},
|
||||||
megaDebridAccountDailyUsageBytes: {},
|
megaDebridAccountDailyUsageBytes: {},
|
||||||
megaDebridAccountTotalUsageBytes: {},
|
megaDebridAccountTotalUsageBytes: {},
|
||||||
|
|||||||
+41
-43
@@ -1,5 +1,6 @@
|
|||||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||||
import { parseMegaDebridAccounts, type MegaDebridAccountEntry } from "../shared/mega-debrid-accounts";
|
import { getMegaDebridAccountsForMode, mergeMegaDebridCredentialPools, parseMegaDebridAccounts, type MegaDebridAccountEntry, type MegaDebridAccountMode } from "../shared/mega-debrid-accounts";
|
||||||
|
import { extractHosterFromUrl } from "../shared/hoster";
|
||||||
import { AllDebridHostInfo, AppSettings, DebridFallbackProvider, DebridLinkHostLimitInfo, DebridProvider } from "../shared/types";
|
import { AllDebridHostInfo, AppSettings, DebridFallbackProvider, DebridLinkHostLimitInfo, DebridProvider } from "../shared/types";
|
||||||
import { isDebridLinkApiKeyDailyLimitReached, isMegaDebridAccountDisabled, isMegaDebridAccountDailyLimitReached, isProviderDailyLimitReached } from "../shared/provider-daily-limits";
|
import { isDebridLinkApiKeyDailyLimitReached, isMegaDebridAccountDisabled, isMegaDebridAccountDailyLimitReached, isProviderDailyLimitReached } from "../shared/provider-daily-limits";
|
||||||
import { isMegaDebridResolveFailure, germanMegaDebridResolveReason } from "../shared/mega-debrid-errors";
|
import { isMegaDebridResolveFailure, germanMegaDebridResolveReason } from "../shared/mega-debrid-errors";
|
||||||
@@ -557,17 +558,7 @@ const PROVIDER_LABELS: Record<DebridProvider, string> = {
|
|||||||
linksnappy: "LinkSnappy"
|
linksnappy: "LinkSnappy"
|
||||||
};
|
};
|
||||||
|
|
||||||
function extractHosterFromUrl(url: string): string {
|
interface ProviderUnrestrictedLink extends UnrestrictedLink {
|
||||||
try {
|
|
||||||
const host = new URL(url).hostname.replace(/^www\./, "").toLowerCase();
|
|
||||||
const parts = host.split(".");
|
|
||||||
return parts.length >= 2 ? parts[parts.length - 2] : host;
|
|
||||||
} catch {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProviderUnrestrictedLink extends UnrestrictedLink {
|
|
||||||
provider: DebridProvider;
|
provider: DebridProvider;
|
||||||
providerLabel: string;
|
providerLabel: string;
|
||||||
}
|
}
|
||||||
@@ -595,7 +586,9 @@ function cloneSettings(settings: AppSettings): AppSettings {
|
|||||||
debridLinkApiKeyDailyLimitBytes: { ...(settings.debridLinkApiKeyDailyLimitBytes || {}) },
|
debridLinkApiKeyDailyLimitBytes: { ...(settings.debridLinkApiKeyDailyLimitBytes || {}) },
|
||||||
debridLinkApiKeyDailyUsageBytes: { ...(settings.debridLinkApiKeyDailyUsageBytes || {}) },
|
debridLinkApiKeyDailyUsageBytes: { ...(settings.debridLinkApiKeyDailyUsageBytes || {}) },
|
||||||
debridLinkApiKeyTotalUsageBytes: { ...(settings.debridLinkApiKeyTotalUsageBytes || {}) },
|
debridLinkApiKeyTotalUsageBytes: { ...(settings.debridLinkApiKeyTotalUsageBytes || {}) },
|
||||||
megaDebridDisabledAccountIds: [...(settings.megaDebridDisabledAccountIds || [])],
|
megaDebridDisabledAccountIds: [...(settings.megaDebridDisabledAccountIds || [])],
|
||||||
|
megaDebridApiDisabledAccountIds: [...(settings.megaDebridApiDisabledAccountIds || [])],
|
||||||
|
megaDebridWebDisabledAccountIds: [...(settings.megaDebridWebDisabledAccountIds || [])],
|
||||||
megaDebridAccountDailyLimitBytes: { ...(settings.megaDebridAccountDailyLimitBytes || {}) },
|
megaDebridAccountDailyLimitBytes: { ...(settings.megaDebridAccountDailyLimitBytes || {}) },
|
||||||
megaDebridAccountDailyUsageBytes: { ...(settings.megaDebridAccountDailyUsageBytes || {}) },
|
megaDebridAccountDailyUsageBytes: { ...(settings.megaDebridAccountDailyUsageBytes || {}) },
|
||||||
megaDebridAccountTotalUsageBytes: { ...(settings.megaDebridAccountTotalUsageBytes || {}) }
|
megaDebridAccountTotalUsageBytes: { ...(settings.megaDebridAccountTotalUsageBytes || {}) }
|
||||||
@@ -612,26 +605,27 @@ export function getAvailableDebridLinkApiKeys(settings: AppSettings, epochMs = D
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAvailableMegaDebridAccounts(settings: AppSettings, epochMs = Date.now()): MegaDebridAccountEntry[] {
|
export function getAvailableMegaDebridAccounts(settings: AppSettings, mode: MegaDebridAccountMode, epochMs = Date.now()): MegaDebridAccountEntry[] {
|
||||||
return getMegaDebridAccountList(settings).filter(
|
return getMegaDebridAccountList(settings, mode).filter(
|
||||||
(entry) => !isMegaDebridAccountDisabled(settings, entry.id) && !isMegaDebridAccountDailyLimitReached(settings, entry.id, epochMs)
|
(entry) => !isMegaDebridAccountDisabled(settings, entry.id, mode) && !isMegaDebridAccountDailyLimitReached(settings, entry.id, epochMs)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMegaDebridAccountList(settings: AppSettings): MegaDebridAccountEntry[] {
|
function getMegaDebridAccountList(settings: AppSettings, mode: MegaDebridAccountMode): MegaDebridAccountEntry[] {
|
||||||
const multiAccounts = parseMegaDebridAccounts(settings.megaCredentials || "");
|
const multiAccounts = getMegaDebridAccountsForMode(settings, mode);
|
||||||
if (multiAccounts.length > 0) {
|
if (multiAccounts.length > 0) {
|
||||||
return multiAccounts;
|
return multiAccounts;
|
||||||
}
|
}
|
||||||
if (settings.megaLogin?.trim() && settings.megaPassword?.trim()) {
|
const hasDedicatedPools = typeof settings.megaDebridApiCredentials === "string" || typeof settings.megaDebridWebCredentials === "string";
|
||||||
return parseMegaDebridAccounts(settings.megaLogin.trim(), settings.megaPassword.trim());
|
if (!hasDedicatedPools && settings.megaLogin?.trim() && settings.megaPassword?.trim()) {
|
||||||
}
|
return parseMegaDebridAccounts(settings.megaLogin.trim(), settings.megaPassword.trim());
|
||||||
return [];
|
}
|
||||||
}
|
return [];
|
||||||
|
}
|
||||||
function hasMegaDebridCredentials(settings: AppSettings): boolean {
|
|
||||||
return getMegaDebridAccountList(settings).length > 0;
|
function hasMegaDebridCredentials(settings: AppSettings): boolean {
|
||||||
}
|
return parseMegaDebridAccounts(mergeMegaDebridCredentialPools(settings.megaDebridApiCredentials || "", settings.megaDebridWebCredentials || "") || settings.megaCredentials || "").length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
function isMegaDebridModeEnabled(settings: AppSettings, mode: "api" | "web"): boolean {
|
function isMegaDebridModeEnabled(settings: AppSettings, mode: "api" | "web"): boolean {
|
||||||
if (mode === "api") {
|
if (mode === "api") {
|
||||||
@@ -2039,12 +2033,12 @@ class MegaDebridClient {
|
|||||||
megaWebUnrestrict: MegaWebUnrestrictor | undefined,
|
megaWebUnrestrict: MegaWebUnrestrictor | undefined,
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
): Promise<UnrestrictedLink> {
|
): Promise<UnrestrictedLink> {
|
||||||
const accounts = getMegaDebridAccountList(settings);
|
const accounts = getMegaDebridAccountList(settings, mode);
|
||||||
if (accounts.length === 0) {
|
if (accounts.length === 0) {
|
||||||
throw new Error("Mega-Debrid: Kein Account konfiguriert");
|
throw new Error("Mega-Debrid: Kein Account konfiguriert");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getAvailableMegaDebridAccounts(settings).length === 0) {
|
if (getAvailableMegaDebridAccounts(settings, mode).length === 0) {
|
||||||
throw new Error("Mega-Debrid: Kein aktiver Account verfuegbar (deaktiviert oder am Tageslimit)");
|
throw new Error("Mega-Debrid: Kein aktiver Account verfuegbar (deaktiviert oder am Tageslimit)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2087,7 +2081,7 @@ class MegaDebridClient {
|
|||||||
const accountLabel = ` (${account.label}/${totalAccounts}, ${account.maskedLogin})`;
|
const accountLabel = ` (${account.label}/${totalAccounts}, ${account.maskedLogin})`;
|
||||||
const rotationLabel = `${account.label}/${totalAccounts} (${account.maskedLogin})`;
|
const rotationLabel = `${account.label}/${totalAccounts} (${account.maskedLogin})`;
|
||||||
|
|
||||||
if (isMegaDebridAccountDisabled(settings, account.id)) {
|
if (isMegaDebridAccountDisabled(settings, account.id, mode)) {
|
||||||
logger.info(`Mega-Debrid${accountLabel}: uebersprungen (manuell deaktiviert), pruefe naechsten Account`);
|
logger.info(`Mega-Debrid${accountLabel}: uebersprungen (manuell deaktiviert), pruefe naechsten Account`);
|
||||||
logAccountRotation("INFO", providerName, rotationLabel, "SKIP_DISABLED", { reason: "manually disabled" });
|
logAccountRotation("INFO", providerName, rotationLabel, "SKIP_DISABLED", { reason: "manually disabled" });
|
||||||
continue;
|
continue;
|
||||||
@@ -2172,7 +2166,7 @@ class MegaDebridClient {
|
|||||||
if (candidate.account.id === account.id) {
|
if (candidate.account.id === account.id) {
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
if (isMegaDebridAccountDisabled(settings, candidate.account.id)) {
|
if (isMegaDebridAccountDisabled(settings, candidate.account.id, mode)) {
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
if (isMegaDebridAccountDailyLimitReached(settings, candidate.account.id)) {
|
if (isMegaDebridAccountDailyLimitReached(settings, candidate.account.id)) {
|
||||||
@@ -2254,7 +2248,7 @@ class MegaDebridClient {
|
|||||||
let nextLabel = "ENDE";
|
let nextLabel = "ENDE";
|
||||||
for (let nextPos = orderPos + 1; nextPos < orderedEntries.length; nextPos += 1) {
|
for (let nextPos = orderPos + 1; nextPos < orderedEntries.length; nextPos += 1) {
|
||||||
const nextAcc = orderedEntries[nextPos].account;
|
const nextAcc = orderedEntries[nextPos].account;
|
||||||
if (!isMegaDebridAccountDisabled(settings, nextAcc.id) && !isMegaDebridAccountDailyLimitReached(settings, nextAcc.id) && !getMegaDebridAccountCooldownState(`${nextAcc.id}:${mode}`)) {
|
if (!isMegaDebridAccountDisabled(settings, nextAcc.id, mode) && !isMegaDebridAccountDailyLimitReached(settings, nextAcc.id) && !getMegaDebridAccountCooldownState(`${nextAcc.id}:${mode}`)) {
|
||||||
nextLabel = `${nextAcc.label}/${totalAccounts} (${nextAcc.maskedLogin})`;
|
nextLabel = `${nextAcc.label}/${totalAccounts} (${nextAcc.maskedLogin})`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -3824,9 +3818,10 @@ export class DebridService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (effectiveProvider === "megadebrid-api" || effectiveProvider === "megadebrid-web") {
|
if (effectiveProvider === "megadebrid-api" || effectiveProvider === "megadebrid-web") {
|
||||||
const configuredAccounts = getMegaDebridAccountList(settings);
|
const mode = effectiveProvider === "megadebrid-web" ? "web" : "api";
|
||||||
if (configuredAccounts.length > 0 && getAvailableMegaDebridAccounts(settings).length === 0) {
|
const configuredAccounts = getMegaDebridAccountList(settings, mode);
|
||||||
|
if (configuredAccounts.length > 0 && getAvailableMegaDebridAccounts(settings, mode).length === 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3842,8 +3837,11 @@ export class DebridService {
|
|||||||
if (effectiveProvider === "debridlink" && parseDebridLinkApiKeys(settings.debridLinkApiKeys).length > 0 && getAvailableDebridLinkApiKeys(settings).length === 0) {
|
if (effectiveProvider === "debridlink" && parseDebridLinkApiKeys(settings.debridLinkApiKeys).length > 0 && getAvailableDebridLinkApiKeys(settings).length === 0) {
|
||||||
return "Debrid-Link nicht verfuegbar (alle aktiven API-Keys deaktiviert oder ausgeschopft)";
|
return "Debrid-Link nicht verfuegbar (alle aktiven API-Keys deaktiviert oder ausgeschopft)";
|
||||||
}
|
}
|
||||||
if ((effectiveProvider === "megadebrid-api" || effectiveProvider === "megadebrid-web") && getMegaDebridAccountList(settings).length > 0 && getAvailableMegaDebridAccounts(settings).length === 0) {
|
if (effectiveProvider === "megadebrid-api" || effectiveProvider === "megadebrid-web") {
|
||||||
return "Mega-Debrid nicht verfuegbar (alle aktiven Accounts deaktiviert oder ausgeschopft)";
|
const mode = effectiveProvider === "megadebrid-web" ? "web" : "api";
|
||||||
|
if (getMegaDebridAccountList(settings, mode).length > 0 && getAvailableMegaDebridAccounts(settings, mode).length === 0) {
|
||||||
|
return "Mega-Debrid nicht verfuegbar (alle aktiven Accounts deaktiviert oder ausgeschopft)";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return `${PROVIDER_LABELS[effectiveProvider]} Tageslimit erreicht`;
|
return `${PROVIDER_LABELS[effectiveProvider]} Tageslimit erreicht`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ import {
|
|||||||
StartConflictEntry,
|
StartConflictEntry,
|
||||||
StartConflictResolutionResult,
|
StartConflictResolutionResult,
|
||||||
UiSnapshot, DebridAccountStatus } from "../shared/types";
|
UiSnapshot, DebridAccountStatus } from "../shared/types";
|
||||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||||
import { isMegaDebridTransientResolveFailure, germanMegaDebridResolveReason } from "../shared/mega-debrid-errors";
|
import { extractHosterFromUrl } from "../shared/hoster";
|
||||||
|
import { isMegaDebridTransientResolveFailure, germanMegaDebridResolveReason } from "../shared/mega-debrid-errors";
|
||||||
|
import { getMegaDebridAccountsForMode } from "../shared/mega-debrid-accounts";
|
||||||
import {
|
import {
|
||||||
addDebridLinkApiKeyDailyUsageBytes,
|
addDebridLinkApiKeyDailyUsageBytes,
|
||||||
addDebridLinkApiKeyTotalUsageBytes,
|
addDebridLinkApiKeyTotalUsageBytes,
|
||||||
@@ -470,15 +472,9 @@ function isArchiveLikePath(filePath: string): boolean {
|
|||||||
return /\.(?:part\d+\.rar|rar|r\d{2,3}|zip(?:\.\d+)?|z\d{1,3}|7z(?:\.\d+)?)$/i.test(lower);
|
return /\.(?:part\d+\.rar|rar|r\d{2,3}|zip(?:\.\d+)?|z\d{1,3}|7z(?:\.\d+)?)$/i.test(lower);
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractHosterKey(link: string): string {
|
function extractHosterKey(link: string): string {
|
||||||
try {
|
return extractHosterFromUrl(link);
|
||||||
const host = new URL(link).hostname.replace(/^www\./, "").toLowerCase();
|
}
|
||||||
const parts = host.split(".");
|
|
||||||
return parts.length >= 2 ? parts[parts.length - 2] : host;
|
|
||||||
} catch {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLargeBinaryLikePath(filePath: string): boolean {
|
function isLargeBinaryLikePath(filePath: string): boolean {
|
||||||
const lower = path.basename(String(filePath || "")).toLowerCase();
|
const lower = path.basename(String(filePath || "")).toLowerCase();
|
||||||
@@ -2175,7 +2171,26 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public setSettings(next: AppSettings, opts?: { suppressRetroactiveCleanup?: boolean; settingsOnlyImport?: boolean }): void {
|
public setSettings(next: AppSettings, opts?: { suppressRetroactiveCleanup?: boolean; settingsOnlyImport?: boolean }): void {
|
||||||
const previous = this.settings;
|
const previous = this.settings;
|
||||||
|
const previousMegaPool = (["api", "web"] as const)
|
||||||
|
.flatMap((mode) => getAvailableMegaDebridAccounts(previous, mode).map((account) => `${mode}:${account.id}:${account.password}`))
|
||||||
|
.sort()
|
||||||
|
.join("\n");
|
||||||
|
const nextMegaAccounts = (["api", "web"] as const)
|
||||||
|
.flatMap((mode) => getAvailableMegaDebridAccounts(next, mode));
|
||||||
|
const nextMegaPool = (["api", "web"] as const)
|
||||||
|
.flatMap((mode) => getAvailableMegaDebridAccounts(next, mode).map((account) => `${mode}:${account.id}:${account.password}`))
|
||||||
|
.sort()
|
||||||
|
.join("\n");
|
||||||
|
const previousMegaWebCredentials = getMegaDebridAccountsForMode(previous, "web")
|
||||||
|
.map((account) => `${account.id}:${account.password}`)
|
||||||
|
.sort()
|
||||||
|
.join("\n");
|
||||||
|
const nextMegaWebCredentials = getMegaDebridAccountsForMode(next, "web")
|
||||||
|
.map((account) => `${account.id}:${account.password}`)
|
||||||
|
.sort()
|
||||||
|
.join("\n");
|
||||||
|
const megaPoolChanged = previousMegaPool !== nextMegaPool && nextMegaAccounts.length > 0;
|
||||||
next.totalDownloadedAllTime = Math.max(next.totalDownloadedAllTime || 0, this.settings.totalDownloadedAllTime || 0);
|
next.totalDownloadedAllTime = Math.max(next.totalDownloadedAllTime || 0, this.settings.totalDownloadedAllTime || 0);
|
||||||
next.totalCompletedFilesAllTime = Math.max(next.totalCompletedFilesAllTime || 0, this.settings.totalCompletedFilesAllTime || 0);
|
next.totalCompletedFilesAllTime = Math.max(next.totalCompletedFilesAllTime || 0, this.settings.totalCompletedFilesAllTime || 0);
|
||||||
const now = nowMs();
|
const now = nowMs();
|
||||||
@@ -2184,8 +2199,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.invalidateSettingsSnapshotCache();
|
this.invalidateSettingsSnapshotCache();
|
||||||
this.runtimePersistedTotalMs = this.settings.totalRuntimeAllTimeMs || 0;
|
this.runtimePersistedTotalMs = this.settings.totalRuntimeAllTimeMs || 0;
|
||||||
this.runtimePersistedAt = now;
|
this.runtimePersistedAt = now;
|
||||||
this.ensureProviderDailyUsageFresh(nowMs());
|
this.ensureProviderDailyUsageFresh(nowMs());
|
||||||
this.debridService.setSettings(next);
|
if (previousMegaWebCredentials !== nextMegaWebCredentials) {
|
||||||
|
this.invalidateMegaSessionFn?.();
|
||||||
|
}
|
||||||
|
this.debridService.setSettings(next);
|
||||||
this.allDebridHostInfoCache.clear();
|
this.allDebridHostInfoCache.clear();
|
||||||
|
|
||||||
const prevOrder = JSON.stringify(previous.providerOrder ?? []);
|
const prevOrder = JSON.stringify(previous.providerOrder ?? []);
|
||||||
@@ -2218,7 +2236,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
{ prev: previous.debridLinkApiKeys || "", next: next.debridLinkApiKeys || "", providers: ["debridlink"] },
|
{ prev: previous.debridLinkApiKeys || "", next: next.debridLinkApiKeys || "", providers: ["debridlink"] },
|
||||||
{ prev: previous.linkSnappyLogin + "|" + previous.linkSnappyPassword, next: next.linkSnappyLogin + "|" + next.linkSnappyPassword, providers: ["linksnappy"] },
|
{ prev: previous.linkSnappyLogin + "|" + previous.linkSnappyPassword, next: next.linkSnappyLogin + "|" + next.linkSnappyPassword, providers: ["linksnappy"] },
|
||||||
{ prev: previous.ddownloadLogin + "|" + previous.ddownloadPassword, next: next.ddownloadLogin + "|" + next.ddownloadPassword, providers: ["ddownload"] },
|
{ prev: previous.ddownloadLogin + "|" + previous.ddownloadPassword, next: next.ddownloadLogin + "|" + next.ddownloadPassword, providers: ["ddownload"] },
|
||||||
{ prev: previous.megaCredentials + "|" + previous.megaPassword, next: next.megaCredentials + "|" + next.megaPassword, providers: ["megadebrid", "megadebrid-api", "megadebrid-web"] }
|
{
|
||||||
|
prev: `${previous.megaDebridApiCredentials}|${previous.megaDebridWebCredentials}|${previous.megaDebridApiEnabled}|${previous.megaDebridWebEnabled}`,
|
||||||
|
next: `${next.megaDebridApiCredentials}|${next.megaDebridWebCredentials}|${next.megaDebridApiEnabled}|${next.megaDebridWebEnabled}`,
|
||||||
|
providers: ["megadebrid", "megadebrid-api", "megadebrid-web"]
|
||||||
|
}
|
||||||
];
|
];
|
||||||
let clearedProviderFailures = 0;
|
let clearedProviderFailures = 0;
|
||||||
for (const change of credChanges) {
|
for (const change of credChanges) {
|
||||||
@@ -2232,9 +2254,13 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (clearedProviderFailures > 0) {
|
if (clearedProviderFailures > 0) {
|
||||||
logger.info(`Settings-Update: ${clearedProviderFailures} Provider-Failure(s) gecleart wegen geaenderter Credentials`);
|
logger.info(`Settings-Update: ${clearedProviderFailures} Provider-Failure(s) gecleart wegen geaenderter Credentials`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!opts?.settingsOnlyImport && megaPoolChanged) {
|
||||||
|
this.releaseMegaDebridResetParks();
|
||||||
|
}
|
||||||
|
|
||||||
if (!opts?.settingsOnlyImport) {
|
if (!opts?.settingsOnlyImport) {
|
||||||
this.resolveExistingQueuedOpaqueFilenames();
|
this.resolveExistingQueuedOpaqueFilenames();
|
||||||
@@ -2243,8 +2269,43 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.applyRetroactiveCleanupPolicy();
|
this.applyRetroactiveCleanupPolicy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.emitState();
|
this.emitState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private releaseMegaDebridResetParks(): number {
|
||||||
|
const activeItemIds = new Set([...this.activeTasks.values()].map((task) => task.itemId));
|
||||||
|
const affectedPackageIds = new Set<string>();
|
||||||
|
let released = 0;
|
||||||
|
for (const item of Object.values(this.session.items)) {
|
||||||
|
if (activeItemIds.has(item.id)) continue;
|
||||||
|
if (item.status !== "queued" && item.status !== "reconnect_wait") continue;
|
||||||
|
if (!/Mega-Debrid bis Tagesreset gesperrt/i.test(item.fullStatus || "")) continue;
|
||||||
|
this.retryAfterByItem.delete(item.id);
|
||||||
|
this.retryStateByItem.delete(item.id);
|
||||||
|
item.status = "queued";
|
||||||
|
item.fullStatus = "Wartet";
|
||||||
|
item.lastError = "";
|
||||||
|
item.provider = null;
|
||||||
|
item.providerLabel = undefined;
|
||||||
|
item.providerAccountId = undefined;
|
||||||
|
item.providerAccountLabel = undefined;
|
||||||
|
item.updatedAt = nowMs();
|
||||||
|
affectedPackageIds.add(item.packageId);
|
||||||
|
released += 1;
|
||||||
|
}
|
||||||
|
for (const packageId of affectedPackageIds) {
|
||||||
|
const pkg = this.session.packages[packageId];
|
||||||
|
if (pkg) this.refreshPackageStatus(pkg);
|
||||||
|
}
|
||||||
|
if (released > 0) {
|
||||||
|
logger.info(`Settings-Update: ${released} Mega-Debrid-Tagesreset-Warteitem(s) wegen geaendertem Account-Pool freigegeben`);
|
||||||
|
this.persistSoon();
|
||||||
|
if (this.session.running) {
|
||||||
|
void this.ensureScheduler().catch((error) => logger.error(`Scheduler nach Account-Update fehlgeschlagen: ${compactErrorText(error)}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return released;
|
||||||
|
}
|
||||||
|
|
||||||
public getSettings(): AppSettings {
|
public getSettings(): AppSettings {
|
||||||
return this.settings;
|
return this.settings;
|
||||||
@@ -8019,12 +8080,12 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (effectiveProvider === "realdebrid") {
|
if (effectiveProvider === "realdebrid") {
|
||||||
return Boolean(this.settings.realDebridUseWebLogin || this.settings.token.trim());
|
return Boolean(this.settings.realDebridUseWebLogin || this.settings.token.trim());
|
||||||
}
|
}
|
||||||
if (effectiveProvider === "megadebrid-api") {
|
if (effectiveProvider === "megadebrid-api") {
|
||||||
const hasMegaCreds = Boolean(this.settings.megaCredentials.trim() || (this.settings.megaLogin.trim() && this.settings.megaPassword.trim()));
|
const hasMegaCreds = getMegaDebridAccountsForMode(this.settings, "api").length > 0;
|
||||||
return Boolean(hasMegaCreds && (resolveMegaDebridProvider(this.settings, "megadebrid") === "megadebrid-api" || this.settings.megaDebridApiEnabled));
|
return Boolean(hasMegaCreds && (resolveMegaDebridProvider(this.settings, "megadebrid") === "megadebrid-api" || this.settings.megaDebridApiEnabled));
|
||||||
}
|
}
|
||||||
if (effectiveProvider === "megadebrid-web") {
|
if (effectiveProvider === "megadebrid-web") {
|
||||||
const hasMegaCreds = Boolean(this.settings.megaCredentials.trim() || (this.settings.megaLogin.trim() && this.settings.megaPassword.trim()));
|
const hasMegaCreds = getMegaDebridAccountsForMode(this.settings, "web").length > 0;
|
||||||
return Boolean(hasMegaCreds && (resolveMegaDebridProvider(this.settings, "megadebrid") === "megadebrid-web" || this.settings.megaDebridWebEnabled));
|
return Boolean(hasMegaCreds && (resolveMegaDebridProvider(this.settings, "megadebrid") === "megadebrid-web" || this.settings.megaDebridWebEnabled));
|
||||||
}
|
}
|
||||||
if (effectiveProvider === "bestdebrid") {
|
if (effectiveProvider === "bestdebrid") {
|
||||||
@@ -8209,7 +8270,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
private getSerializedValidatingLimit(provider: DebridProvider | null): number {
|
private getSerializedValidatingLimit(provider: DebridProvider | null): number {
|
||||||
if (provider === "megadebrid-web" || provider === "megadebrid-api") {
|
if (provider === "megadebrid-web" || provider === "megadebrid-api") {
|
||||||
const mode = provider === "megadebrid-web" ? "web" : "api";
|
const mode = provider === "megadebrid-web" ? "web" : "api";
|
||||||
const usableAccounts = getAvailableMegaDebridAccounts(this.settings)
|
const usableAccounts = getAvailableMegaDebridAccounts(this.settings, mode)
|
||||||
.filter((account) => !getMegaDebridAccountCooldownState(`${account.id}:${mode}`))
|
.filter((account) => !getMegaDebridAccountCooldownState(`${account.id}:${mode}`))
|
||||||
.length;
|
.length;
|
||||||
return Math.max(1, usableAccounts);
|
return Math.max(1, usableAccounts);
|
||||||
@@ -8793,9 +8854,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
genericRetries: Number(active.genericErrorRetries || 0),
|
genericRetries: Number(active.genericErrorRetries || 0),
|
||||||
freshRetryUsed: Boolean(active.freshRetryUsed),
|
freshRetryUsed: Boolean(active.freshRetryUsed),
|
||||||
resumeHardResetUsed: Boolean(active.resumeHardResetUsed)
|
resumeHardResetUsed: Boolean(active.resumeHardResetUsed)
|
||||||
});
|
});
|
||||||
this.retryAfterByItem.set(item.id, nowMs() + waitMs);
|
this.retryAfterByItem.set(item.id, nowMs() + waitMs);
|
||||||
}
|
const pkg = this.session.packages[item.packageId];
|
||||||
|
if (pkg) this.refreshPackageStatus(pkg);
|
||||||
|
}
|
||||||
|
|
||||||
private scheduleHttp416Retry(
|
private scheduleHttp416Retry(
|
||||||
item: DownloadItem,
|
item: DownloadItem,
|
||||||
|
|||||||
@@ -226,7 +226,9 @@ export class MegaWebFallback {
|
|||||||
|
|
||||||
private getCredentials: () => MegaCredentials;
|
private getCredentials: () => MegaCredentials;
|
||||||
|
|
||||||
private sessions = new Map<string, { cookie: string; setAt: number }>();
|
private sessions = new Map<string, { cookie: string; setAt: number }>();
|
||||||
|
|
||||||
|
private sessionGeneration = 0;
|
||||||
|
|
||||||
public constructor(getCredentials: () => MegaCredentials) {
|
public constructor(getCredentials: () => MegaCredentials) {
|
||||||
this.getCredentials = getCredentials;
|
this.getCredentials = getCredentials;
|
||||||
@@ -241,18 +243,19 @@ export class MegaWebFallback {
|
|||||||
const creds = (account && account.login.trim() && account.password.trim())
|
const creds = (account && account.login.trim() && account.password.trim())
|
||||||
? account
|
? account
|
||||||
: this.getCredentials();
|
: this.getCredentials();
|
||||||
if (!creds.login.trim() || !creds.password.trim()) {
|
if (!creds.login.trim() || !creds.password.trim()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const key = creds.login.trim().toLowerCase();
|
const key = creds.login.trim().toLowerCase();
|
||||||
return this.runExclusive(async () => {
|
const sessionGeneration = this.sessionGeneration;
|
||||||
throwIfAborted(overallSignal);
|
return this.runExclusive(async () => {
|
||||||
let cookie = await this.ensureSession(key, creds.login, creds.password, overallSignal);
|
throwIfAborted(overallSignal);
|
||||||
|
let cookie = await this.ensureSession(key, creds.login, creds.password, overallSignal, sessionGeneration);
|
||||||
let generated = await this.generate(link, cookie, overallSignal);
|
|
||||||
if (!generated) {
|
let generated = await this.generate(link, cookie, overallSignal);
|
||||||
this.sessions.delete(key);
|
if (!generated) {
|
||||||
cookie = await this.ensureSession(key, creds.login, creds.password, overallSignal);
|
this.sessions.delete(key);
|
||||||
|
cookie = await this.ensureSession(key, creds.login, creds.password, overallSignal, sessionGeneration);
|
||||||
generated = await this.generate(link, cookie, overallSignal);
|
generated = await this.generate(link, cookie, overallSignal);
|
||||||
if (!generated) {
|
if (!generated) {
|
||||||
return null;
|
return null;
|
||||||
@@ -267,19 +270,28 @@ export class MegaWebFallback {
|
|||||||
}, key, overallSignal);
|
}, key, overallSignal);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureSession(key: string, login: string, password: string, signal?: AbortSignal): Promise<string> {
|
private async ensureSession(
|
||||||
|
key: string,
|
||||||
|
login: string,
|
||||||
|
password: string,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
generation = this.sessionGeneration
|
||||||
|
): Promise<string> {
|
||||||
const existing = this.sessions.get(key);
|
const existing = this.sessions.get(key);
|
||||||
if (existing && existing.cookie && Date.now() - existing.setAt <= 20 * 60 * 1000) {
|
if (existing && existing.cookie && Date.now() - existing.setAt <= 20 * 60 * 1000) {
|
||||||
return existing.cookie;
|
return existing.cookie;
|
||||||
}
|
}
|
||||||
const cookie = await this.login(login, password, signal);
|
const cookie = await this.login(login, password, signal);
|
||||||
this.sessions.set(key, { cookie, setAt: Date.now() });
|
if (generation === this.sessionGeneration) {
|
||||||
return cookie;
|
this.sessions.set(key, { cookie, setAt: Date.now() });
|
||||||
}
|
}
|
||||||
|
return cookie;
|
||||||
public invalidateSession(): void {
|
}
|
||||||
this.sessions.clear();
|
|
||||||
}
|
public invalidateSession(): void {
|
||||||
|
this.sessionGeneration += 1;
|
||||||
|
this.sessions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
private async runExclusive<T>(job: () => Promise<T>, key: string, signal?: AbortSignal): Promise<T> {
|
private async runExclusive<T>(job: () => Promise<T>, key: string, signal?: AbortSignal): Promise<T> {
|
||||||
const queuedAt = Date.now();
|
const queuedAt = Date.now();
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
|
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
|
||||||
import { getMegaDebridAccountIds } from "../shared/mega-debrid-accounts";
|
import { getMegaDebridAccountIds, mergeMegaDebridCredentialPools } from "../shared/mega-debrid-accounts";
|
||||||
import type { AppSettings } from "../shared/types";
|
import type { AppSettings } from "../shared/types";
|
||||||
|
|
||||||
export function overlayLiveUsageCounters(target: AppSettings, liveSettings: AppSettings, liveTotalRuntimeMs: number): void {
|
export function overlayLiveUsageCounters(target: AppSettings, liveSettings: AppSettings, liveTotalRuntimeMs: number): void {
|
||||||
const debridLinkKeyIds = new Set(getDebridLinkApiKeyIds(target.debridLinkApiKeys));
|
const debridLinkKeyIds = new Set(getDebridLinkApiKeyIds(target.debridLinkApiKeys));
|
||||||
const megaAccountIds = new Set(getMegaDebridAccountIds(target.megaCredentials || "", target.megaPassword || ""));
|
const megaAccountIds = new Set(getMegaDebridAccountIds(mergeMegaDebridCredentialPools(target.megaDebridApiCredentials || "", target.megaDebridWebCredentials || "") || target.megaCredentials || "", target.megaPassword || ""));
|
||||||
const validAccountIds = new Set([...debridLinkKeyIds, ...megaAccountIds]);
|
const validAccountIds = new Set([...debridLinkKeyIds, ...megaAccountIds]);
|
||||||
target.totalDownloadedAllTime = Math.max(target.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0);
|
target.totalDownloadedAllTime = Math.max(target.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0);
|
||||||
target.totalCompletedFilesAllTime = Math.max(target.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0);
|
target.totalCompletedFilesAllTime = Math.max(target.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0);
|
||||||
target.totalRuntimeAllTimeMs = Math.max(target.totalRuntimeAllTimeMs || 0, liveTotalRuntimeMs);
|
target.totalRuntimeAllTimeMs = Math.max(target.totalRuntimeAllTimeMs || 0, liveTotalRuntimeMs);
|
||||||
target.providerDailyUsageDay = liveSettings.providerDailyUsageDay;
|
target.providerDailyUsageDay = liveSettings.providerDailyUsageDay;
|
||||||
target.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) };
|
target.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) };
|
||||||
target.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) };
|
target.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) };
|
||||||
target.debridLinkApiKeyDailyUsageBytes = Object.fromEntries(
|
target.debridLinkApiKeyDailyUsageBytes = Object.fromEntries(
|
||||||
Object.entries(liveSettings.debridLinkApiKeyDailyUsageBytes || {}).filter(([keyId]) => debridLinkKeyIds.has(keyId))
|
Object.entries(liveSettings.debridLinkApiKeyDailyUsageBytes || {}).filter(([keyId]) => debridLinkKeyIds.has(keyId))
|
||||||
);
|
);
|
||||||
target.debridLinkApiKeyTotalUsageBytes = Object.fromEntries(
|
target.debridLinkApiKeyTotalUsageBytes = Object.fromEntries(
|
||||||
Object.entries(liveSettings.debridLinkApiKeyTotalUsageBytes || {}).filter(([keyId]) => debridLinkKeyIds.has(keyId))
|
Object.entries(liveSettings.debridLinkApiKeyTotalUsageBytes || {}).filter(([keyId]) => debridLinkKeyIds.has(keyId))
|
||||||
);
|
);
|
||||||
target.megaDebridAccountDailyUsageBytes = Object.fromEntries(
|
target.megaDebridAccountDailyUsageBytes = Object.fromEntries(
|
||||||
Object.entries(liveSettings.megaDebridAccountDailyUsageBytes || {}).filter(([accountId]) => megaAccountIds.has(accountId))
|
Object.entries(liveSettings.megaDebridAccountDailyUsageBytes || {}).filter(([accountId]) => megaAccountIds.has(accountId))
|
||||||
);
|
);
|
||||||
target.megaDebridAccountTotalUsageBytes = Object.fromEntries(
|
target.megaDebridAccountTotalUsageBytes = Object.fromEntries(
|
||||||
Object.entries(liveSettings.megaDebridAccountTotalUsageBytes || {}).filter(([accountId]) => megaAccountIds.has(accountId))
|
Object.entries(liveSettings.megaDebridAccountTotalUsageBytes || {}).filter(([accountId]) => megaAccountIds.has(accountId))
|
||||||
);
|
);
|
||||||
target.debridAccountStatuses = Object.fromEntries(
|
target.debridAccountStatuses = Object.fromEntries(
|
||||||
Object.entries(liveSettings.debridAccountStatuses || {}).filter(([accountId]) => validAccountIds.has(accountId))
|
Object.entries(liveSettings.debridAccountStatuses || {}).filter(([accountId]) => validAccountIds.has(accountId))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-34
@@ -3,7 +3,7 @@ import fsp from "node:fs/promises";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
|
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
|
||||||
import { getMegaDebridAccountIds } from "../shared/mega-debrid-accounts";
|
import { getMegaDebridAccountIds, mergeMegaDebridCredentialPools, parseMegaDebridAccounts } from "../shared/mega-debrid-accounts";
|
||||||
import { AppSettings, AudioStripSummary, BandwidthScheduleEntry, DebridAccountStatus, DebridFallbackProvider, DebridProvider, DownloadItem, DownloadStatus, HistoryEntry, HistoryRetentionMode, PackageEntry, PackagePriority, SessionState } from "../shared/types";
|
import { AppSettings, AudioStripSummary, BandwidthScheduleEntry, DebridAccountStatus, DebridFallbackProvider, DebridProvider, DownloadItem, DownloadStatus, HistoryEntry, HistoryRetentionMode, PackageEntry, PackagePriority, SessionState } from "../shared/types";
|
||||||
import { getProviderUsageDayKey } from "../shared/provider-daily-limits";
|
import { getProviderUsageDayKey } from "../shared/provider-daily-limits";
|
||||||
import { defaultSettings } from "./constants";
|
import { defaultSettings } from "./constants";
|
||||||
@@ -381,21 +381,49 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
|||||||
const defaults = defaultSettings();
|
const defaults = defaultSettings();
|
||||||
const directorySettings = migrateLegacyDefaultDirectories(settings, defaults);
|
const directorySettings = migrateLegacyDefaultDirectories(settings, defaults);
|
||||||
const currentUsageDay = getProviderUsageDayKey();
|
const currentUsageDay = getProviderUsageDayKey();
|
||||||
const megaLogin = asText(settings.megaLogin);
|
const legacyMegaLogin = asText(settings.megaLogin);
|
||||||
const megaPassword = asText(settings.megaPassword);
|
const legacyMegaPassword = asText(settings.megaPassword);
|
||||||
let megaCredentials = String(settings.megaCredentials ?? "").replace(/\r\n|\r/g, "\n").trim();
|
let legacyMegaCredentials = String(settings.megaCredentials ?? "").replace(/\r\n|\r/g, "\n").trim();
|
||||||
if (!megaCredentials && megaLogin && megaPassword) {
|
if (!legacyMegaCredentials && legacyMegaLogin && legacyMegaPassword) {
|
||||||
megaCredentials = `${megaLogin}:${megaPassword}`;
|
legacyMegaCredentials = `${legacyMegaLogin}:${legacyMegaPassword}`;
|
||||||
}
|
}
|
||||||
const megaDebridAccountIds = getMegaDebridAccountIds(megaCredentials);
|
const megaDebridPreferApi = settings.megaDebridPreferApi !== undefined ? Boolean(settings.megaDebridPreferApi) : true;
|
||||||
const megaDebridPreferApi = settings.megaDebridPreferApi !== undefined ? Boolean(settings.megaDebridPreferApi) : true;
|
const hasLegacyMegaCredentials = getMegaDebridAccountIds(legacyMegaCredentials).length > 0;
|
||||||
const hasMegaCreds = Boolean(megaLogin && megaPassword);
|
const requestedMegaDebridApiEnabled = settings.megaDebridApiEnabled !== undefined
|
||||||
const megaDebridApiEnabled = settings.megaDebridApiEnabled !== undefined
|
? Boolean(settings.megaDebridApiEnabled)
|
||||||
? Boolean(settings.megaDebridApiEnabled)
|
: (hasLegacyMegaCredentials ? megaDebridPreferApi : defaults.megaDebridApiEnabled);
|
||||||
: (hasMegaCreds ? megaDebridPreferApi : defaults.megaDebridApiEnabled);
|
const requestedMegaDebridWebEnabled = settings.megaDebridWebEnabled !== undefined
|
||||||
const megaDebridWebEnabled = settings.megaDebridWebEnabled !== undefined
|
? Boolean(settings.megaDebridWebEnabled)
|
||||||
? Boolean(settings.megaDebridWebEnabled)
|
: (hasLegacyMegaCredentials ? !megaDebridPreferApi : defaults.megaDebridWebEnabled);
|
||||||
: (hasMegaCreds ? !megaDebridPreferApi : defaults.megaDebridWebEnabled);
|
const legacyMegaMode = requestedMegaDebridApiEnabled !== requestedMegaDebridWebEnabled
|
||||||
|
? requestedMegaDebridApiEnabled ? "api" : "web"
|
||||||
|
: megaDebridPreferApi ? "api" : "web";
|
||||||
|
const rawMegaDebridApiCredentials = String(settings.megaDebridApiCredentials || "").replace(/\r\n|\r/g, "\n").trim();
|
||||||
|
const rawMegaDebridWebCredentials = String(settings.megaDebridWebCredentials || "").replace(/\r\n|\r/g, "\n").trim();
|
||||||
|
const hasDedicatedMegaPools = Boolean(rawMegaDebridApiCredentials || rawMegaDebridWebCredentials) || !legacyMegaCredentials;
|
||||||
|
const megaDebridApiCredentials = hasDedicatedMegaPools
|
||||||
|
? rawMegaDebridApiCredentials
|
||||||
|
: legacyMegaMode === "api" ? legacyMegaCredentials : "";
|
||||||
|
const megaDebridWebCredentials = hasDedicatedMegaPools
|
||||||
|
? rawMegaDebridWebCredentials
|
||||||
|
: legacyMegaMode === "web" ? legacyMegaCredentials : "";
|
||||||
|
const megaDebridApiAccountIds = getMegaDebridAccountIds(megaDebridApiCredentials);
|
||||||
|
const megaDebridWebAccountIds = getMegaDebridAccountIds(megaDebridWebCredentials);
|
||||||
|
const megaCredentials = mergeMegaDebridCredentialPools(megaDebridApiCredentials, megaDebridWebCredentials);
|
||||||
|
const megaDebridAccountIds = getMegaDebridAccountIds(megaCredentials);
|
||||||
|
const primaryMegaAccount = parseMegaDebridAccounts(megaCredentials)[0];
|
||||||
|
const megaLogin = primaryMegaAccount?.login || "";
|
||||||
|
const megaPassword = primaryMegaAccount?.password || "";
|
||||||
|
const megaDebridApiEnabled = requestedMegaDebridApiEnabled && megaDebridApiAccountIds.length > 0;
|
||||||
|
const megaDebridWebEnabled = requestedMegaDebridWebEnabled && megaDebridWebAccountIds.length > 0;
|
||||||
|
const legacyMegaDisabledAccountIds = normalizeStringList(settings.megaDebridDisabledAccountIds, megaDebridAccountIds);
|
||||||
|
const megaDebridApiDisabledAccountIds = Array.isArray(settings.megaDebridApiDisabledAccountIds)
|
||||||
|
? normalizeStringList(settings.megaDebridApiDisabledAccountIds, megaDebridApiAccountIds)
|
||||||
|
: legacyMegaDisabledAccountIds.filter((id) => megaDebridApiAccountIds.includes(id));
|
||||||
|
const megaDebridWebDisabledAccountIds = Array.isArray(settings.megaDebridWebDisabledAccountIds)
|
||||||
|
? normalizeStringList(settings.megaDebridWebDisabledAccountIds, megaDebridWebAccountIds)
|
||||||
|
: legacyMegaDisabledAccountIds.filter((id) => megaDebridWebAccountIds.includes(id));
|
||||||
|
const megaDebridDisabledAccountIds = [...new Set([...megaDebridApiDisabledAccountIds, ...megaDebridWebDisabledAccountIds])];
|
||||||
const providerDailyUsageDayRaw = asText(settings.providerDailyUsageDay);
|
const providerDailyUsageDayRaw = asText(settings.providerDailyUsageDay);
|
||||||
const providerDailyUsageDay = /^\d{4}-\d{2}-\d{2}$/.test(providerDailyUsageDayRaw)
|
const providerDailyUsageDay = /^\d{4}-\d{2}-\d{2}$/.test(providerDailyUsageDayRaw)
|
||||||
? providerDailyUsageDayRaw
|
? providerDailyUsageDayRaw
|
||||||
@@ -429,8 +457,10 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
|||||||
token: asText(settings.token),
|
token: asText(settings.token),
|
||||||
realDebridUseWebLogin: Boolean(settings.realDebridUseWebLogin),
|
realDebridUseWebLogin: Boolean(settings.realDebridUseWebLogin),
|
||||||
megaLogin,
|
megaLogin,
|
||||||
megaPassword,
|
megaPassword,
|
||||||
megaCredentials,
|
megaCredentials,
|
||||||
|
megaDebridApiCredentials,
|
||||||
|
megaDebridWebCredentials,
|
||||||
megaDebridApiEnabled,
|
megaDebridApiEnabled,
|
||||||
megaDebridWebEnabled,
|
megaDebridWebEnabled,
|
||||||
megaDebridPreferApi,
|
megaDebridPreferApi,
|
||||||
@@ -527,7 +557,9 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
|||||||
debridLinkApiKeyDailyLimitBytes,
|
debridLinkApiKeyDailyLimitBytes,
|
||||||
debridLinkApiKeyDailyUsageBytes: providerDailyUsageDay === currentUsageDay ? debridLinkApiKeyDailyUsageBytes : {},
|
debridLinkApiKeyDailyUsageBytes: providerDailyUsageDay === currentUsageDay ? debridLinkApiKeyDailyUsageBytes : {},
|
||||||
debridLinkApiKeyTotalUsageBytes,
|
debridLinkApiKeyTotalUsageBytes,
|
||||||
megaDebridDisabledAccountIds: normalizeStringList(settings.megaDebridDisabledAccountIds, megaDebridAccountIds),
|
megaDebridDisabledAccountIds,
|
||||||
|
megaDebridApiDisabledAccountIds,
|
||||||
|
megaDebridWebDisabledAccountIds,
|
||||||
megaDebridAccountDailyLimitBytes: normalizeNamedByteMap(settings.megaDebridAccountDailyLimitBytes, megaDebridAccountIds),
|
megaDebridAccountDailyLimitBytes: normalizeNamedByteMap(settings.megaDebridAccountDailyLimitBytes, megaDebridAccountIds),
|
||||||
megaDebridAccountDailyUsageBytes: providerDailyUsageDay === currentUsageDay
|
megaDebridAccountDailyUsageBytes: providerDailyUsageDay === currentUsageDay
|
||||||
? normalizeNamedByteMap(settings.megaDebridAccountDailyUsageBytes, megaDebridAccountIds)
|
? normalizeNamedByteMap(settings.megaDebridAccountDailyUsageBytes, megaDebridAccountIds)
|
||||||
@@ -580,9 +612,11 @@ function sanitizeCredentialPersistence(settings: AppSettings): AppSettings {
|
|||||||
...settings,
|
...settings,
|
||||||
token: "",
|
token: "",
|
||||||
realDebridUseWebLogin: settings.realDebridUseWebLogin,
|
realDebridUseWebLogin: settings.realDebridUseWebLogin,
|
||||||
megaLogin: "",
|
megaLogin: "",
|
||||||
megaPassword: "",
|
megaPassword: "",
|
||||||
megaCredentials: "",
|
megaCredentials: "",
|
||||||
|
megaDebridApiCredentials: "",
|
||||||
|
megaDebridWebCredentials: "",
|
||||||
bestToken: "",
|
bestToken: "",
|
||||||
bestDebridUseWebLogin: settings.bestDebridUseWebLogin,
|
bestDebridUseWebLogin: settings.bestDebridUseWebLogin,
|
||||||
allDebridToken: "",
|
allDebridToken: "",
|
||||||
@@ -668,11 +702,14 @@ function normalizeAudioStripSummary(raw: unknown): AudioStripSummary | undefined
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateLegacyMegaEnableFlags(parsed: AppSettings): AppSettings {
|
function migrateLegacyMegaEnableFlags(parsed: AppSettings): AppSettings {
|
||||||
if (parsed.megaDebridApiEnabled !== undefined || parsed.megaDebridWebEnabled !== undefined) {
|
if (parsed.megaDebridApiEnabled !== undefined || parsed.megaDebridWebEnabled !== undefined) {
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
const hasMegaCreds = Boolean(asText(parsed.megaLogin) && asText(parsed.megaPassword));
|
const hasMegaCreds = Boolean(
|
||||||
|
getMegaDebridAccountIds(asText(parsed.megaCredentials), asText(parsed.megaPassword)).length
|
||||||
|
|| (asText(parsed.megaLogin) && asText(parsed.megaPassword))
|
||||||
|
);
|
||||||
if (!hasMegaCreds) {
|
if (!hasMegaCreds) {
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
@@ -680,16 +717,24 @@ function migrateLegacyMegaEnableFlags(parsed: AppSettings): AppSettings {
|
|||||||
return { ...parsed, megaDebridApiEnabled: preferApi, megaDebridWebEnabled: !preferApi };
|
return { ...parsed, megaDebridApiEnabled: preferApi, megaDebridWebEnabled: !preferApi };
|
||||||
}
|
}
|
||||||
|
|
||||||
function readSettingsFile(filePath: string): AppSettings | null {
|
function readSettingsFile(filePath: string): AppSettings | null {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as AppSettings;
|
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as AppSettings;
|
||||||
const migratedLanguage = (parsed as Partial<AppSettings>).language === undefined ? "de" : parsed.language;
|
const migratedLanguage = (parsed as Partial<AppSettings>).language === undefined ? "de" : parsed.language;
|
||||||
const merged = normalizeSettings({
|
const migrated = migrateLegacyMegaEnableFlags(parsed);
|
||||||
|
const mergedInput = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
...migrateLegacyMegaEnableFlags(parsed),
|
...migrated,
|
||||||
language: migratedLanguage,
|
language: migratedLanguage,
|
||||||
columnOrderVersion: parsed.columnOrderVersion
|
columnOrderVersion: parsed.columnOrderVersion
|
||||||
});
|
} as AppSettings;
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(parsed, "megaDebridApiDisabledAccountIds")) {
|
||||||
|
delete (mergedInput as Partial<AppSettings>).megaDebridApiDisabledAccountIds;
|
||||||
|
}
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(parsed, "megaDebridWebDisabledAccountIds")) {
|
||||||
|
delete (mergedInput as Partial<AppSettings>).megaDebridWebDisabledAccountIds;
|
||||||
|
}
|
||||||
|
const merged = normalizeSettings(mergedInput);
|
||||||
return sanitizeCredentialPersistence(merged);
|
return sanitizeCredentialPersistence(merged);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const code = (error as NodeJS.ErrnoException)?.code || "";
|
const code = (error as NodeJS.ErrnoException)?.code || "";
|
||||||
|
|||||||
+130
-32
@@ -126,26 +126,36 @@ function combineSignals(primary: AbortSignal, secondary?: AbortSignal): AbortSig
|
|||||||
return AbortSignal.any([primary, secondary]);
|
return AbortSignal.any([primary, secondary]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readJsonBody(response: Response, timeoutMs: number): Promise<Record<string, unknown> | null> {
|
async function readJsonValue(response: Response, timeoutMs: number): Promise<unknown> {
|
||||||
let timer: NodeJS.Timeout | null = null;
|
let timer: NodeJS.Timeout | null = null;
|
||||||
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
void response.body?.cancel().catch(() => undefined);
|
void response.body?.cancel().catch(() => undefined);
|
||||||
reject(new Error(`timeout:${timeoutMs}`));
|
reject(new Error(`timeout:${timeoutMs}`));
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await Promise.race([
|
return await Promise.race([
|
||||||
response.json().catch(() => null) as Promise<unknown>,
|
response.json().catch(() => null) as Promise<unknown>,
|
||||||
timeoutPromise,
|
timeoutPromise,
|
||||||
]);
|
]);
|
||||||
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
|
} finally {
|
||||||
return data as Record<string, unknown>;
|
if (timer) clearTimeout(timer);
|
||||||
} finally {
|
}
|
||||||
if (timer) clearTimeout(timer);
|
}
|
||||||
}
|
|
||||||
}
|
async function readJsonBody(response: Response, timeoutMs: number): Promise<Record<string, unknown> | null> {
|
||||||
|
const data = await readJsonValue(response, timeoutMs);
|
||||||
|
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
|
||||||
|
return data as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readJsonArray(response: Response, timeoutMs: number): Promise<Array<Record<string, unknown>> | null> {
|
||||||
|
const data = await readJsonValue(response, timeoutMs);
|
||||||
|
if (!Array.isArray(data)) return null;
|
||||||
|
return data.filter((entry): entry is Record<string, unknown> => Boolean(entry) && typeof entry === "object" && !Array.isArray(entry));
|
||||||
|
}
|
||||||
|
|
||||||
async function readTextBody(response: Response, timeoutMs: number): Promise<string> {
|
async function readTextBody(response: Response, timeoutMs: number): Promise<string> {
|
||||||
let timer: NodeJS.Timeout | null = null;
|
let timer: NodeJS.Timeout | null = null;
|
||||||
@@ -357,7 +367,7 @@ async function verifyDownloadedInstaller(filePath: string, digestRaw: string): P
|
|||||||
logger.info(`${expected.algorithm.toUpperCase()} Integrität bestätigt`);
|
logger.info(`${expected.algorithm.toUpperCase()} Integrität bestätigt`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchRelease(repo: string, endpoint: string): Promise<{
|
async function fetchRelease(repo: string, endpoint: string): Promise<{
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
status: number;
|
status: number;
|
||||||
payload: Record<string, unknown> | null;
|
payload: Record<string, unknown> | null;
|
||||||
@@ -372,8 +382,26 @@ async function fetchRelease(repo: string, endpoint: string): Promise<{
|
|||||||
return { ok: response.ok, status: response.status, payload };
|
return { ok: response.ok, status: response.status, payload };
|
||||||
} finally {
|
} finally {
|
||||||
tc.clear();
|
tc.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchReleasePage(repo: string, page: number): Promise<{
|
||||||
|
ok: boolean;
|
||||||
|
status: number;
|
||||||
|
payload: Array<Record<string, unknown>> | null;
|
||||||
|
}> {
|
||||||
|
const tc = timeoutController(RELEASE_FETCH_TIMEOUT_MS);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/repos/${repo}/releases?per_page=100&page=${page}`, {
|
||||||
|
headers: { Accept: "application/vnd.github+json", "User-Agent": USER_AGENT },
|
||||||
|
signal: tc.signal,
|
||||||
|
});
|
||||||
|
const payload = await readJsonArray(response, RELEASE_FETCH_TIMEOUT_MS);
|
||||||
|
return { ok: response.ok, status: response.status, payload };
|
||||||
|
} finally {
|
||||||
|
tc.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function readAssets(payload: Record<string, unknown>): ReleaseAsset[] {
|
function readAssets(payload: Record<string, unknown>): ReleaseAsset[] {
|
||||||
const raw = Array.isArray(payload.assets) ? (payload.assets as Array<Record<string, unknown>>) : [];
|
const raw = Array.isArray(payload.assets) ? (payload.assets as Array<Record<string, unknown>>) : [];
|
||||||
@@ -408,7 +436,7 @@ function isDraftOrPrerelease(payload: Record<string, unknown>): boolean {
|
|||||||
return Boolean(payload.draft) || Boolean(payload.prerelease);
|
return Boolean(payload.draft) || Boolean(payload.prerelease);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseReleasePayload(payload: Record<string, unknown>, fallbackUrl: string): UpdateCheckResult {
|
function parseReleasePayload(payload: Record<string, unknown>, fallbackUrl: string): UpdateCheckResult {
|
||||||
const latestTag = String(payload.tag_name || `v${APP_VERSION}`).trim();
|
const latestTag = String(payload.tag_name || `v${APP_VERSION}`).trim();
|
||||||
const latestVersion = latestTag.replace(/^v/i, "") || APP_VERSION;
|
const latestVersion = latestTag.replace(/^v/i, "") || APP_VERSION;
|
||||||
const releaseUrl = String(payload.html_url || fallbackUrl);
|
const releaseUrl = String(payload.html_url || fallbackUrl);
|
||||||
@@ -425,8 +453,73 @@ function parseReleasePayload(payload: Record<string, unknown>, fallbackUrl: stri
|
|||||||
setupAssetName: setup?.name || "",
|
setupAssetName: setup?.name || "",
|
||||||
setupAssetDigest: setup?.digest || "",
|
setupAssetDigest: setup?.digest || "",
|
||||||
releaseNotes: body || undefined,
|
releaseNotes: body || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function combineReleaseNotes(
|
||||||
|
payloads: Array<Record<string, unknown>>,
|
||||||
|
currentVersion: string,
|
||||||
|
latestVersion: string,
|
||||||
|
): string {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const releases = payloads
|
||||||
|
.filter((payload) => !isDraftOrPrerelease(payload))
|
||||||
|
.map((payload) => {
|
||||||
|
const tag = String(payload.tag_name || "").trim();
|
||||||
|
return {
|
||||||
|
tag,
|
||||||
|
version: tag.replace(/^v/i, ""),
|
||||||
|
body: typeof payload.body === "string" ? payload.body.trim() : "",
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((release) => (
|
||||||
|
release.tag
|
||||||
|
&& release.body
|
||||||
|
&& isRemoteNewer(currentVersion, release.version)
|
||||||
|
&& !isRemoteNewer(latestVersion, release.version)
|
||||||
|
))
|
||||||
|
.sort((first, second) => {
|
||||||
|
if (isRemoteNewer(first.version, second.version)) return 1;
|
||||||
|
if (isRemoteNewer(second.version, first.version)) return -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return releases
|
||||||
|
.filter((release) => {
|
||||||
|
const key = release.version.toLowerCase();
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((release) => `${release.tag}\n${release.body}`)
|
||||||
|
.join("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveMissingReleaseNotes(
|
||||||
|
repo: string,
|
||||||
|
latestPayload: Record<string, unknown>,
|
||||||
|
latestVersion: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const fallbackBody = typeof latestPayload.body === "string" ? latestPayload.body.trim() : "";
|
||||||
|
const payloads: Array<Record<string, unknown>> = [latestPayload];
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let page = 1; page <= 10; page += 1) {
|
||||||
|
const result = await fetchReleasePage(repo, page);
|
||||||
|
if (!result.ok || !result.payload) break;
|
||||||
|
payloads.push(...result.payload);
|
||||||
|
const reachedInstalledVersion = result.payload.some((payload) => {
|
||||||
|
const version = String(payload.tag_name || "").trim().replace(/^v/i, "");
|
||||||
|
return version && !isRemoteNewer(APP_VERSION, version);
|
||||||
|
});
|
||||||
|
if (result.payload.length < 100 || reachedInstalledVersion) break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return fallbackBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
return combineReleaseNotes(payloads, APP_VERSION, latestVersion) || fallbackBody;
|
||||||
|
}
|
||||||
|
|
||||||
function uniqueStrings(values: string[]): string[] {
|
function uniqueStrings(values: string[]): string[] {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
@@ -804,7 +897,7 @@ export function buildInstallerLaunchArgs(): string[] {
|
|||||||
return ["/S", "--updated", "--force-run"];
|
return ["/S", "--updated", "--force-run"];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkGitHubUpdate(repo: string): Promise<UpdateCheckResult> {
|
export async function checkGitHubUpdate(repo: string): Promise<UpdateCheckResult> {
|
||||||
const safeRepo = normalizeUpdateRepo(repo);
|
const safeRepo = normalizeUpdateRepo(repo);
|
||||||
const fallbackUrl = `${WEB_BASE}/${safeRepo}/releases/latest`;
|
const fallbackUrl = `${WEB_BASE}/${safeRepo}/releases/latest`;
|
||||||
const fallback: UpdateCheckResult = {
|
const fallback: UpdateCheckResult = {
|
||||||
@@ -817,15 +910,20 @@ export async function checkGitHubUpdate(repo: string): Promise<UpdateCheckResult
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { ok, status, payload } = await fetchRelease(safeRepo, "releases/latest");
|
const { ok, status, payload } = await fetchRelease(safeRepo, "releases/latest");
|
||||||
if (!ok || !payload) {
|
if (!ok || !payload) {
|
||||||
const reason = String((payload?.message as string) || `HTTP ${status}`);
|
const reason = String((payload?.message as string) || `HTTP ${status}`);
|
||||||
return { ...fallback, error: reason };
|
return { ...fallback, error: reason };
|
||||||
}
|
}
|
||||||
return parseReleasePayload(payload, fallbackUrl);
|
const result = parseReleasePayload(payload, fallbackUrl);
|
||||||
} catch (error) {
|
if (!result.updateAvailable) return result;
|
||||||
return { ...fallback, error: compactErrorText(error) };
|
return {
|
||||||
}
|
...result,
|
||||||
}
|
releaseNotes: await resolveMissingReleaseNotes(safeRepo, payload, result.latestVersion),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return { ...fallback, error: compactErrorText(error) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function installLatestUpdate(
|
export async function installLatestUpdate(
|
||||||
repo: string,
|
repo: string,
|
||||||
|
|||||||
+180
-172
@@ -1,7 +1,7 @@
|
|||||||
import { DragEvent, ReactElement, memo, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { DragEvent, ReactElement, memo, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import { flushSync } from "react-dom";
|
import { flushSync } from "react-dom";
|
||||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||||
import { getMegaDebridAccountId, parseMegaDebridAccounts, serializeMegaDebridAccounts, maskMegaDebridLogin } from "../shared/mega-debrid-accounts";
|
import { getMegaDebridAccountId, getMegaDebridAccountsForMode, getMegaDebridCredentialsForMode, getMegaDebridDisabledAccountIdsForMode, mergeMegaDebridCredentialPools, parseMegaDebridAccounts, serializeMegaDebridAccounts, type MegaDebridAccountMode } from "../shared/mega-debrid-accounts";
|
||||||
import type {
|
import type {
|
||||||
AllDebridHostInfo,
|
AllDebridHostInfo,
|
||||||
AppSettings,
|
AppSettings,
|
||||||
@@ -445,10 +445,9 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
|
|||||||
kind: "megadebrid-api",
|
kind: "megadebrid-api",
|
||||||
service: "megadebrid-api",
|
service: "megadebrid-api",
|
||||||
serviceLabel: "Mega-Debrid",
|
serviceLabel: "Mega-Debrid",
|
||||||
title: "Mega-Debrid API",
|
title: "Mega-Debrid API",
|
||||||
modeLabel: "API",
|
modeLabel: "API",
|
||||||
pickerDescription: "Login:Passwort-Paare für Mega-Debrid (API). Mehrere Accounts zeilenweise für Multi-Account.",
|
pickerDescription: "Login:Passwort-Paare für Mega-Debrid (API). Mehrere Accounts zeilenweise für Multi-Account."
|
||||||
needsToken: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "megadebrid-web",
|
kind: "megadebrid-web",
|
||||||
@@ -456,8 +455,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
|
|||||||
serviceLabel: "Mega-Debrid",
|
serviceLabel: "Mega-Debrid",
|
||||||
title: "Mega-Debrid Web-Login",
|
title: "Mega-Debrid Web-Login",
|
||||||
modeLabel: "Web-Login",
|
modeLabel: "Web-Login",
|
||||||
pickerDescription: "Login:Passwort-Paare für Mega-Debrid (Web). Mehrere Accounts zeilenweise für Multi-Account.",
|
pickerDescription: "Login:Passwort-Paare für Mega-Debrid (Web). Mehrere Accounts zeilenweise für Multi-Account."
|
||||||
needsToken: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "bestdebrid-api",
|
kind: "bestdebrid-api",
|
||||||
@@ -621,20 +619,22 @@ function getAccountCredentialLabel(kind: AccountKind): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasMegaDebridCredentials(settings: AppSettings): boolean {
|
function hasMegaDebridCredentials(settings: AppSettings, mode?: MegaDebridAccountMode): boolean {
|
||||||
if (parseMegaDebridAccounts(settings.megaCredentials || "").length > 0) return true;
|
if (mode) {
|
||||||
return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim());
|
return getMegaDebridAccountsForMode(settings, mode).length > 0;
|
||||||
}
|
}
|
||||||
|
return getMegaDebridAccountsForMode(settings, "api").length > 0 || getMegaDebridAccountsForMode(settings, "web").length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
function getConfiguredProvidersFromSettings(settings: AppSettings): DebridProvider[] {
|
function getConfiguredProvidersFromSettings(settings: AppSettings): DebridProvider[] {
|
||||||
const list: DebridProvider[] = [];
|
const list: DebridProvider[] = [];
|
||||||
if (settings.token.trim() || settings.realDebridUseWebLogin) {
|
if (settings.token.trim() || settings.realDebridUseWebLogin) {
|
||||||
list.push("realdebrid");
|
list.push("realdebrid");
|
||||||
}
|
}
|
||||||
if (hasMegaDebridCredentials(settings) && settings.megaDebridApiEnabled) {
|
if (hasMegaDebridCredentials(settings, "api") && settings.megaDebridApiEnabled) {
|
||||||
list.push("megadebrid-api");
|
list.push("megadebrid-api");
|
||||||
}
|
}
|
||||||
if (hasMegaDebridCredentials(settings) && settings.megaDebridWebEnabled) {
|
if (hasMegaDebridCredentials(settings, "web") && settings.megaDebridWebEnabled) {
|
||||||
list.push("megadebrid-web");
|
list.push("megadebrid-web");
|
||||||
}
|
}
|
||||||
if (settings.bestDebridUseWebLogin || settings.bestToken.trim()) {
|
if (settings.bestDebridUseWebLogin || settings.bestToken.trim()) {
|
||||||
@@ -682,9 +682,9 @@ function getConfiguredAccountKind(settings: AppSettings, service: AccountService
|
|||||||
if (settings.realDebridUseWebLogin) return "realdebrid-web";
|
if (settings.realDebridUseWebLogin) return "realdebrid-web";
|
||||||
return settings.token.trim() ? "realdebrid-api" : null;
|
return settings.token.trim() ? "realdebrid-api" : null;
|
||||||
case "megadebrid-api":
|
case "megadebrid-api":
|
||||||
return hasMegaDebridCredentials(settings) && settings.megaDebridApiEnabled ? "megadebrid-api" : null;
|
return hasMegaDebridCredentials(settings, "api") && settings.megaDebridApiEnabled ? "megadebrid-api" : null;
|
||||||
case "megadebrid-web":
|
case "megadebrid-web":
|
||||||
return hasMegaDebridCredentials(settings) && settings.megaDebridWebEnabled ? "megadebrid-web" : null;
|
return hasMegaDebridCredentials(settings, "web") && settings.megaDebridWebEnabled ? "megadebrid-web" : null;
|
||||||
case "bestdebrid":
|
case "bestdebrid":
|
||||||
if (settings.bestDebridUseWebLogin) return "bestdebrid-web";
|
if (settings.bestDebridUseWebLogin) return "bestdebrid-web";
|
||||||
return settings.bestToken.trim() ? "bestdebrid-api" : null;
|
return settings.bestToken.trim() ? "bestdebrid-api" : null;
|
||||||
@@ -719,9 +719,10 @@ function summarizeAccount(kind: AccountKind, settings: AppSettings): string {
|
|||||||
return maskValue(settings.token, 3, 3);
|
return maskValue(settings.token, 3, 3);
|
||||||
case "realdebrid-web":
|
case "realdebrid-web":
|
||||||
return "Browser-Login";
|
return "Browser-Login";
|
||||||
case "megadebrid-api":
|
case "megadebrid-api":
|
||||||
case "megadebrid-web": {
|
case "megadebrid-web": {
|
||||||
const megaAccounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword);
|
const mode = kind === "megadebrid-web" ? "web" : "api";
|
||||||
|
const megaAccounts = getMegaDebridAccountsForMode(settings, mode);
|
||||||
if (megaAccounts.length > 1) return `${megaAccounts.length} Accounts`;
|
if (megaAccounts.length > 1) return `${megaAccounts.length} Accounts`;
|
||||||
if (megaAccounts.length === 1) return megaAccounts[0].maskedLogin;
|
if (megaAccounts.length === 1) return megaAccounts[0].maskedLogin;
|
||||||
return settings.megaLogin.trim() ? maskValue(settings.megaLogin.trim(), 2, 6) : "Nicht hinterlegt";
|
return settings.megaLogin.trim() ? maskValue(settings.megaLogin.trim(), 2, 6) : "Nicht hinterlegt";
|
||||||
@@ -757,8 +758,8 @@ function summarizeAccountLines(kind: AccountKind, settings: AppSettings): string
|
|||||||
return keys.map((entry) => `${entry.label}: ${entry.masked}`);
|
return keys.map((entry) => `${entry.label}: ${entry.masked}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (kind === "megadebrid-api" || kind === "megadebrid-web") {
|
if (kind === "megadebrid-api" || kind === "megadebrid-web") {
|
||||||
const accounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword);
|
const accounts = getMegaDebridAccountsForMode(settings, kind === "megadebrid-web" ? "web" : "api");
|
||||||
if (accounts.length > 1) {
|
if (accounts.length > 1) {
|
||||||
return accounts.map((entry) => `${entry.label}: ${entry.maskedLogin}`);
|
return accounts.map((entry) => `${entry.label}: ${entry.maskedLogin}`);
|
||||||
}
|
}
|
||||||
@@ -782,7 +783,7 @@ function getStoredAccountUsername(kind: AccountKind, settings: AppSettings): str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | null, settings: AppSettings): AccountDialogState {
|
export function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | null, settings: AppSettings): AccountDialogState {
|
||||||
const baseMega: Pick<AccountDialogState, "megaAccounts" | "megaNewLogin" | "megaNewPassword" | "megaDisabledIds"> = { megaAccounts: [], megaNewLogin: "", megaNewPassword: "", megaDisabledIds: [] };
|
const baseMega: Pick<AccountDialogState, "megaAccounts" | "megaNewLogin" | "megaNewPassword" | "megaDisabledIds"> = { megaAccounts: [], megaNewLogin: "", megaNewPassword: "", megaDisabledIds: [] };
|
||||||
if (!kind) {
|
if (!kind) {
|
||||||
return {
|
return {
|
||||||
@@ -805,17 +806,14 @@ function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | n
|
|||||||
return { mode, kind, service, token: settings.token, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega };
|
return { mode, kind, service, token: settings.token, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega };
|
||||||
case "realdebrid-web":
|
case "realdebrid-web":
|
||||||
return { mode, kind, service, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega };
|
return { mode, kind, service, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega };
|
||||||
case "megadebrid-api":
|
case "megadebrid-api":
|
||||||
case "megadebrid-web": {
|
case "megadebrid-web": {
|
||||||
let megaToken = (settings.megaCredentials || "").trim();
|
const megaMode = kind === "megadebrid-web" ? "web" : "api";
|
||||||
if (!megaToken && settings.megaLogin.trim() && settings.megaPassword.trim()) {
|
const parsed = getMegaDebridAccountsForMode(settings, megaMode);
|
||||||
megaToken = `${settings.megaLogin.trim()}:${settings.megaPassword.trim()}`;
|
const megaAccounts = parsed.map((a) => ({ login: a.login, password: a.password }));
|
||||||
}
|
const loadedIds = new Set(parsed.map((a) => a.id));
|
||||||
const parsed = parseMegaDebridAccounts(megaToken);
|
const megaDisabledIds = getMegaDebridDisabledAccountIdsForMode(settings, megaMode).filter((id) => loadedIds.has(id));
|
||||||
const megaAccounts = parsed.map((a) => ({ login: a.login, password: a.password }));
|
return { mode, kind, service, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, megaAccounts, megaNewLogin: "", megaNewPassword: "", megaDisabledIds };
|
||||||
const loadedIds = new Set(parsed.map((a) => a.id));
|
|
||||||
const megaDisabledIds = (settings.megaDebridDisabledAccountIds || []).filter((id) => loadedIds.has(id));
|
|
||||||
return { mode, kind, service, token: megaToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, megaAccounts, megaNewLogin: "", megaNewPassword: "", megaDisabledIds };
|
|
||||||
}
|
}
|
||||||
case "bestdebrid-api":
|
case "bestdebrid-api":
|
||||||
return { mode, kind, service, token: settings.bestToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega };
|
return { mode, kind, service, token: settings.bestToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega };
|
||||||
@@ -846,9 +844,86 @@ function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | n
|
|||||||
default:
|
default:
|
||||||
return { mode, kind, service, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega };
|
return { mode, kind, service, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyAccountDialogToSettings(settings: AppSettings, dialog: AccountDialogState): AppSettings {
|
export function buildAccountAddFields(dialog: AccountDialogState | null): AccountDialogField[] {
|
||||||
|
if (!dialog?.kind) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const option = findAccountOption(dialog.kind);
|
||||||
|
return [
|
||||||
|
...((dialog.kind === "megadebrid-api" || dialog.kind === "megadebrid-web") ? [
|
||||||
|
{ id: "megaNewLogin", label: "Login / E-Mail", type: "text" as const, value: dialog.megaNewLogin },
|
||||||
|
{ id: "megaNewPassword", label: "Passwort", type: "password" as const, value: dialog.megaNewPassword }
|
||||||
|
] : option.needsCredentials ? [
|
||||||
|
{ id: "login", label: "Login / E-Mail", type: "text" as const, value: dialog.login },
|
||||||
|
{ id: "password", label: "Passwort", type: "password" as const, value: dialog.password }
|
||||||
|
] : []),
|
||||||
|
...(option.needsToken ? [
|
||||||
|
{ id: "token", label: dialog.kind === "debridlink-api" ? "API-Key" : "Token / API-Key", type: "password" as const, value: dialog.token }
|
||||||
|
] : []),
|
||||||
|
{
|
||||||
|
id: "dailyLimitGb",
|
||||||
|
label: "Tageslimit (GB, optional)",
|
||||||
|
type: "number" as const,
|
||||||
|
value: dialog.dailyLimitGb,
|
||||||
|
placeholder: "Kein Limit",
|
||||||
|
help: "Der Zähler wird täglich um 00:00 Uhr zurückgesetzt."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMegaDebridDialogToSettings(settings: AppSettings, dialog: AccountDialogState, mode: MegaDebridAccountMode, providerDailyLimitBytes: AppSettings["providerDailyLimitBytes"]): AppSettings {
|
||||||
|
const selectedCredentials = serializeMegaDebridAccounts(dialog.megaAccounts);
|
||||||
|
const apiCredentials = mode === "api" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "api");
|
||||||
|
const webCredentials = mode === "web" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "web");
|
||||||
|
const mergedCredentials = mergeMegaDebridCredentialPools(apiCredentials, webCredentials);
|
||||||
|
const first = parseMegaDebridAccounts(mergedCredentials)[0];
|
||||||
|
const selectedIds = new Set(parseMegaDebridAccounts(selectedCredentials).map((account) => account.id));
|
||||||
|
const selectedDisabledIds = (dialog.megaDisabledIds || []).filter((id) => selectedIds.has(id));
|
||||||
|
const apiDisabledIds = mode === "api" ? selectedDisabledIds : getMegaDebridDisabledAccountIdsForMode(settings, "api");
|
||||||
|
const webDisabledIds = mode === "web" ? selectedDisabledIds : getMegaDebridDisabledAccountIdsForMode(settings, "web");
|
||||||
|
return {
|
||||||
|
...settings,
|
||||||
|
megaCredentials: mergedCredentials,
|
||||||
|
megaLogin: first?.login || "",
|
||||||
|
megaPassword: first?.password || "",
|
||||||
|
megaDebridApiCredentials: apiCredentials,
|
||||||
|
megaDebridWebCredentials: webCredentials,
|
||||||
|
megaDebridApiEnabled: mode === "api" ? selectedIds.size > 0 : settings.megaDebridApiEnabled && parseMegaDebridAccounts(apiCredentials).length > 0,
|
||||||
|
megaDebridWebEnabled: mode === "web" ? selectedIds.size > 0 : settings.megaDebridWebEnabled && parseMegaDebridAccounts(webCredentials).length > 0,
|
||||||
|
megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])],
|
||||||
|
megaDebridApiDisabledAccountIds: apiDisabledIds,
|
||||||
|
megaDebridWebDisabledAccountIds: webDisabledIds,
|
||||||
|
providerDailyLimitBytes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMegaDebridModeFromSettings(settings: AppSettings, mode: MegaDebridAccountMode, providerDailyLimitBytes: AppSettings["providerDailyLimitBytes"], providerDailyUsageBytes: AppSettings["providerDailyUsageBytes"]): AppSettings {
|
||||||
|
const apiCredentials = mode === "api" ? "" : getMegaDebridCredentialsForMode(settings, "api");
|
||||||
|
const webCredentials = mode === "web" ? "" : getMegaDebridCredentialsForMode(settings, "web");
|
||||||
|
const mergedCredentials = mergeMegaDebridCredentialPools(apiCredentials, webCredentials);
|
||||||
|
const first = parseMegaDebridAccounts(mergedCredentials)[0];
|
||||||
|
const apiDisabledIds = mode === "api" ? [] : getMegaDebridDisabledAccountIdsForMode(settings, "api");
|
||||||
|
const webDisabledIds = mode === "web" ? [] : getMegaDebridDisabledAccountIdsForMode(settings, "web");
|
||||||
|
return {
|
||||||
|
...settings,
|
||||||
|
megaCredentials: mergedCredentials,
|
||||||
|
megaLogin: first?.login || "",
|
||||||
|
megaPassword: first?.password || "",
|
||||||
|
megaDebridApiCredentials: apiCredentials,
|
||||||
|
megaDebridWebCredentials: webCredentials,
|
||||||
|
megaDebridApiEnabled: mode === "api" ? false : settings.megaDebridApiEnabled && parseMegaDebridAccounts(apiCredentials).length > 0,
|
||||||
|
megaDebridWebEnabled: mode === "web" ? false : settings.megaDebridWebEnabled && parseMegaDebridAccounts(webCredentials).length > 0,
|
||||||
|
megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])],
|
||||||
|
megaDebridApiDisabledAccountIds: apiDisabledIds,
|
||||||
|
megaDebridWebDisabledAccountIds: webDisabledIds,
|
||||||
|
providerDailyLimitBytes,
|
||||||
|
providerDailyUsageBytes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyAccountDialogToSettings(settings: AppSettings, dialog: AccountDialogState): AppSettings {
|
||||||
if (!dialog.kind) {
|
if (!dialog.kind) {
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
@@ -876,23 +951,11 @@ export function applyAccountDialogToSettings(settings: AppSettings, dialog: Acco
|
|||||||
return { ...settings, token, realDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes };
|
return { ...settings, token, realDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes };
|
||||||
case "realdebrid-web":
|
case "realdebrid-web":
|
||||||
return { ...settings, token: "", realDebridUseWebLogin: true, providerDailyLimitBytes: nextProviderDailyLimitBytes };
|
return { ...settings, token: "", realDebridUseWebLogin: true, providerDailyLimitBytes: nextProviderDailyLimitBytes };
|
||||||
case "megadebrid-api": {
|
case "megadebrid-api": {
|
||||||
const megaSerialized = serializeMegaDebridAccounts(dialog.megaAccounts);
|
return applyMegaDebridDialogToSettings(settings, dialog, "api", nextProviderDailyLimitBytes);
|
||||||
const megaParsed = parseMegaDebridAccounts(megaSerialized);
|
}
|
||||||
const firstLogin = megaParsed.length > 0 ? megaParsed[0].login : "";
|
case "megadebrid-web": {
|
||||||
const firstPassword = megaParsed.length > 0 ? megaParsed[0].password : "";
|
return applyMegaDebridDialogToSettings(settings, dialog, "web", nextProviderDailyLimitBytes);
|
||||||
const validIds = new Set(megaParsed.map((a) => a.id));
|
|
||||||
const megaDebridDisabledAccountIds = (dialog.megaDisabledIds || []).filter((id) => validIds.has(id));
|
|
||||||
return { ...settings, megaCredentials: megaSerialized, megaLogin: firstLogin, megaPassword: firstPassword, megaDebridApiEnabled: true, megaDebridDisabledAccountIds, providerDailyLimitBytes: nextProviderDailyLimitBytes };
|
|
||||||
}
|
|
||||||
case "megadebrid-web": {
|
|
||||||
const megaSerialized = serializeMegaDebridAccounts(dialog.megaAccounts);
|
|
||||||
const megaParsed = parseMegaDebridAccounts(megaSerialized);
|
|
||||||
const firstLogin = megaParsed.length > 0 ? megaParsed[0].login : "";
|
|
||||||
const firstPassword = megaParsed.length > 0 ? megaParsed[0].password : "";
|
|
||||||
const validIds = new Set(megaParsed.map((a) => a.id));
|
|
||||||
const megaDebridDisabledAccountIds = (dialog.megaDisabledIds || []).filter((id) => validIds.has(id));
|
|
||||||
return { ...settings, megaCredentials: megaSerialized, megaLogin: firstLogin, megaPassword: firstPassword, megaDebridWebEnabled: true, megaDebridDisabledAccountIds, providerDailyLimitBytes: nextProviderDailyLimitBytes };
|
|
||||||
}
|
}
|
||||||
case "bestdebrid-api":
|
case "bestdebrid-api":
|
||||||
return { ...settings, bestToken: token, bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes };
|
return { ...settings, bestToken: token, bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes };
|
||||||
@@ -937,14 +1000,10 @@ function clearAccountServiceFromSettings(settings: AppSettings, service: Account
|
|||||||
switch (service) {
|
switch (service) {
|
||||||
case "realdebrid":
|
case "realdebrid":
|
||||||
return { ...settings, token: "", realDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes };
|
return { ...settings, token: "", realDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes };
|
||||||
case "megadebrid-api":
|
case "megadebrid-api":
|
||||||
return settings.megaDebridWebEnabled
|
return clearMegaDebridModeFromSettings(settings, "api", nextProviderDailyLimitBytes, nextProviderDailyUsageBytes);
|
||||||
? { ...settings, megaDebridApiEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes }
|
case "megadebrid-web":
|
||||||
: { ...settings, megaCredentials: "", megaLogin: "", megaPassword: "", megaDebridApiEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes };
|
return clearMegaDebridModeFromSettings(settings, "web", nextProviderDailyLimitBytes, nextProviderDailyUsageBytes);
|
||||||
case "megadebrid-web":
|
|
||||||
return settings.megaDebridApiEnabled
|
|
||||||
? { ...settings, megaDebridWebEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes }
|
|
||||||
: { ...settings, megaCredentials: "", megaLogin: "", megaPassword: "", megaDebridWebEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes };
|
|
||||||
case "bestdebrid":
|
case "bestdebrid":
|
||||||
return { ...settings, bestToken: "", bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes };
|
return { ...settings, bestToken: "", bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes };
|
||||||
case "alldebrid":
|
case "alldebrid":
|
||||||
@@ -1029,7 +1088,7 @@ const emptyStats = (): DownloadStats => ({
|
|||||||
|
|
||||||
const emptySnapshot = (): UiSnapshot => ({
|
const emptySnapshot = (): UiSnapshot => ({
|
||||||
settings: {
|
settings: {
|
||||||
language: "en", token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaCredentials: "", megaDebridApiEnabled: false, megaDebridWebEnabled: false, megaDebridPreferApi: true, bestToken: "", bestDebridUseWebLogin: false, allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "", debridLinkApiKeys: "", linkSnappyLogin: "", linkSnappyPassword: "",
|
language: "en", token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaCredentials: "", megaDebridApiCredentials: "", megaDebridWebCredentials: "", megaDebridApiEnabled: false, megaDebridWebEnabled: false, megaDebridPreferApi: true, bestToken: "", bestDebridUseWebLogin: false, allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "", debridLinkApiKeys: "", linkSnappyLogin: "", linkSnappyPassword: "",
|
||||||
debridLinkDisabledKeyIds: [],
|
debridLinkDisabledKeyIds: [],
|
||||||
archivePasswordList: "",
|
archivePasswordList: "",
|
||||||
rememberToken: true, providerOrder: [], providerPrimary: "realdebrid", providerSecondary: "none",
|
rememberToken: true, providerOrder: [], providerPrimary: "realdebrid", providerSecondary: "none",
|
||||||
@@ -1056,7 +1115,9 @@ const emptySnapshot = (): UiSnapshot => ({
|
|||||||
debridLinkApiKeyDailyLimitBytes: {},
|
debridLinkApiKeyDailyLimitBytes: {},
|
||||||
debridLinkApiKeyDailyUsageBytes: {},
|
debridLinkApiKeyDailyUsageBytes: {},
|
||||||
debridLinkApiKeyTotalUsageBytes: {},
|
debridLinkApiKeyTotalUsageBytes: {},
|
||||||
megaDebridDisabledAccountIds: [],
|
megaDebridDisabledAccountIds: [],
|
||||||
|
megaDebridApiDisabledAccountIds: [],
|
||||||
|
megaDebridWebDisabledAccountIds: [],
|
||||||
megaDebridAccountDailyLimitBytes: {},
|
megaDebridAccountDailyLimitBytes: {},
|
||||||
megaDebridAccountDailyUsageBytes: {},
|
megaDebridAccountDailyUsageBytes: {},
|
||||||
megaDebridAccountTotalUsageBytes: {},
|
megaDebridAccountTotalUsageBytes: {},
|
||||||
@@ -1753,8 +1814,6 @@ export function App(): ReactElement {
|
|||||||
const snapshotRef = useRef(snapshot);
|
const snapshotRef = useRef(snapshot);
|
||||||
snapshotRef.current = snapshot;
|
snapshotRef.current = snapshot;
|
||||||
const tabRef = useRef(tab);
|
const tabRef = useRef(tab);
|
||||||
const autoExpandedPkgsRef = useRef(new Set<string>());
|
|
||||||
const manualCollapsedPkgsRef = useRef(new Set<string>());
|
|
||||||
tabRef.current = tab;
|
tabRef.current = tab;
|
||||||
const stateFlushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const stateFlushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
@@ -2270,12 +2329,7 @@ export function App(): ReactElement {
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const packageId of Array.from(manualCollapsedPkgsRef.current)) {
|
return changed ? next : prev;
|
||||||
if (!snapshot.session.packages[packageId]) {
|
|
||||||
manualCollapsedPkgsRef.current.delete(packageId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return changed ? next : prev;
|
|
||||||
});
|
});
|
||||||
}, [downloadsTabActive, packageOrderKey, snapshot.session.packageOrder, snapshot.session.packages, totalPackageCount]);
|
}, [downloadsTabActive, packageOrderKey, snapshot.session.packageOrder, snapshot.session.packages, totalPackageCount]);
|
||||||
|
|
||||||
@@ -2287,18 +2341,18 @@ export function App(): ReactElement {
|
|||||||
}, [snapshot.session.packages, snapshot.session.items]);
|
}, [snapshot.session.packages, snapshot.session.items]);
|
||||||
|
|
||||||
const sortRelevantItems = (snapshot.session.running && settingsDraft.autoSortPackagesByProgress && packages.length > 1)
|
const sortRelevantItems = (snapshot.session.running && settingsDraft.autoSortPackagesByProgress && packages.length > 1)
|
||||||
? snapshot.session.items
|
? snapshot.session.items
|
||||||
: null;
|
: null;
|
||||||
const visiblePackages = useMemo(() => {
|
const visiblePackages = useMemo(() => {
|
||||||
if (!sortRelevantItems) {
|
if (!sortRelevantItems) {
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
return sortPackagesForDisplay(
|
return sortPackagesForDisplay(
|
||||||
packages,
|
packages,
|
||||||
sortRelevantItems,
|
sortRelevantItems,
|
||||||
true,
|
true,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
}, [packages, sortRelevantItems]);
|
}, [packages, sortRelevantItems]);
|
||||||
|
|
||||||
const downloadsViewCore = useMemo(() => buildDownloadsViewModel({
|
const downloadsViewCore = useMemo(() => buildDownloadsViewModel({
|
||||||
@@ -2338,36 +2392,6 @@ export function App(): ReactElement {
|
|||||||
void loadAllDebridHostInfo(true);
|
void loadAllDebridHostInfo(true);
|
||||||
}, [settingsSubTab, hasSavedAllDebridAccount, snapshot.settings.allDebridToken, snapshot.settings.allDebridUseWebLogin, loadAllDebridHostInfo]);
|
}, [settingsSubTab, hasSavedAllDebridAccount, snapshot.settings.allDebridToken, snapshot.settings.allDebridUseWebLogin, loadAllDebridHostInfo]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const extractingPkgIds: string[] = [];
|
|
||||||
const currentlyExtracting = new Set<string>();
|
|
||||||
for (const pkg of packages) {
|
|
||||||
const items = (pkg.itemIds ?? []).map((id) => snapshot.session.items[id]).filter(Boolean);
|
|
||||||
const isExtracting = items.some((item) => item.fullStatus?.startsWith("Entpacken -") && !item.fullStatus?.includes("Done"));
|
|
||||||
if (isExtracting) {
|
|
||||||
currentlyExtracting.add(pkg.id);
|
|
||||||
if (collapsedPackages[pkg.id]
|
|
||||||
&& !manualCollapsedPkgsRef.current.has(pkg.id)
|
|
||||||
&& !autoExpandedPkgsRef.current.has(pkg.id)) {
|
|
||||||
extractingPkgIds.push(pkg.id);
|
|
||||||
autoExpandedPkgsRef.current.add(pkg.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const id of autoExpandedPkgsRef.current) {
|
|
||||||
if (!currentlyExtracting.has(id)) {
|
|
||||||
autoExpandedPkgsRef.current.delete(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (extractingPkgIds.length > 0) {
|
|
||||||
setCollapsedPackages((prev) => {
|
|
||||||
const next = { ...prev };
|
|
||||||
for (const id of extractingPkgIds) next[id] = false;
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [packages, snapshot.session.items, collapsedPackages]);
|
|
||||||
|
|
||||||
const allPackagesCollapsed = useMemo(() => (
|
const allPackagesCollapsed = useMemo(() => (
|
||||||
packages.length > 0 && packages.every((pkg) => collapsedPackages[pkg.id])
|
packages.length > 0 && packages.every((pkg) => collapsedPackages[pkg.id])
|
||||||
), [packages, collapsedPackages]);
|
), [packages, collapsedPackages]);
|
||||||
@@ -2453,12 +2477,12 @@ export function App(): ReactElement {
|
|||||||
const option = findAccountOption(kind);
|
const option = findAccountOption(kind);
|
||||||
let statusLabel = "Aktiviert";
|
let statusLabel = "Aktiviert";
|
||||||
let note = "";
|
let note = "";
|
||||||
if (kind === "megadebrid-api") {
|
if (kind === "megadebrid-api") {
|
||||||
const megaAccountCount = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword).length;
|
const megaAccountCount = getMegaDebridAccountsForMode(settingsDraft, "api").length;
|
||||||
statusLabel = megaAccountCount > 1 ? `${megaAccountCount} Accounts` : "Aktiviert";
|
statusLabel = megaAccountCount > 1 ? `${megaAccountCount} Accounts` : "Aktiviert";
|
||||||
note = "Nur API aktiv. Kein Web-Fallback.";
|
note = "Nur API aktiv. Kein Web-Fallback.";
|
||||||
} else if (kind === "megadebrid-web") {
|
} else if (kind === "megadebrid-web") {
|
||||||
const megaAccountCount = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword).length;
|
const megaAccountCount = getMegaDebridAccountsForMode(settingsDraft, "web").length;
|
||||||
statusLabel = megaAccountCount > 1 ? `${megaAccountCount} Accounts` : "Aktiviert";
|
statusLabel = megaAccountCount > 1 ? `${megaAccountCount} Accounts` : "Aktiviert";
|
||||||
note = "Nur Web aktiv. Kein API-Fallback.";
|
note = "Nur Web aktiv. Kein API-Fallback.";
|
||||||
} else if (kind === "realdebrid-web") {
|
} else if (kind === "realdebrid-web") {
|
||||||
@@ -2569,8 +2593,10 @@ export function App(): ReactElement {
|
|||||||
const accountRows = useMemo(() => {
|
const accountRows = useMemo(() => {
|
||||||
const rows: AccountTableRow[] = [];
|
const rows: AccountTableRow[] = [];
|
||||||
for (const entry of configuredAccounts) {
|
for (const entry of configuredAccounts) {
|
||||||
if (entry.kind === "megadebrid-api" || entry.kind === "megadebrid-web") {
|
if (entry.kind === "megadebrid-api" || entry.kind === "megadebrid-web") {
|
||||||
const accounts = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword || "");
|
const mode = entry.kind === "megadebrid-web" ? "web" : "api";
|
||||||
|
const accounts = getMegaDebridAccountsForMode(settingsDraft, mode);
|
||||||
|
const disabledAccountIds = getMegaDebridDisabledAccountIdsForMode(settingsDraft, mode);
|
||||||
for (const acc of accounts) {
|
for (const acc of accounts) {
|
||||||
const used = getMegaDebridAccountDailyUsageBytes(snapshot.settings, acc.id);
|
const used = getMegaDebridAccountDailyUsageBytes(snapshot.settings, acc.id);
|
||||||
const limit = getMegaDebridAccountDailyLimitBytes(settingsDraft, acc.id);
|
const limit = getMegaDebridAccountDailyLimitBytes(settingsDraft, acc.id);
|
||||||
@@ -2583,7 +2609,7 @@ export function App(): ReactElement {
|
|||||||
credentialLabel: "••••••",
|
credentialLabel: "••••••",
|
||||||
accountId: acc.id,
|
accountId: acc.id,
|
||||||
checkable: true,
|
checkable: true,
|
||||||
disabled: (settingsDraft.megaDebridDisabledAccountIds || []).includes(acc.id),
|
disabled: disabledAccountIds.includes(acc.id),
|
||||||
dailyUsedBytes: used,
|
dailyUsedBytes: used,
|
||||||
dailyLimitBytes: limit,
|
dailyLimitBytes: limit,
|
||||||
dailyRemainingBytes: limit > 0 ? Math.max(0, limit - used) : 0,
|
dailyRemainingBytes: limit > 0 ? Math.max(0, limit - used) : 0,
|
||||||
@@ -3095,9 +3121,23 @@ export function App(): ReactElement {
|
|||||||
megaCredentials: serializeMegaDebridAccounts([newMegaCredentials]),
|
megaCredentials: serializeMegaDebridAccounts([newMegaCredentials]),
|
||||||
megaLogin: newMegaCredentials.login,
|
megaLogin: newMegaCredentials.login,
|
||||||
megaPassword: newMegaCredentials.password,
|
megaPassword: newMegaCredentials.password,
|
||||||
|
megaDebridApiCredentials: selectedOption?.service === "megadebrid-web" ? "" : serializeMegaDebridAccounts([newMegaCredentials]),
|
||||||
|
megaDebridWebCredentials: selectedOption?.service === "megadebrid-web" ? serializeMegaDebridAccounts([newMegaCredentials]) : "",
|
||||||
|
megaDebridApiEnabled: selectedOption?.service !== "megadebrid-web",
|
||||||
|
megaDebridWebEnabled: selectedOption?.service === "megadebrid-web",
|
||||||
debridLinkApiKeys: ""
|
debridLinkApiKeys: ""
|
||||||
}
|
}
|
||||||
: { ...nextDraft, megaCredentials: "", megaLogin: "", megaPassword: "", debridLinkApiKeys: newDebridLinkToken };
|
: {
|
||||||
|
...nextDraft,
|
||||||
|
megaCredentials: "",
|
||||||
|
megaLogin: "",
|
||||||
|
megaPassword: "",
|
||||||
|
megaDebridApiCredentials: "",
|
||||||
|
megaDebridWebCredentials: "",
|
||||||
|
megaDebridApiEnabled: false,
|
||||||
|
megaDebridWebEnabled: false,
|
||||||
|
debridLinkApiKeys: newDebridLinkToken
|
||||||
|
};
|
||||||
const statuses = await window.rd.checkDebridAccounts(checkSettings, true, targetedCheck.expectedStatusId);
|
const statuses = await window.rd.checkDebridAccounts(checkSettings, true, targetedCheck.expectedStatusId);
|
||||||
const status = statuses.length === 1 && statuses[0].accountId === targetedCheck.expectedStatusId ? statuses[0] : null;
|
const status = statuses.length === 1 && statuses[0].accountId === targetedCheck.expectedStatusId ? statuses[0] : null;
|
||||||
if (!status || !status.valid) {
|
if (!status || !status.valid) {
|
||||||
@@ -3193,30 +3233,26 @@ export function App(): ReactElement {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onToggleMegaAccountEnabled = async (login: string, currentlyDisabled: boolean): Promise<void> => {
|
const onToggleMegaAccountEnabled = async (kind: "megadebrid-api" | "megadebrid-web", login: string, currentlyDisabled: boolean): Promise<void> => {
|
||||||
const accId = getMegaDebridAccountId(login.trim());
|
const accId = getMegaDebridAccountId(login.trim());
|
||||||
await performQuickAction(async () => {
|
await performQuickAction(async () => {
|
||||||
const current = settingsDraft.megaDebridDisabledAccountIds || [];
|
const mode = kind === "megadebrid-web" ? "web" : "api";
|
||||||
const next = currentlyDisabled ? current.filter((id) => id !== accId) : [...current, accId];
|
const current = getMegaDebridDisabledAccountIdsForMode(settingsDraft, mode);
|
||||||
await persistSpecificSettings({ ...settingsDraft, megaDebridDisabledAccountIds: next });
|
const next = currentlyDisabled ? current.filter((id) => id !== accId) : [...current, accId];
|
||||||
|
const apiDisabledIds = mode === "api" ? next : getMegaDebridDisabledAccountIdsForMode(settingsDraft, "api");
|
||||||
|
const webDisabledIds = mode === "web" ? next : getMegaDebridDisabledAccountIdsForMode(settingsDraft, "web");
|
||||||
|
await persistSpecificSettings({
|
||||||
|
...settingsDraft,
|
||||||
|
megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])],
|
||||||
|
megaDebridApiDisabledAccountIds: apiDisabledIds,
|
||||||
|
megaDebridWebDisabledAccountIds: webDisabledIds
|
||||||
|
});
|
||||||
showToast(currentlyDisabled ? "Account aktiviert" : "Account deaktiviert", 2000);
|
showToast(currentlyDisabled ? "Account aktiviert" : "Account deaktiviert", 2000);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
showToast(`Umschalten fehlgeschlagen: ${String(error)}`, 3200);
|
showToast(`Umschalten fehlgeschlagen: ${String(error)}`, 3200);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRemoveMegaAccount = async (login: string): Promise<void> => {
|
|
||||||
const confirmed = await askConfirmPrompt({ title: "Account entfernen", message: `Soll der Mega-Debrid-Account ${maskMegaDebridLogin(login)} wirklich entfernt werden?`, confirmLabel: "Entfernen", danger: true });
|
|
||||||
if (!confirmed) return;
|
|
||||||
await performQuickAction(async () => {
|
|
||||||
const remaining = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword || "")
|
|
||||||
.filter((a) => a.login.trim().toLowerCase() !== login.trim().toLowerCase());
|
|
||||||
const nextCreds = serializeMegaDebridAccounts(remaining.map((a) => ({ login: a.login, password: a.password })));
|
|
||||||
await persistSpecificSettings({ ...settingsDraft, megaCredentials: nextCreds, megaLogin: remaining[0]?.login ?? "", megaPassword: remaining[0]?.password ?? "" });
|
|
||||||
showToast("Account entfernt", 2000);
|
|
||||||
}, (error) => { showToast(`Entfernen fehlgeschlagen: ${String(error)}`, 3200); });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRemoveDebridLinkKey = async (key: DebridLinkAccountKeyEntry): Promise<void> => {
|
const onRemoveDebridLinkKey = async (key: DebridLinkAccountKeyEntry): Promise<void> => {
|
||||||
const confirmed = await askConfirmPrompt({ title: "Key entfernen", message: `Soll der Debrid-Link-Key ${key.masked} wirklich entfernt werden?`, confirmLabel: "Entfernen", danger: true });
|
const confirmed = await askConfirmPrompt({ title: "Key entfernen", message: `Soll der Debrid-Link-Key ${key.masked} wirklich entfernt werden?`, confirmLabel: "Entfernen", danger: true });
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
@@ -3253,7 +3289,7 @@ export function App(): ReactElement {
|
|||||||
const toggleAccountTableRow = (row: AccountTableRow): void => {
|
const toggleAccountTableRow = (row: AccountTableRow): void => {
|
||||||
setAccountContextMenu(null);
|
setAccountContextMenu(null);
|
||||||
if (row.toggleKind === "mega" && row.megaLogin) {
|
if (row.toggleKind === "mega" && row.megaLogin) {
|
||||||
void onToggleMegaAccountEnabled(row.megaLogin, row.disabled);
|
void onToggleMegaAccountEnabled(row.entry.kind as "megadebrid-api" | "megadebrid-web", row.megaLogin, row.disabled);
|
||||||
} else if (row.toggleKind === "dl" && row.dlKey) {
|
} else if (row.toggleKind === "dl" && row.dlKey) {
|
||||||
void onToggleDebridLinkApiKeyEnabled(row.entry, row.dlKey);
|
void onToggleDebridLinkApiKeyEnabled(row.entry, row.dlKey);
|
||||||
} else {
|
} else {
|
||||||
@@ -3273,7 +3309,9 @@ export function App(): ReactElement {
|
|||||||
);
|
);
|
||||||
const nextDraft: AppSettings = {
|
const nextDraft: AppSettings = {
|
||||||
...settingsDraft,
|
...settingsDraft,
|
||||||
...nextEnabledState
|
...nextEnabledState,
|
||||||
|
megaDebridApiDisabledAccountIds: enabled ? [] : accountRows.filter((row) => row.entry.kind === "megadebrid-api" && row.accountId).map((row) => row.accountId as string),
|
||||||
|
megaDebridWebDisabledAccountIds: enabled ? [] : accountRows.filter((row) => row.entry.kind === "megadebrid-web" && row.accountId).map((row) => row.accountId as string)
|
||||||
};
|
};
|
||||||
await persistSpecificSettings(nextDraft);
|
await persistSpecificSettings(nextDraft);
|
||||||
showToast(enabled ? "Accounts aktiviert" : "Accounts deaktiviert", 2200);
|
showToast(enabled ? "Accounts aktiviert" : "Accounts deaktiviert", 2200);
|
||||||
@@ -3310,7 +3348,7 @@ export function App(): ReactElement {
|
|||||||
const checkAccountTableRow = (row: AccountTableRow): void => {
|
const checkAccountTableRow = (row: AccountTableRow): void => {
|
||||||
setAccountContextMenu(null);
|
setAccountContextMenu(null);
|
||||||
if (row.toggleKind === "mega" && row.megaLogin) {
|
if (row.toggleKind === "mega" && row.megaLogin) {
|
||||||
const account = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword || "")
|
const account = getMegaDebridAccountsForMode(settingsDraft, row.entry.kind === "megadebrid-web" ? "web" : "api")
|
||||||
.find((entry) => entry.id === row.accountId);
|
.find((entry) => entry.id === row.accountId);
|
||||||
if (account) {
|
if (account) {
|
||||||
void runMegaAccountCheck(account.login, account.password);
|
void runMegaAccountCheck(account.login, account.password);
|
||||||
@@ -4029,17 +4067,11 @@ export function App(): ReactElement {
|
|||||||
}
|
}
|
||||||
}, [showToast]);
|
}, [showToast]);
|
||||||
|
|
||||||
const onPackageToggleCollapse = useCallback((packageId: string): void => {
|
const onPackageToggleCollapse = useCallback((packageId: string): void => {
|
||||||
setCollapsedPackages((prev) => {
|
setCollapsedPackages((prev) => {
|
||||||
const nextCollapsed = !(prev[packageId] ?? false);
|
const nextCollapsed = !(prev[packageId] ?? false);
|
||||||
if (nextCollapsed) {
|
return { ...prev, [packageId]: nextCollapsed };
|
||||||
manualCollapsedPkgsRef.current.add(packageId);
|
});
|
||||||
} else {
|
|
||||||
manualCollapsedPkgsRef.current.delete(packageId);
|
|
||||||
autoExpandedPkgsRef.current.delete(packageId);
|
|
||||||
}
|
|
||||||
return { ...prev, [packageId]: nextCollapsed };
|
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onPackageCancel = useCallback((packageId: string): void => {
|
const onPackageCancel = useCallback((packageId: string): void => {
|
||||||
@@ -5014,11 +5046,6 @@ export function App(): ReactElement {
|
|||||||
const next = { ...current };
|
const next = { ...current };
|
||||||
for (const entry of packages) {
|
for (const entry of packages) {
|
||||||
next[entry.id] = targetState;
|
next[entry.id] = targetState;
|
||||||
if (targetState) manualCollapsedPkgsRef.current.add(entry.id);
|
|
||||||
else {
|
|
||||||
manualCollapsedPkgsRef.current.delete(entry.id);
|
|
||||||
autoExpandedPkgsRef.current.delete(entry.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
@@ -5480,26 +5507,7 @@ export function App(): ReactElement {
|
|||||||
multi: option.kind === "megadebrid-api" || option.kind === "megadebrid-web" || option.kind === "debridlink-api",
|
multi: option.kind === "megadebrid-api" || option.kind === "megadebrid-web" || option.kind === "debridlink-api",
|
||||||
icon: ACCOUNT_SERVICE_ICONS[option.service]
|
icon: ACCOUNT_SERVICE_ICONS[option.service]
|
||||||
}));
|
}));
|
||||||
const accountAddFields: AccountDialogField[] = accountDialog && accountDialogOption ? [
|
const accountAddFields = buildAccountAddFields(accountDialog);
|
||||||
...((accountDialog.kind === "megadebrid-api" || accountDialog.kind === "megadebrid-web") ? [
|
|
||||||
{ id: "megaNewLogin", label: "Login / E-Mail", type: "text" as const, value: accountDialog.megaNewLogin },
|
|
||||||
{ id: "megaNewPassword", label: "Passwort", type: "password" as const, value: accountDialog.megaNewPassword }
|
|
||||||
] : accountDialogOption.needsCredentials ? [
|
|
||||||
{ id: "login", label: "Login / E-Mail", type: "text" as const, value: accountDialog.login },
|
|
||||||
{ id: "password", label: "Passwort", type: "password" as const, value: accountDialog.password }
|
|
||||||
] : []),
|
|
||||||
...(accountDialogOption.needsToken ? [
|
|
||||||
{ id: "token", label: accountDialog.kind === "debridlink-api" ? "API-Key" : "Token / API-Key", type: "password" as const, value: accountDialog.token }
|
|
||||||
] : []),
|
|
||||||
{
|
|
||||||
id: "dailyLimitGb",
|
|
||||||
label: "Tageslimit (GB, optional)",
|
|
||||||
type: "number" as const,
|
|
||||||
value: accountDialog.dailyLimitGb,
|
|
||||||
placeholder: "Kein Limit",
|
|
||||||
help: "Der Zähler wird täglich um 00:00 Uhr zurückgesetzt."
|
|
||||||
}
|
|
||||||
] : [];
|
|
||||||
const accountAddDialog = (
|
const accountAddDialog = (
|
||||||
<AccountAddDialog
|
<AccountAddDialog
|
||||||
actions={{
|
actions={{
|
||||||
|
|||||||
+469
-422
@@ -1,458 +1,505 @@
|
|||||||
import { parseDebridLinkApiKeys, getDebridLinkApiKeyId } from "../shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys, getDebridLinkApiKeyId } from "../shared/debrid-link-keys";
|
||||||
import { getMegaDebridAccountId, parseMegaDebridAccounts, serializeMegaDebridAccounts } from "../shared/mega-debrid-accounts";
|
import { getMegaDebridAccountId, getMegaDebridAccountsForMode, getMegaDebridCredentialsForMode, getMegaDebridDisabledAccountIdsForMode, mergeMegaDebridCredentialPools, parseMegaDebridAccounts, serializeMegaDebridAccounts, type MegaDebridAccountMode } from "../shared/mega-debrid-accounts";
|
||||||
import type { AppSettings, DebridAccountStatus, DebridProvider } from "../shared/types";
|
import type { AppSettings, DebridAccountStatus, DebridProvider } from "../shared/types";
|
||||||
|
|
||||||
export type AccountService = "realdebrid" | "megadebrid-api" | "megadebrid-web" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink" | "linksnappy";
|
export type AccountService = "realdebrid" | "megadebrid-api" | "megadebrid-web" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink" | "linksnappy";
|
||||||
|
|
||||||
export type AccountKind =
|
export type AccountKind =
|
||||||
| "realdebrid-api"
|
| "realdebrid-api"
|
||||||
| "realdebrid-web"
|
| "realdebrid-web"
|
||||||
| "megadebrid-api"
|
| "megadebrid-api"
|
||||||
| "megadebrid-web"
|
| "megadebrid-web"
|
||||||
| "bestdebrid-api"
|
| "bestdebrid-api"
|
||||||
| "bestdebrid-web"
|
| "bestdebrid-web"
|
||||||
| "alldebrid-api"
|
| "alldebrid-api"
|
||||||
| "alldebrid-web"
|
| "alldebrid-web"
|
||||||
| "ddownload-login"
|
| "ddownload-login"
|
||||||
| "onefichier-api"
|
| "onefichier-api"
|
||||||
| "debridlink-api"
|
| "debridlink-api"
|
||||||
| "linksnappy-login";
|
| "linksnappy-login";
|
||||||
|
|
||||||
export type SingleAccountKind = Exclude<AccountKind, "megadebrid-api" | "megadebrid-web" | "debridlink-api">;
|
export type SingleAccountKind = Exclude<AccountKind, "megadebrid-api" | "megadebrid-web" | "debridlink-api">;
|
||||||
|
|
||||||
export type AccountEditTarget =
|
export type AccountEditTarget =
|
||||||
| {
|
| {
|
||||||
type: "single";
|
type: "single";
|
||||||
rowKey: string;
|
rowKey: string;
|
||||||
kind: SingleAccountKind;
|
kind: SingleAccountKind;
|
||||||
service: AccountService;
|
service: AccountService;
|
||||||
provider: DebridProvider;
|
provider: DebridProvider;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "mega";
|
type: "mega";
|
||||||
rowKey: string;
|
rowKey: string;
|
||||||
kind: "megadebrid-api" | "megadebrid-web";
|
kind: "megadebrid-api" | "megadebrid-web";
|
||||||
service: "megadebrid-api" | "megadebrid-web";
|
service: "megadebrid-api" | "megadebrid-web";
|
||||||
accountId: string;
|
accountId: string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: "debridlink";
|
type: "debridlink";
|
||||||
rowKey: string;
|
rowKey: string;
|
||||||
kind: "debridlink-api";
|
kind: "debridlink-api";
|
||||||
service: "debridlink";
|
service: "debridlink";
|
||||||
keyId: string;
|
keyId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AccountEditState {
|
export interface AccountEditState {
|
||||||
target: AccountEditTarget;
|
target: AccountEditTarget;
|
||||||
login: string;
|
login: string;
|
||||||
password: string;
|
password: string;
|
||||||
token: string;
|
token: string;
|
||||||
dailyLimitGb: string;
|
dailyLimitGb: string;
|
||||||
originalDailyLimitBytes: number;
|
originalDailyLimitBytes: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BYTES_PER_GIB = 1024 * 1024 * 1024;
|
const BYTES_PER_GIB = 1024 * 1024 * 1024;
|
||||||
|
|
||||||
function formatDailyLimit(limitBytes: number): string {
|
function getMegaTargetMode(target: Extract<AccountEditTarget, { type: "mega" }>): MegaDebridAccountMode {
|
||||||
if (!Number.isFinite(limitBytes) || limitBytes <= 0) {
|
return target.kind === "megadebrid-web" ? "web" : "api";
|
||||||
return "";
|
|
||||||
}
|
|
||||||
const gib = limitBytes / BYTES_PER_GIB;
|
|
||||||
const precision = gib >= 100 ? 0 : gib >= 10 ? 1 : 2;
|
|
||||||
return gib.toFixed(precision).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDailyLimit(value: string): number | null {
|
function formatDailyLimit(limitBytes: number): string {
|
||||||
const normalized = value.trim().replace(",", ".");
|
if (!Number.isFinite(limitBytes) || limitBytes <= 0) {
|
||||||
if (!normalized) {
|
return "";
|
||||||
return null;
|
}
|
||||||
}
|
const gib = limitBytes / BYTES_PER_GIB;
|
||||||
const parsed = Number(normalized);
|
const precision = gib >= 100 ? 0 : gib >= 10 ? 1 : 2;
|
||||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
return gib.toFixed(precision).replace(/\.0+$/, "").replace(/(\.\d*?)0+$/, "$1");
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
return Math.floor(parsed * BYTES_PER_GIB);
|
function parseDailyLimit(value: string): number | null {
|
||||||
}
|
const normalized = value.trim().replace(",", ".");
|
||||||
|
if (!normalized) {
|
||||||
function withoutRecordKeys<T>(record: Record<string, T>, ...keys: string[]): Record<string, T> {
|
return null;
|
||||||
const blocked = new Set(keys);
|
}
|
||||||
return Object.fromEntries(Object.entries(record || {}).filter(([key]) => !blocked.has(key)));
|
const parsed = Number(normalized);
|
||||||
}
|
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||||
|
return null;
|
||||||
function resolveDailyLimit(value: string, originalBytes: number): number | null {
|
}
|
||||||
return value === formatDailyLimit(originalBytes) ? (originalBytes > 0 ? originalBytes : null) : parseDailyLimit(value);
|
return Math.floor(parsed * BYTES_PER_GIB);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTargetLimit(record: Record<string, number>, oldId: string, newId: string, value: string, originalBytes: number): Record<string, number> {
|
function withoutRecordKeys<T>(record: Record<string, T>, ...keys: string[]): Record<string, T> {
|
||||||
const next = withoutRecordKeys(record || {}, oldId, newId);
|
const blocked = new Set(keys);
|
||||||
const limit = resolveDailyLimit(value, originalBytes);
|
return Object.fromEntries(Object.entries(record || {}).filter(([key]) => !blocked.has(key)));
|
||||||
if (limit && limit > 0) {
|
}
|
||||||
next[newId] = limit;
|
|
||||||
}
|
function resolveDailyLimit(value: string, originalBytes: number): number | null {
|
||||||
return next;
|
return value === formatDailyLimit(originalBytes) ? (originalBytes > 0 ? originalBytes : null) : parseDailyLimit(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateDisabledId(ids: readonly string[], oldId: string, newId: string): string[] {
|
function updateTargetLimit(record: Record<string, number>, oldId: string, newId: string, value: string, originalBytes: number): Record<string, number> {
|
||||||
const wasDisabled = ids.includes(oldId);
|
const next = withoutRecordKeys(record || {}, oldId, newId);
|
||||||
const next = ids.filter((id) => id !== oldId && id !== newId);
|
const limit = resolveDailyLimit(value, originalBytes);
|
||||||
if (wasDisabled) {
|
if (limit && limit > 0) {
|
||||||
next.push(newId);
|
next[newId] = limit;
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateProviderLimit(settings: AppSettings, provider: DebridProvider, value: string, originalBytes: number): AppSettings["providerDailyLimitBytes"] {
|
function migrateDisabledId(ids: readonly string[], oldId: string, newId: string): string[] {
|
||||||
const next = { ...(settings.providerDailyLimitBytes || {}) };
|
const wasDisabled = ids.includes(oldId);
|
||||||
const limit = resolveDailyLimit(value, originalBytes);
|
const next = ids.filter((id) => id !== oldId && id !== newId);
|
||||||
if (limit && limit > 0) {
|
if (wasDisabled) {
|
||||||
next[provider] = limit;
|
next.push(newId);
|
||||||
} else {
|
}
|
||||||
delete next[provider];
|
return next;
|
||||||
}
|
}
|
||||||
return next;
|
|
||||||
}
|
function updateProviderLimit(settings: AppSettings, provider: DebridProvider, value: string, originalBytes: number): AppSettings["providerDailyLimitBytes"] {
|
||||||
|
const next = { ...(settings.providerDailyLimitBytes || {}) };
|
||||||
function createSingleEditState(target: Extract<AccountEditTarget, { type: "single" }>, settings: AppSettings): AccountEditState {
|
const limit = resolveDailyLimit(value, originalBytes);
|
||||||
const originalDailyLimitBytes = settings.providerDailyLimitBytes?.[target.provider] || 0;
|
if (limit && limit > 0) {
|
||||||
const base = {
|
next[provider] = limit;
|
||||||
target,
|
} else {
|
||||||
login: "",
|
delete next[provider];
|
||||||
password: "",
|
}
|
||||||
token: "",
|
return next;
|
||||||
dailyLimitGb: formatDailyLimit(originalDailyLimitBytes),
|
}
|
||||||
originalDailyLimitBytes
|
|
||||||
};
|
function createSingleEditState(target: Extract<AccountEditTarget, { type: "single" }>, settings: AppSettings): AccountEditState {
|
||||||
switch (target.kind) {
|
const originalDailyLimitBytes = settings.providerDailyLimitBytes?.[target.provider] || 0;
|
||||||
case "realdebrid-api":
|
const base = {
|
||||||
return { ...base, token: settings.token };
|
target,
|
||||||
case "bestdebrid-api":
|
login: "",
|
||||||
return { ...base, token: settings.bestToken };
|
password: "",
|
||||||
case "alldebrid-api":
|
token: "",
|
||||||
return { ...base, token: settings.allDebridToken };
|
dailyLimitGb: formatDailyLimit(originalDailyLimitBytes),
|
||||||
case "onefichier-api":
|
originalDailyLimitBytes
|
||||||
return { ...base, token: settings.oneFichierApiKey };
|
};
|
||||||
case "ddownload-login":
|
switch (target.kind) {
|
||||||
return { ...base, login: settings.ddownloadLogin, password: settings.ddownloadPassword };
|
case "realdebrid-api":
|
||||||
case "linksnappy-login":
|
return { ...base, token: settings.token };
|
||||||
return { ...base, login: settings.linkSnappyLogin, password: settings.linkSnappyPassword };
|
case "bestdebrid-api":
|
||||||
default:
|
return { ...base, token: settings.bestToken };
|
||||||
return base;
|
case "alldebrid-api":
|
||||||
}
|
return { ...base, token: settings.allDebridToken };
|
||||||
}
|
case "onefichier-api":
|
||||||
|
return { ...base, token: settings.oneFichierApiKey };
|
||||||
export function createAccountEditState(target: AccountEditTarget, settings: AppSettings): AccountEditState {
|
case "ddownload-login":
|
||||||
if (target.type === "single") {
|
return { ...base, login: settings.ddownloadLogin, password: settings.ddownloadPassword };
|
||||||
return createSingleEditState(target, settings);
|
case "linksnappy-login":
|
||||||
}
|
return { ...base, login: settings.linkSnappyLogin, password: settings.linkSnappyPassword };
|
||||||
|
default:
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAccountEditState(target: AccountEditTarget, settings: AppSettings): AccountEditState {
|
||||||
|
if (target.type === "single") {
|
||||||
|
return createSingleEditState(target, settings);
|
||||||
|
}
|
||||||
if (target.type === "mega") {
|
if (target.type === "mega") {
|
||||||
const account = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword || "")
|
const account = getMegaDebridAccountsForMode(settings, getMegaTargetMode(target))
|
||||||
.find((entry) => entry.id === target.accountId);
|
.find((entry) => entry.id === target.accountId);
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error("Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.");
|
throw new Error("Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.");
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
target,
|
target,
|
||||||
login: account.login,
|
login: account.login,
|
||||||
password: account.password,
|
password: account.password,
|
||||||
token: "",
|
token: "",
|
||||||
dailyLimitGb: formatDailyLimit(settings.megaDebridAccountDailyLimitBytes?.[account.id] || 0),
|
dailyLimitGb: formatDailyLimit(settings.megaDebridAccountDailyLimitBytes?.[account.id] || 0),
|
||||||
originalDailyLimitBytes: settings.megaDebridAccountDailyLimitBytes?.[account.id] || 0
|
originalDailyLimitBytes: settings.megaDebridAccountDailyLimitBytes?.[account.id] || 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const key = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "").find((entry) => entry.id === target.keyId);
|
const key = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "").find((entry) => entry.id === target.keyId);
|
||||||
if (!key) {
|
if (!key) {
|
||||||
throw new Error("Der ausgewählte Debrid-Link-Key wurde nicht gefunden.");
|
throw new Error("Der ausgewählte Debrid-Link-Key wurde nicht gefunden.");
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
target,
|
target,
|
||||||
login: "",
|
login: "",
|
||||||
password: "",
|
password: "",
|
||||||
token: key.token,
|
token: key.token,
|
||||||
dailyLimitGb: formatDailyLimit(settings.debridLinkApiKeyDailyLimitBytes?.[key.id] || 0),
|
dailyLimitGb: formatDailyLimit(settings.debridLinkApiKeyDailyLimitBytes?.[key.id] || 0),
|
||||||
originalDailyLimitBytes: settings.debridLinkApiKeyDailyLimitBytes?.[key.id] || 0
|
originalDailyLimitBytes: settings.debridLinkApiKeyDailyLimitBytes?.[key.id] || 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateDailyLimit(value: string): string | null {
|
function validateDailyLimit(value: string): string | null {
|
||||||
const normalized = value.trim().replace(",", ".");
|
const normalized = value.trim().replace(",", ".");
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const parsed = Number(normalized);
|
const parsed = Number(normalized);
|
||||||
return Number.isFinite(parsed) && parsed >= 0 ? null : "Das Tageslimit muss eine positive Zahl oder 0 sein.";
|
return Number.isFinite(parsed) && parsed >= 0 ? null : "Das Tageslimit muss eine positive Zahl oder 0 sein.";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateAccountEdit(state: AccountEditState, settings: AppSettings): string | null {
|
export function validateAccountEdit(state: AccountEditState, settings: AppSettings): string | null {
|
||||||
const limitError = validateDailyLimit(state.dailyLimitGb);
|
const limitError = validateDailyLimit(state.dailyLimitGb);
|
||||||
if (limitError) {
|
if (limitError) {
|
||||||
return limitError;
|
return limitError;
|
||||||
}
|
}
|
||||||
if (state.target.type === "mega") {
|
if (state.target.type === "mega") {
|
||||||
const target = state.target;
|
const target = state.target;
|
||||||
const login = state.login.trim();
|
const login = state.login.trim();
|
||||||
if (!login || !state.password.trim()) {
|
if (!login || !state.password.trim()) {
|
||||||
return "Login und Passwort werden benötigt.";
|
return "Login und Passwort werden benötigt.";
|
||||||
}
|
}
|
||||||
if (/[:\r\n]/.test(login)) {
|
if (/[:\r\n]/.test(login)) {
|
||||||
return "Der Login darf keinen Doppelpunkt oder Zeilenumbruch enthalten.";
|
return "Der Login darf keinen Doppelpunkt oder Zeilenumbruch enthalten.";
|
||||||
}
|
}
|
||||||
if (/[\r\n]/.test(state.password)) {
|
if (/[\r\n]/.test(state.password)) {
|
||||||
return "Das Passwort darf keinen Zeilenumbruch enthalten.";
|
return "Das Passwort darf keinen Zeilenumbruch enthalten.";
|
||||||
}
|
}
|
||||||
const accounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword || "");
|
const accounts = getMegaDebridAccountsForMode(settings, getMegaTargetMode(target));
|
||||||
if (!accounts.some((entry) => entry.id === target.accountId)) {
|
if (!accounts.some((entry) => entry.id === target.accountId)) {
|
||||||
return "Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.";
|
return "Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.";
|
||||||
}
|
}
|
||||||
if (accounts.some((entry) => entry.id !== target.accountId && entry.login.toLowerCase() === login.toLowerCase())) {
|
if (accounts.some((entry) => entry.id !== target.accountId && entry.login.toLowerCase() === login.toLowerCase())) {
|
||||||
return "Dieser Mega-Debrid-Login ist bereits vorhanden.";
|
return "Dieser Mega-Debrid-Login ist bereits vorhanden.";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (state.target.type === "debridlink") {
|
if (state.target.type === "debridlink") {
|
||||||
const target = state.target;
|
const target = state.target;
|
||||||
const token = state.token.trim();
|
const token = state.token.trim();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return "Der API-Key wird benötigt.";
|
return "Der API-Key wird benötigt.";
|
||||||
}
|
}
|
||||||
if (/[,\r\n]/.test(token)) {
|
if (/[,\r\n]/.test(token)) {
|
||||||
return "Beim Bearbeiten ist genau ein API-Key erlaubt.";
|
return "Beim Bearbeiten ist genau ein API-Key erlaubt.";
|
||||||
}
|
}
|
||||||
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "");
|
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "");
|
||||||
if (!keys.some((entry) => entry.id === target.keyId)) {
|
if (!keys.some((entry) => entry.id === target.keyId)) {
|
||||||
return "Der ausgewählte Debrid-Link-Key wurde nicht gefunden.";
|
return "Der ausgewählte Debrid-Link-Key wurde nicht gefunden.";
|
||||||
}
|
}
|
||||||
if (keys.some((entry) => entry.id !== target.keyId && entry.token === token)) {
|
if (keys.some((entry) => entry.id !== target.keyId && entry.token === token)) {
|
||||||
return "Dieser Debrid-Link-Key ist bereits vorhanden.";
|
return "Dieser Debrid-Link-Key ist bereits vorhanden.";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (["realdebrid-api", "bestdebrid-api", "alldebrid-api", "onefichier-api"].includes(state.target.kind) && !state.token.trim()) {
|
if (["realdebrid-api", "bestdebrid-api", "alldebrid-api", "onefichier-api"].includes(state.target.kind) && !state.token.trim()) {
|
||||||
return "Der Zugangstoken wird benötigt.";
|
return "Der Zugangstoken wird benötigt.";
|
||||||
}
|
}
|
||||||
if (["ddownload-login", "linksnappy-login"].includes(state.target.kind)) {
|
if (["ddownload-login", "linksnappy-login"].includes(state.target.kind)) {
|
||||||
if (!state.login.trim() || !state.password.trim()) {
|
if (!state.login.trim() || !state.password.trim()) {
|
||||||
return "Login und Passwort werden benötigt.";
|
return "Login und Passwort werden benötigt.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySingleEdit(settings: AppSettings, state: AccountEditState & { target: Extract<AccountEditTarget, { type: "single" }> }): AppSettings {
|
function applySingleEdit(settings: AppSettings, state: AccountEditState & { target: Extract<AccountEditTarget, { type: "single" }> }): AppSettings {
|
||||||
const providerDailyLimitBytes = updateProviderLimit(settings, state.target.provider, state.dailyLimitGb, state.originalDailyLimitBytes);
|
const providerDailyLimitBytes = updateProviderLimit(settings, state.target.provider, state.dailyLimitGb, state.originalDailyLimitBytes);
|
||||||
const token = state.token.trim();
|
const token = state.token.trim();
|
||||||
const login = state.login.trim();
|
const login = state.login.trim();
|
||||||
switch (state.target.kind) {
|
switch (state.target.kind) {
|
||||||
case "realdebrid-api":
|
case "realdebrid-api":
|
||||||
return { ...settings, token, realDebridUseWebLogin: false, providerDailyLimitBytes };
|
return { ...settings, token, realDebridUseWebLogin: false, providerDailyLimitBytes };
|
||||||
case "realdebrid-web":
|
case "realdebrid-web":
|
||||||
return { ...settings, token: "", realDebridUseWebLogin: true, providerDailyLimitBytes };
|
return { ...settings, token: "", realDebridUseWebLogin: true, providerDailyLimitBytes };
|
||||||
case "bestdebrid-api":
|
case "bestdebrid-api":
|
||||||
return { ...settings, bestToken: token, bestDebridUseWebLogin: false, providerDailyLimitBytes };
|
return { ...settings, bestToken: token, bestDebridUseWebLogin: false, providerDailyLimitBytes };
|
||||||
case "bestdebrid-web":
|
case "bestdebrid-web":
|
||||||
return { ...settings, bestToken: "", bestDebridUseWebLogin: true, providerDailyLimitBytes };
|
return { ...settings, bestToken: "", bestDebridUseWebLogin: true, providerDailyLimitBytes };
|
||||||
case "alldebrid-api":
|
case "alldebrid-api":
|
||||||
return { ...settings, allDebridToken: token, allDebridUseWebLogin: false, providerDailyLimitBytes };
|
return { ...settings, allDebridToken: token, allDebridUseWebLogin: false, providerDailyLimitBytes };
|
||||||
case "alldebrid-web":
|
case "alldebrid-web":
|
||||||
return { ...settings, allDebridToken: "", allDebridUseWebLogin: true, providerDailyLimitBytes };
|
return { ...settings, allDebridToken: "", allDebridUseWebLogin: true, providerDailyLimitBytes };
|
||||||
case "ddownload-login":
|
case "ddownload-login":
|
||||||
return { ...settings, ddownloadLogin: login, ddownloadPassword: state.password, providerDailyLimitBytes };
|
return { ...settings, ddownloadLogin: login, ddownloadPassword: state.password, providerDailyLimitBytes };
|
||||||
case "onefichier-api":
|
case "onefichier-api":
|
||||||
return { ...settings, oneFichierApiKey: token, providerDailyLimitBytes };
|
return { ...settings, oneFichierApiKey: token, providerDailyLimitBytes };
|
||||||
case "linksnappy-login":
|
case "linksnappy-login":
|
||||||
return { ...settings, linkSnappyLogin: login, linkSnappyPassword: state.password, providerDailyLimitBytes };
|
return { ...settings, linkSnappyLogin: login, linkSnappyPassword: state.password, providerDailyLimitBytes };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyMegaEdit(settings: AppSettings, state: AccountEditState & { target: Extract<AccountEditTarget, { type: "mega" }> }): AppSettings {
|
function applyMegaEdit(settings: AppSettings, state: AccountEditState & { target: Extract<AccountEditTarget, { type: "mega" }> }): AppSettings {
|
||||||
const accounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword || "");
|
const mode = getMegaTargetMode(state.target);
|
||||||
|
const otherMode: MegaDebridAccountMode = mode === "api" ? "web" : "api";
|
||||||
|
const accounts = getMegaDebridAccountsForMode(settings, mode);
|
||||||
const index = accounts.findIndex((entry) => entry.id === state.target.accountId);
|
const index = accounts.findIndex((entry) => entry.id === state.target.accountId);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
throw new Error("Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.");
|
throw new Error("Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.");
|
||||||
}
|
}
|
||||||
const oldId = state.target.accountId;
|
const oldId = state.target.accountId;
|
||||||
const login = state.login.trim();
|
const login = state.login.trim();
|
||||||
const newId = getMegaDebridAccountId(login);
|
const newId = getMegaDebridAccountId(login);
|
||||||
const nextAccounts = accounts.map((entry, accountIndex) => accountIndex === index
|
const nextAccounts = accounts.map((entry, accountIndex) => accountIndex === index
|
||||||
? { login, password: state.password }
|
? { login, password: state.password }
|
||||||
: { login: entry.login, password: entry.password });
|
: { login: entry.login, password: entry.password });
|
||||||
const idChanged = oldId !== newId;
|
const idChanged = oldId !== newId;
|
||||||
const first = nextAccounts[0] || { login: "", password: "" };
|
const selectedCredentials = serializeMegaDebridAccounts(nextAccounts);
|
||||||
|
const apiCredentials = mode === "api" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "api");
|
||||||
|
const webCredentials = mode === "web" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "web");
|
||||||
|
const mergedCredentials = mergeMegaDebridCredentialPools(apiCredentials, webCredentials);
|
||||||
|
const first = parseMegaDebridAccounts(mergedCredentials)[0];
|
||||||
|
const oldIdStillUsed = idChanged && getMegaDebridAccountsForMode(settings, otherMode).some((entry) => entry.id === oldId);
|
||||||
|
const selectedDisabledIds = idChanged
|
||||||
|
? migrateDisabledId(getMegaDebridDisabledAccountIdsForMode(settings, mode), oldId, newId)
|
||||||
|
: getMegaDebridDisabledAccountIdsForMode(settings, mode);
|
||||||
|
const otherDisabledIds = getMegaDebridDisabledAccountIdsForMode(settings, otherMode);
|
||||||
|
const apiDisabledIds = mode === "api" ? selectedDisabledIds : otherDisabledIds;
|
||||||
|
const webDisabledIds = mode === "web" ? selectedDisabledIds : otherDisabledIds;
|
||||||
|
const nextDailyLimits = updateTargetLimit(settings.megaDebridAccountDailyLimitBytes || {}, oldId, newId, state.dailyLimitGb, state.originalDailyLimitBytes);
|
||||||
|
if (oldIdStillUsed && settings.megaDebridAccountDailyLimitBytes?.[oldId]) {
|
||||||
|
nextDailyLimits[oldId] = settings.megaDebridAccountDailyLimitBytes[oldId];
|
||||||
|
}
|
||||||
|
const retainOldMetadata = <T,>(record: Record<string, T>): Record<string, T> => idChanged
|
||||||
|
? oldIdStillUsed ? withoutRecordKeys(record, newId) : withoutRecordKeys(record, oldId, newId)
|
||||||
|
: { ...record };
|
||||||
return {
|
return {
|
||||||
...settings,
|
...settings,
|
||||||
megaCredentials: serializeMegaDebridAccounts(nextAccounts),
|
megaCredentials: mergedCredentials,
|
||||||
megaLogin: first.login,
|
megaLogin: first?.login || "",
|
||||||
megaPassword: first.password,
|
megaPassword: first?.password || "",
|
||||||
megaDebridDisabledAccountIds: idChanged
|
megaDebridApiCredentials: apiCredentials,
|
||||||
? migrateDisabledId(settings.megaDebridDisabledAccountIds || [], oldId, newId)
|
megaDebridWebCredentials: webCredentials,
|
||||||
: [...(settings.megaDebridDisabledAccountIds || [])],
|
megaDebridApiEnabled: settings.megaDebridApiEnabled && parseMegaDebridAccounts(apiCredentials).length > 0,
|
||||||
megaDebridAccountDailyLimitBytes: updateTargetLimit(settings.megaDebridAccountDailyLimitBytes || {}, oldId, newId, state.dailyLimitGb, state.originalDailyLimitBytes),
|
megaDebridWebEnabled: settings.megaDebridWebEnabled && parseMegaDebridAccounts(webCredentials).length > 0,
|
||||||
megaDebridAccountDailyUsageBytes: idChanged
|
megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])],
|
||||||
? withoutRecordKeys(settings.megaDebridAccountDailyUsageBytes || {}, oldId, newId)
|
megaDebridApiDisabledAccountIds: apiDisabledIds,
|
||||||
: { ...(settings.megaDebridAccountDailyUsageBytes || {}) },
|
megaDebridWebDisabledAccountIds: webDisabledIds,
|
||||||
megaDebridAccountTotalUsageBytes: idChanged
|
megaDebridAccountDailyLimitBytes: nextDailyLimits,
|
||||||
? withoutRecordKeys(settings.megaDebridAccountTotalUsageBytes || {}, oldId, newId)
|
megaDebridAccountDailyUsageBytes: retainOldMetadata(settings.megaDebridAccountDailyUsageBytes || {}),
|
||||||
: { ...(settings.megaDebridAccountTotalUsageBytes || {}) },
|
megaDebridAccountTotalUsageBytes: retainOldMetadata(settings.megaDebridAccountTotalUsageBytes || {}),
|
||||||
debridAccountStatuses: idChanged
|
debridAccountStatuses: retainOldMetadata(settings.debridAccountStatuses || {})
|
||||||
? withoutRecordKeys(settings.debridAccountStatuses || {}, oldId, newId)
|
|
||||||
: { ...(settings.debridAccountStatuses || {}) }
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyDebridLinkEdit(settings: AppSettings, state: AccountEditState & { target: Extract<AccountEditTarget, { type: "debridlink" }> }): AppSettings {
|
function applyDebridLinkEdit(settings: AppSettings, state: AccountEditState & { target: Extract<AccountEditTarget, { type: "debridlink" }> }): AppSettings {
|
||||||
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "");
|
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "");
|
||||||
const index = keys.findIndex((entry) => entry.id === state.target.keyId);
|
const index = keys.findIndex((entry) => entry.id === state.target.keyId);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
throw new Error("Der ausgewählte Debrid-Link-Key wurde nicht gefunden.");
|
throw new Error("Der ausgewählte Debrid-Link-Key wurde nicht gefunden.");
|
||||||
}
|
}
|
||||||
const oldId = state.target.keyId;
|
const oldId = state.target.keyId;
|
||||||
const token = state.token.trim();
|
const token = state.token.trim();
|
||||||
const newId = getDebridLinkApiKeyId(token);
|
const newId = getDebridLinkApiKeyId(token);
|
||||||
const tokens = keys.map((entry, keyIndex) => keyIndex === index ? token : entry.token);
|
const tokens = keys.map((entry, keyIndex) => keyIndex === index ? token : entry.token);
|
||||||
const idChanged = oldId !== newId;
|
const idChanged = oldId !== newId;
|
||||||
return {
|
return {
|
||||||
...settings,
|
...settings,
|
||||||
debridLinkApiKeys: tokens.join("\n"),
|
debridLinkApiKeys: tokens.join("\n"),
|
||||||
debridLinkDisabledKeyIds: idChanged
|
debridLinkDisabledKeyIds: idChanged
|
||||||
? migrateDisabledId(settings.debridLinkDisabledKeyIds || [], oldId, newId)
|
? migrateDisabledId(settings.debridLinkDisabledKeyIds || [], oldId, newId)
|
||||||
: [...(settings.debridLinkDisabledKeyIds || [])],
|
: [...(settings.debridLinkDisabledKeyIds || [])],
|
||||||
debridLinkApiKeyDailyLimitBytes: updateTargetLimit(settings.debridLinkApiKeyDailyLimitBytes || {}, oldId, newId, state.dailyLimitGb, state.originalDailyLimitBytes),
|
debridLinkApiKeyDailyLimitBytes: updateTargetLimit(settings.debridLinkApiKeyDailyLimitBytes || {}, oldId, newId, state.dailyLimitGb, state.originalDailyLimitBytes),
|
||||||
debridLinkApiKeyDailyUsageBytes: idChanged
|
debridLinkApiKeyDailyUsageBytes: idChanged
|
||||||
? withoutRecordKeys(settings.debridLinkApiKeyDailyUsageBytes || {}, oldId, newId)
|
? withoutRecordKeys(settings.debridLinkApiKeyDailyUsageBytes || {}, oldId, newId)
|
||||||
: { ...(settings.debridLinkApiKeyDailyUsageBytes || {}) },
|
: { ...(settings.debridLinkApiKeyDailyUsageBytes || {}) },
|
||||||
debridLinkApiKeyTotalUsageBytes: idChanged
|
debridLinkApiKeyTotalUsageBytes: idChanged
|
||||||
? withoutRecordKeys(settings.debridLinkApiKeyTotalUsageBytes || {}, oldId, newId)
|
? withoutRecordKeys(settings.debridLinkApiKeyTotalUsageBytes || {}, oldId, newId)
|
||||||
: { ...(settings.debridLinkApiKeyTotalUsageBytes || {}) },
|
: { ...(settings.debridLinkApiKeyTotalUsageBytes || {}) },
|
||||||
debridAccountStatuses: idChanged
|
debridAccountStatuses: idChanged
|
||||||
? withoutRecordKeys(settings.debridAccountStatuses || {}, oldId, newId)
|
? withoutRecordKeys(settings.debridAccountStatuses || {}, oldId, newId)
|
||||||
: { ...(settings.debridAccountStatuses || {}) }
|
: { ...(settings.debridAccountStatuses || {}) }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyAccountEdit(settings: AppSettings, state: AccountEditState): AppSettings {
|
export function applyAccountEdit(settings: AppSettings, state: AccountEditState): AppSettings {
|
||||||
if (state.target.type === "single") {
|
if (state.target.type === "single") {
|
||||||
return applySingleEdit(settings, state as AccountEditState & { target: Extract<AccountEditTarget, { type: "single" }> });
|
return applySingleEdit(settings, state as AccountEditState & { target: Extract<AccountEditTarget, { type: "single" }> });
|
||||||
}
|
}
|
||||||
if (state.target.type === "mega") {
|
if (state.target.type === "mega") {
|
||||||
return applyMegaEdit(settings, state as AccountEditState & { target: Extract<AccountEditTarget, { type: "mega" }> });
|
return applyMegaEdit(settings, state as AccountEditState & { target: Extract<AccountEditTarget, { type: "mega" }> });
|
||||||
}
|
}
|
||||||
return applyDebridLinkEdit(settings, state as AccountEditState & { target: Extract<AccountEditTarget, { type: "debridlink" }> });
|
return applyDebridLinkEdit(settings, state as AccountEditState & { target: Extract<AccountEditTarget, { type: "debridlink" }> });
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearSingleAccount(settings: AppSettings, target: Extract<AccountEditTarget, { type: "single" }>): AppSettings {
|
function clearSingleAccount(settings: AppSettings, target: Extract<AccountEditTarget, { type: "single" }>): AppSettings {
|
||||||
const providerDailyLimitBytes = { ...(settings.providerDailyLimitBytes || {}) };
|
const providerDailyLimitBytes = { ...(settings.providerDailyLimitBytes || {}) };
|
||||||
const providerDailyUsageBytes = { ...(settings.providerDailyUsageBytes || {}) };
|
const providerDailyUsageBytes = { ...(settings.providerDailyUsageBytes || {}) };
|
||||||
const providerTotalUsageBytes = { ...(settings.providerTotalUsageBytes || {}) };
|
const providerTotalUsageBytes = { ...(settings.providerTotalUsageBytes || {}) };
|
||||||
delete providerDailyLimitBytes[target.provider];
|
delete providerDailyLimitBytes[target.provider];
|
||||||
delete providerDailyUsageBytes[target.provider];
|
delete providerDailyUsageBytes[target.provider];
|
||||||
delete providerTotalUsageBytes[target.provider];
|
delete providerTotalUsageBytes[target.provider];
|
||||||
const base = { ...settings, providerDailyLimitBytes, providerDailyUsageBytes, providerTotalUsageBytes };
|
const base = { ...settings, providerDailyLimitBytes, providerDailyUsageBytes, providerTotalUsageBytes };
|
||||||
switch (target.service) {
|
switch (target.service) {
|
||||||
case "realdebrid":
|
case "realdebrid":
|
||||||
return { ...base, token: "", realDebridUseWebLogin: false };
|
return { ...base, token: "", realDebridUseWebLogin: false };
|
||||||
case "bestdebrid":
|
case "bestdebrid":
|
||||||
return { ...base, bestToken: "", bestDebridUseWebLogin: false };
|
return { ...base, bestToken: "", bestDebridUseWebLogin: false };
|
||||||
case "alldebrid":
|
case "alldebrid":
|
||||||
return { ...base, allDebridToken: "", allDebridUseWebLogin: false };
|
return { ...base, allDebridToken: "", allDebridUseWebLogin: false };
|
||||||
case "ddownload":
|
case "ddownload":
|
||||||
return { ...base, ddownloadLogin: "", ddownloadPassword: "" };
|
return { ...base, ddownloadLogin: "", ddownloadPassword: "" };
|
||||||
case "onefichier":
|
case "onefichier":
|
||||||
return { ...base, oneFichierApiKey: "" };
|
return { ...base, oneFichierApiKey: "" };
|
||||||
case "linksnappy":
|
case "linksnappy":
|
||||||
return { ...base, linkSnappyLogin: "", linkSnappyPassword: "" };
|
return { ...base, linkSnappyLogin: "", linkSnappyPassword: "" };
|
||||||
default:
|
default:
|
||||||
return base;
|
return base;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeAccountTarget(settings: AppSettings, target: AccountEditTarget): AppSettings {
|
export function removeAccountTarget(settings: AppSettings, target: AccountEditTarget): AppSettings {
|
||||||
if (target.type === "single") {
|
if (target.type === "single") {
|
||||||
return clearSingleAccount(settings, target);
|
return clearSingleAccount(settings, target);
|
||||||
}
|
}
|
||||||
if (target.type === "mega") {
|
if (target.type === "mega") {
|
||||||
const accounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword || "")
|
const mode = getMegaTargetMode(target);
|
||||||
|
const selectedAccounts = getMegaDebridAccountsForMode(settings, mode)
|
||||||
.filter((entry) => entry.id !== target.accountId)
|
.filter((entry) => entry.id !== target.accountId)
|
||||||
.map((entry) => ({ login: entry.login, password: entry.password }));
|
.map((entry) => ({ login: entry.login, password: entry.password }));
|
||||||
const first = accounts[0] || { login: "", password: "" };
|
const selectedCredentials = serializeMegaDebridAccounts(selectedAccounts);
|
||||||
|
const apiCredentials = mode === "api" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "api");
|
||||||
|
const webCredentials = mode === "web" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "web");
|
||||||
|
const mergedCredentials = mergeMegaDebridCredentialPools(apiCredentials, webCredentials);
|
||||||
|
const first = parseMegaDebridAccounts(mergedCredentials)[0];
|
||||||
|
const accountStillUsed = parseMegaDebridAccounts(mode === "api" ? webCredentials : apiCredentials).some((entry) => entry.id === target.accountId);
|
||||||
|
const apiDisabledIds = (mode === "api" ? getMegaDebridDisabledAccountIdsForMode(settings, "api").filter((id) => id !== target.accountId) : getMegaDebridDisabledAccountIdsForMode(settings, "api"));
|
||||||
|
const webDisabledIds = (mode === "web" ? getMegaDebridDisabledAccountIdsForMode(settings, "web").filter((id) => id !== target.accountId) : getMegaDebridDisabledAccountIdsForMode(settings, "web"));
|
||||||
|
const keepOrRemoveMetadata = <T,>(record: Record<string, T>): Record<string, T> => accountStillUsed ? { ...record } : withoutRecordKeys(record, target.accountId);
|
||||||
return {
|
return {
|
||||||
...settings,
|
...settings,
|
||||||
megaCredentials: serializeMegaDebridAccounts(accounts),
|
megaCredentials: mergedCredentials,
|
||||||
megaLogin: first.login,
|
megaLogin: first?.login || "",
|
||||||
megaPassword: first.password,
|
megaPassword: first?.password || "",
|
||||||
megaDebridApiEnabled: accounts.length > 0 && settings.megaDebridApiEnabled,
|
megaDebridApiCredentials: apiCredentials,
|
||||||
megaDebridWebEnabled: accounts.length > 0 && settings.megaDebridWebEnabled,
|
megaDebridWebCredentials: webCredentials,
|
||||||
megaDebridDisabledAccountIds: (settings.megaDebridDisabledAccountIds || []).filter((id) => id !== target.accountId),
|
megaDebridApiEnabled: parseMegaDebridAccounts(apiCredentials).length > 0 && settings.megaDebridApiEnabled,
|
||||||
megaDebridAccountDailyLimitBytes: withoutRecordKeys(settings.megaDebridAccountDailyLimitBytes || {}, target.accountId),
|
megaDebridWebEnabled: parseMegaDebridAccounts(webCredentials).length > 0 && settings.megaDebridWebEnabled,
|
||||||
megaDebridAccountDailyUsageBytes: withoutRecordKeys(settings.megaDebridAccountDailyUsageBytes || {}, target.accountId),
|
megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])],
|
||||||
megaDebridAccountTotalUsageBytes: withoutRecordKeys(settings.megaDebridAccountTotalUsageBytes || {}, target.accountId),
|
megaDebridApiDisabledAccountIds: apiDisabledIds,
|
||||||
debridAccountStatuses: withoutRecordKeys(settings.debridAccountStatuses || {}, target.accountId)
|
megaDebridWebDisabledAccountIds: webDisabledIds,
|
||||||
};
|
megaDebridAccountDailyLimitBytes: keepOrRemoveMetadata(settings.megaDebridAccountDailyLimitBytes || {}),
|
||||||
}
|
megaDebridAccountDailyUsageBytes: keepOrRemoveMetadata(settings.megaDebridAccountDailyUsageBytes || {}),
|
||||||
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "").filter((entry) => entry.id !== target.keyId);
|
megaDebridAccountTotalUsageBytes: keepOrRemoveMetadata(settings.megaDebridAccountTotalUsageBytes || {}),
|
||||||
return {
|
debridAccountStatuses: keepOrRemoveMetadata(settings.debridAccountStatuses || {})
|
||||||
...settings,
|
};
|
||||||
debridLinkApiKeys: keys.map((entry) => entry.token).join("\n"),
|
}
|
||||||
debridLinkDisabledKeyIds: (settings.debridLinkDisabledKeyIds || []).filter((id) => id !== target.keyId),
|
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "").filter((entry) => entry.id !== target.keyId);
|
||||||
debridLinkApiKeyDailyLimitBytes: withoutRecordKeys(settings.debridLinkApiKeyDailyLimitBytes || {}, target.keyId),
|
return {
|
||||||
debridLinkApiKeyDailyUsageBytes: withoutRecordKeys(settings.debridLinkApiKeyDailyUsageBytes || {}, target.keyId),
|
...settings,
|
||||||
debridLinkApiKeyTotalUsageBytes: withoutRecordKeys(settings.debridLinkApiKeyTotalUsageBytes || {}, target.keyId),
|
debridLinkApiKeys: keys.map((entry) => entry.token).join("\n"),
|
||||||
debridAccountStatuses: withoutRecordKeys(settings.debridAccountStatuses || {}, target.keyId)
|
debridLinkDisabledKeyIds: (settings.debridLinkDisabledKeyIds || []).filter((id) => id !== target.keyId),
|
||||||
};
|
debridLinkApiKeyDailyLimitBytes: withoutRecordKeys(settings.debridLinkApiKeyDailyLimitBytes || {}, target.keyId),
|
||||||
}
|
debridLinkApiKeyDailyUsageBytes: withoutRecordKeys(settings.debridLinkApiKeyDailyUsageBytes || {}, target.keyId),
|
||||||
|
debridLinkApiKeyTotalUsageBytes: withoutRecordKeys(settings.debridLinkApiKeyTotalUsageBytes || {}, target.keyId),
|
||||||
export function buildAccountEditCheckSettings(settings: AppSettings, state: AccountEditState): AppSettings {
|
debridAccountStatuses: withoutRecordKeys(settings.debridAccountStatuses || {}, target.keyId)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildAccountEditCheckSettings(settings: AppSettings, state: AccountEditState): AppSettings {
|
||||||
if (state.target.type === "mega") {
|
if (state.target.type === "mega") {
|
||||||
const login = state.login.trim();
|
const login = state.login.trim();
|
||||||
|
const credentials = serializeMegaDebridAccounts([{ login, password: state.password }]);
|
||||||
|
const mode = getMegaTargetMode(state.target);
|
||||||
return {
|
return {
|
||||||
...settings,
|
...settings,
|
||||||
megaCredentials: serializeMegaDebridAccounts([{ login, password: state.password }]),
|
megaCredentials: credentials,
|
||||||
megaLogin: login,
|
megaLogin: login,
|
||||||
megaPassword: state.password,
|
megaPassword: state.password,
|
||||||
|
megaDebridApiCredentials: mode === "api" ? credentials : "",
|
||||||
|
megaDebridWebCredentials: mode === "web" ? credentials : "",
|
||||||
|
megaDebridApiEnabled: mode === "api",
|
||||||
|
megaDebridWebEnabled: mode === "web",
|
||||||
|
megaDebridApiDisabledAccountIds: [],
|
||||||
|
megaDebridWebDisabledAccountIds: [],
|
||||||
debridLinkApiKeys: ""
|
debridLinkApiKeys: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (state.target.type === "debridlink") {
|
if (state.target.type === "debridlink") {
|
||||||
return {
|
return {
|
||||||
...settings,
|
...settings,
|
||||||
megaCredentials: "",
|
megaCredentials: "",
|
||||||
megaLogin: "",
|
megaLogin: "",
|
||||||
megaPassword: "",
|
megaPassword: "",
|
||||||
|
megaDebridApiCredentials: "",
|
||||||
|
megaDebridWebCredentials: "",
|
||||||
debridLinkApiKeys: state.token.trim()
|
debridLinkApiKeys: state.token.trim()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...settings,
|
...settings,
|
||||||
megaCredentials: "",
|
megaCredentials: "",
|
||||||
megaLogin: "",
|
megaLogin: "",
|
||||||
megaPassword: "",
|
megaPassword: "",
|
||||||
|
megaDebridApiCredentials: "",
|
||||||
|
megaDebridWebCredentials: "",
|
||||||
debridLinkApiKeys: ""
|
debridLinkApiKeys: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateAccountEditStatuses(state: AccountEditState, statuses: readonly DebridAccountStatus[]): string | null {
|
export function validateAccountEditStatuses(state: AccountEditState, statuses: readonly DebridAccountStatus[]): string | null {
|
||||||
if (state.target.type === "single") {
|
if (state.target.type === "single") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (statuses.length === 0) {
|
if (statuses.length === 0) {
|
||||||
return "Die Prüfung hat keinen Account zurückgegeben.";
|
return "Die Prüfung hat keinen Account zurückgegeben.";
|
||||||
}
|
}
|
||||||
if (statuses.length !== 1) {
|
if (statuses.length !== 1) {
|
||||||
return "Die Prüfung hat mehr als den ausgewählten Account zurückgegeben.";
|
return "Die Prüfung hat mehr als den ausgewählten Account zurückgegeben.";
|
||||||
}
|
}
|
||||||
const expectedId = getAccountEditExpectedStatusId(state);
|
const expectedId = getAccountEditExpectedStatusId(state);
|
||||||
const status = statuses[0];
|
const status = statuses[0];
|
||||||
if (status.accountId !== expectedId) {
|
if (status.accountId !== expectedId) {
|
||||||
return "Die Prüfung hat den falschen Account zurückgegeben.";
|
return "Die Prüfung hat den falschen Account zurückgegeben.";
|
||||||
}
|
}
|
||||||
return status.valid ? null : status.message || "Zugangsdaten ungültig";
|
return status.valid ? null : status.message || "Zugangsdaten ungültig";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAccountEditExpectedStatusId(state: AccountEditState): string | null {
|
export function getAccountEditExpectedStatusId(state: AccountEditState): string | null {
|
||||||
if (state.target.type === "mega") {
|
if (state.target.type === "mega") {
|
||||||
return getMegaDebridAccountId(state.login);
|
return getMegaDebridAccountId(state.login);
|
||||||
}
|
}
|
||||||
if (state.target.type === "debridlink") {
|
if (state.target.type === "debridlink") {
|
||||||
return getDebridLinkApiKeyId(state.token);
|
return getDebridLinkApiKeyId(state.token);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { AudioStripSummary, DebridProvider } from "../shared/types";
|
import type { AudioStripSummary, DebridProvider } from "../shared/types";
|
||||||
|
import { extractHosterFromUrl } from "../shared/hoster";
|
||||||
import { hosterIconSources } from "./hoster-icons";
|
import { hosterIconSources } from "./hoster-icons";
|
||||||
|
|
||||||
export const providerLabels: Record<DebridProvider, string> = {
|
export const providerLabels: Record<DebridProvider, string> = {
|
||||||
@@ -48,13 +49,7 @@ export function formatDateTime(timestamp: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function extractHoster(url: string): string {
|
export function extractHoster(url: string): string {
|
||||||
try {
|
return extractHosterFromUrl(url);
|
||||||
const host = new URL(url).hostname.replace(/^www\./, "");
|
|
||||||
const parts = host.split(".");
|
|
||||||
return parts.length >= 2 ? parts[parts.length - 2] : host;
|
|
||||||
} catch {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatHosterLabel(hoster: string): { compact: string; title: string; iconSrc?: string } {
|
export function formatHosterLabel(hoster: string): { compact: string; title: string; iconSrc?: string } {
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import type { DownloadItem, DownloadStatus, PackageEntry } from "../shared/types";
|
import type { DownloadItem, DownloadStatus, PackageEntry } from "../shared/types";
|
||||||
|
|
||||||
const ACTIVE_PACKAGE_STATUSES = new Set<DownloadStatus>(["downloading", "validating", "integrity_check", "extracting"]);
|
const ACTIVE_PACKAGE_STATUSES = new Set<DownloadStatus>(["downloading", "validating", "integrity_check", "extracting"]);
|
||||||
|
|
||||||
|
function isPackageActive(pkg: PackageEntry, itemsById: Record<string, DownloadItem>): boolean {
|
||||||
|
return pkg.itemIds.some((id) => {
|
||||||
|
const item = itemsById[id];
|
||||||
|
return item != null && ACTIVE_PACKAGE_STATUSES.has(item.status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function reorderPackageOrderByDrop(order: string[], draggedPackageId: string, targetPackageId: string): string[] {
|
export function reorderPackageOrderByDrop(order: string[], draggedPackageId: string, targetPackageId: string): string[] {
|
||||||
const fromIndex = order.indexOf(draggedPackageId);
|
const fromIndex = order.indexOf(draggedPackageId);
|
||||||
@@ -26,36 +33,36 @@ export function sortPackageOrderByName(order: string[], packages: Record<string,
|
|||||||
return sorted;
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sortPackagesForDisplay(
|
export function sortPackagesForDisplay(
|
||||||
packages: PackageEntry[],
|
packages: PackageEntry[],
|
||||||
itemsById: Record<string, DownloadItem>,
|
itemsById: Record<string, DownloadItem>,
|
||||||
running: boolean,
|
running: boolean,
|
||||||
autoSortPackagesByProgress: boolean
|
autoSortPackagesByProgress: boolean
|
||||||
): PackageEntry[] {
|
): PackageEntry[] {
|
||||||
if (!running || !autoSortPackagesByProgress || packages.length <= 1) {
|
if (!running || !autoSortPackagesByProgress || packages.length <= 1) {
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
const active: PackageEntry[] = [];
|
const active = packages
|
||||||
const rest: PackageEntry[] = [];
|
.map((pkg, index) => ({ pkg, index }))
|
||||||
|
.filter(({ pkg }) => isPackageActive(pkg, itemsById))
|
||||||
// Float packages that have an active item to the top, but keep BOTH groups in
|
.sort((left, right) => {
|
||||||
// their original (queue) order. Earlier this sorted the active group by live
|
const leftStartedAt = left.pkg.downloadStartedAt || 0;
|
||||||
// completedRatio/downloadedBytes — which change on every progress tick (every
|
const rightStartedAt = right.pkg.downloadStartedAt || 0;
|
||||||
// 150-700ms), so active packages visibly reshuffled the whole time. A package
|
if (leftStartedAt > 0 && rightStartedAt > 0 && leftStartedAt !== rightStartedAt) {
|
||||||
// entering/leaving the active bucket is a real, discrete event (start/finish);
|
return leftStartedAt - rightStartedAt;
|
||||||
// ranking *within* the bucket by live bytes was pure jitter nobody needs.
|
}
|
||||||
for (const pkg of packages) {
|
if (leftStartedAt > 0 && rightStartedAt <= 0) return -1;
|
||||||
const hasActive = pkg.itemIds.some((id) => {
|
if (leftStartedAt <= 0 && rightStartedAt > 0) return 1;
|
||||||
const item = itemsById[id];
|
return left.index - right.index;
|
||||||
return item != null && ACTIVE_PACKAGE_STATUSES.has(item.status);
|
})
|
||||||
});
|
.map(({ pkg }) => pkg);
|
||||||
(hasActive ? active : rest).push(pkg);
|
const activeSet = new Set(active.map((pkg) => pkg.id));
|
||||||
}
|
const rest = packages.filter((pkg) => !activeSet.has(pkg.id));
|
||||||
|
|
||||||
if (active.length === 0 || active.length === packages.length) {
|
if (active.length === 0) {
|
||||||
return packages;
|
return packages;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...active, ...rest];
|
return [...active, ...rest];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,7 +155,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-update-trigger {
|
.md-update-trigger {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
min-width: 90px;
|
min-width: 90px;
|
||||||
@@ -164,8 +164,8 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: var(--ui-primary);
|
background: var(--ui-update);
|
||||||
color: var(--ui-primary-text);
|
color: var(--ui-update-text);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -173,8 +173,8 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-update-trigger:hover {
|
.md-update-trigger:hover {
|
||||||
background: var(--ui-primary-hover);
|
background: var(--ui-update-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-update-trigger:focus-visible,
|
.md-update-trigger:focus-visible,
|
||||||
@@ -308,13 +308,18 @@
|
|||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-update-release-notes pre {
|
.md-update-release-notes pre {
|
||||||
margin: 10px 0 0;
|
margin: 10px 0 0;
|
||||||
color: var(--ui-text-secondary);
|
max-height: min(360px, 45vh);
|
||||||
font: inherit;
|
overflow-y: auto;
|
||||||
line-height: 20px;
|
padding-right: 8px;
|
||||||
white-space: pre-wrap;
|
color: var(--ui-text-secondary);
|
||||||
}
|
font: inherit;
|
||||||
|
line-height: 20px;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.md-update-dialog-actions {
|
.md-update-dialog-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
--ui-primary: #D6D6D6;
|
--ui-primary: #D6D6D6;
|
||||||
--ui-primary-hover: #E6E6E6;
|
--ui-primary-hover: #E6E6E6;
|
||||||
--ui-primary-text: #181A1F;
|
--ui-primary-text: #181A1F;
|
||||||
|
--ui-update: #BAD0FC;
|
||||||
|
--ui-update-hover: #8AA5DC;
|
||||||
|
--ui-update-text: #181A1F;
|
||||||
--ui-accent: #4A4A4A;
|
--ui-accent: #4A4A4A;
|
||||||
--ui-focus: #9AB8E8;
|
--ui-focus: #9AB8E8;
|
||||||
--ui-speed-accent: #4ADE80;
|
--ui-speed-accent: #4ADE80;
|
||||||
@@ -48,6 +51,9 @@
|
|||||||
--ui-primary: #3A3A3A;
|
--ui-primary: #3A3A3A;
|
||||||
--ui-primary-hover: #202020;
|
--ui-primary-hover: #202020;
|
||||||
--ui-primary-text: #FFFFFF;
|
--ui-primary-text: #FFFFFF;
|
||||||
|
--ui-update: #BAD0FC;
|
||||||
|
--ui-update-hover: #8AA5DC;
|
||||||
|
--ui-update-text: #181A1F;
|
||||||
--ui-accent: #5E5E5E;
|
--ui-accent: #5E5E5E;
|
||||||
--ui-focus: #24558D;
|
--ui-focus: #24558D;
|
||||||
--ui-speed-accent: #1E9E55;
|
--ui-speed-accent: #1E9E55;
|
||||||
|
|||||||
@@ -139,6 +139,10 @@ export function compactDownloadStatus(value: string): string {
|
|||||||
if (/Download running\b/i.test(status)) return "Download running";
|
if (/Download running\b/i.test(status)) return "Download running";
|
||||||
if (/^Passwort gefunden\b/i.test(status)) return "Passwort gefunden";
|
if (/^Passwort gefunden\b/i.test(status)) return "Passwort gefunden";
|
||||||
if (/^Password found\b/i.test(status)) return "Password found";
|
if (/^Password found\b/i.test(status)) return "Password found";
|
||||||
|
const passwordCracking = status.match(/^(Passwort knacken|Cracking password):?\s*(\d+)%\s*(?:\((\d+\/\d+)\))?/i);
|
||||||
|
if (passwordCracking) {
|
||||||
|
return `${passwordCracking[1]}: ${passwordCracking[2]}%${passwordCracking[3] ? ` (${passwordCracking[3]})` : ""}`;
|
||||||
|
}
|
||||||
if (/^Entpack-Fehler\b/i.test(status)) return "Entpack-Fehler";
|
if (/^Entpack-Fehler\b/i.test(status)) return "Entpack-Fehler";
|
||||||
if (/^Extraction error\b/i.test(status)) return "Extraction error";
|
if (/^Extraction error\b/i.test(status)) return "Extraction error";
|
||||||
const extractionPending = status.match(/^(Entpacken|Extracting)\s*-\s*(Ausstehend|Pending|Warten auf Parts|Waiting for parts)/i);
|
const extractionPending = status.match(/^(Entpacken|Extracting)\s*-\s*(Ausstehend|Pending|Warten auf Parts|Waiting for parts)/i);
|
||||||
|
|||||||
@@ -420,8 +420,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.downloads-cell-slot > :is(.downloads-status-cell, .downloads-service-cell) {
|
.downloads-cell-slot > :is(.downloads-status-cell, .downloads-service-cell) {
|
||||||
justify-content: flex-start;
|
justify-content: center;
|
||||||
text-align: left;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.downloads-cell,
|
.downloads-cell,
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
const DOMAIN_ALIASES: Readonly<Record<string, string>> = Object.freeze({
|
||||||
|
"rapidgator.net": "rapidgator",
|
||||||
|
"rapidgator.asia": "rapidgator",
|
||||||
|
"rg.to": "rapidgator"
|
||||||
|
});
|
||||||
|
|
||||||
|
export function normalizeHosterHostname(hostname: string): string {
|
||||||
|
const normalized = hostname.trim().toLowerCase().replace(/^www\./, "").replace(/\.$/, "");
|
||||||
|
for (const [domain, hoster] of Object.entries(DOMAIN_ALIASES)) {
|
||||||
|
if (normalized === domain || normalized.endsWith(`.${domain}`)) {
|
||||||
|
return hoster;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parts = normalized.split(".").filter(Boolean);
|
||||||
|
return parts.length >= 2 ? parts[parts.length - 2] : normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractHosterFromUrl(url: string): string {
|
||||||
|
try {
|
||||||
|
return normalizeHosterHostname(new URL(url).hostname);
|
||||||
|
} catch {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,26 @@
|
|||||||
export interface MegaDebridAccountEntry {
|
export interface MegaDebridAccountEntry {
|
||||||
id: string;
|
id: string;
|
||||||
login: string;
|
login: string;
|
||||||
password: string;
|
password: string;
|
||||||
index: number;
|
index: number;
|
||||||
label: string;
|
label: string;
|
||||||
maskedLogin: string;
|
maskedLogin: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MegaDebridAccountMode = "api" | "web";
|
||||||
|
|
||||||
|
type MegaDebridModeSettings = {
|
||||||
|
megaCredentials?: string;
|
||||||
|
megaPassword?: string;
|
||||||
|
megaDebridApiCredentials?: string;
|
||||||
|
megaDebridWebCredentials?: string;
|
||||||
|
megaDebridApiEnabled?: boolean;
|
||||||
|
megaDebridWebEnabled?: boolean;
|
||||||
|
megaDebridPreferApi?: boolean;
|
||||||
|
megaDebridDisabledAccountIds?: string[];
|
||||||
|
megaDebridApiDisabledAccountIds?: string[];
|
||||||
|
megaDebridWebDisabledAccountIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
const FNV64_OFFSET_BASIS = 0xcbf29ce484222325n;
|
const FNV64_OFFSET_BASIS = 0xcbf29ce484222325n;
|
||||||
const FNV64_PRIME = 0x100000001b3n;
|
const FNV64_PRIME = 0x100000001b3n;
|
||||||
@@ -85,6 +100,52 @@ export function serializeMegaDebridAccounts(accounts: { login: string; password:
|
|||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMegaDebridAccountIds(raw: string, legacyPassword = ""): string[] {
|
export function getMegaDebridAccountIds(raw: string, legacyPassword = ""): string[] {
|
||||||
return parseMegaDebridAccounts(raw, legacyPassword).map((entry) => entry.id);
|
return parseMegaDebridAccounts(raw, legacyPassword).map((entry) => entry.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMegaDebridCredentialsForMode(settings: MegaDebridModeSettings, mode: MegaDebridAccountMode): string {
|
||||||
|
const dedicated = mode === "api" ? settings.megaDebridApiCredentials : settings.megaDebridWebCredentials;
|
||||||
|
const otherDedicated = mode === "api" ? settings.megaDebridWebCredentials : settings.megaDebridApiCredentials;
|
||||||
|
if (String(dedicated || "").trim() || String(otherDedicated || "").trim()) {
|
||||||
|
return String(dedicated || "").trim();
|
||||||
|
}
|
||||||
|
const legacy = String(settings.megaCredentials || "").trim();
|
||||||
|
if (!legacy) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const apiEnabled = Boolean(settings.megaDebridApiEnabled);
|
||||||
|
const webEnabled = Boolean(settings.megaDebridWebEnabled);
|
||||||
|
const preferredMode: MegaDebridAccountMode = settings.megaDebridPreferApi === false ? "web" : "api";
|
||||||
|
if (apiEnabled !== webEnabled) {
|
||||||
|
return mode === (apiEnabled ? "api" : "web") ? legacy : "";
|
||||||
|
}
|
||||||
|
return mode === preferredMode ? legacy : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMegaDebridAccountsForMode(settings: MegaDebridModeSettings, mode: MegaDebridAccountMode): MegaDebridAccountEntry[] {
|
||||||
|
return parseMegaDebridAccounts(getMegaDebridCredentialsForMode(settings, mode), settings.megaPassword || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMegaDebridDisabledAccountIdsForMode(settings: MegaDebridModeSettings, mode: MegaDebridAccountMode): string[] {
|
||||||
|
const dedicated = mode === "api" ? settings.megaDebridApiDisabledAccountIds : settings.megaDebridWebDisabledAccountIds;
|
||||||
|
const hasDedicatedCredentials = Boolean(
|
||||||
|
String(settings.megaDebridApiCredentials || "").trim()
|
||||||
|
|| String(settings.megaDebridWebCredentials || "").trim()
|
||||||
|
);
|
||||||
|
if ((hasDedicatedCredentials || !getMegaDebridCredentialsForMode(settings, mode)) && Array.isArray(dedicated)) {
|
||||||
|
return [...dedicated];
|
||||||
|
}
|
||||||
|
return Array.isArray(settings.megaDebridDisabledAccountIds) ? [...settings.megaDebridDisabledAccountIds] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mergeMegaDebridCredentialPools(apiCredentials: string, webCredentials: string): string {
|
||||||
|
const merged = new Map<string, { login: string; password: string }>();
|
||||||
|
for (const entry of [...parseMegaDebridAccounts(apiCredentials), ...parseMegaDebridAccounts(webCredentials)]) {
|
||||||
|
const key = entry.login.trim().toLowerCase();
|
||||||
|
if (!merged.has(key)) {
|
||||||
|
merged.set(key, { login: entry.login, password: entry.password });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return serializeMegaDebridAccounts([...merged.values()]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { AppSettings, DebridProvider } from "./types";
|
import type { AppSettings, DebridProvider } from "./types";
|
||||||
|
import { getMegaDebridDisabledAccountIdsForMode, type MegaDebridAccountMode } from "./mega-debrid-accounts";
|
||||||
|
|
||||||
export type ProviderByteMap = Partial<Record<DebridProvider, number>>;
|
export type ProviderByteMap = Partial<Record<DebridProvider, number>>;
|
||||||
export type DebridLinkKeyByteMap = Record<string, number>;
|
export type DebridLinkKeyByteMap = Record<string, number>;
|
||||||
@@ -6,7 +7,7 @@ export type DebridLinkKeyByteMap = Record<string, number>;
|
|||||||
type ProviderDailySettings =
|
type ProviderDailySettings =
|
||||||
Pick<AppSettings, "providerDailyLimitBytes" | "providerDailyUsageBytes" | "providerDailyUsageDay">
|
Pick<AppSettings, "providerDailyLimitBytes" | "providerDailyUsageBytes" | "providerDailyUsageDay">
|
||||||
& Partial<Pick<AppSettings, "debridLinkApiKeyDailyLimitBytes" | "debridLinkApiKeyDailyUsageBytes">>
|
& Partial<Pick<AppSettings, "debridLinkApiKeyDailyLimitBytes" | "debridLinkApiKeyDailyUsageBytes">>
|
||||||
& Partial<Pick<AppSettings, "megaDebridDisabledAccountIds" | "megaDebridAccountDailyLimitBytes" | "megaDebridAccountDailyUsageBytes">>;
|
& Partial<Pick<AppSettings, "megaDebridDisabledAccountIds" | "megaDebridApiDisabledAccountIds" | "megaDebridWebDisabledAccountIds" | "megaDebridAccountDailyLimitBytes" | "megaDebridAccountDailyUsageBytes">>;
|
||||||
|
|
||||||
type ProviderUsageSettings =
|
type ProviderUsageSettings =
|
||||||
ProviderDailySettings
|
ProviderDailySettings
|
||||||
@@ -250,9 +251,12 @@ export function addDebridLinkApiKeyTotalUsageBytes(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMegaDebridAccountDisabled(settings: ProviderDailySettings, accountId: string): boolean {
|
export function isMegaDebridAccountDisabled(settings: ProviderDailySettings, accountId: string, mode?: MegaDebridAccountMode): boolean {
|
||||||
return Array.isArray(settings.megaDebridDisabledAccountIds) && settings.megaDebridDisabledAccountIds.includes(accountId);
|
if (mode) {
|
||||||
}
|
return getMegaDebridDisabledAccountIdsForMode(settings, mode).includes(accountId);
|
||||||
|
}
|
||||||
|
return Array.isArray(settings.megaDebridDisabledAccountIds) && settings.megaDebridDisabledAccountIds.includes(accountId);
|
||||||
|
}
|
||||||
|
|
||||||
export function getMegaDebridAccountDailyLimitBytes(settings: ProviderDailySettings, accountId: string): number {
|
export function getMegaDebridAccountDailyLimitBytes(settings: ProviderDailySettings, accountId: string): number {
|
||||||
return normalizePositiveBytes(settings.megaDebridAccountDailyLimitBytes?.[accountId]);
|
return normalizePositiveBytes(settings.megaDebridAccountDailyLimitBytes?.[accountId]);
|
||||||
|
|||||||
+9
-5
@@ -71,10 +71,12 @@ export interface AppSettings {
|
|||||||
language: AppLanguage;
|
language: AppLanguage;
|
||||||
token: string;
|
token: string;
|
||||||
realDebridUseWebLogin: boolean;
|
realDebridUseWebLogin: boolean;
|
||||||
megaLogin: string;
|
megaLogin: string;
|
||||||
megaPassword: string;
|
megaPassword: string;
|
||||||
megaCredentials: string;
|
megaCredentials: string;
|
||||||
megaDebridApiEnabled: boolean;
|
megaDebridApiCredentials: string;
|
||||||
|
megaDebridWebCredentials: string;
|
||||||
|
megaDebridApiEnabled: boolean;
|
||||||
megaDebridWebEnabled: boolean;
|
megaDebridWebEnabled: boolean;
|
||||||
megaDebridPreferApi: boolean;
|
megaDebridPreferApi: boolean;
|
||||||
bestToken: string;
|
bestToken: string;
|
||||||
@@ -158,7 +160,9 @@ export interface AppSettings {
|
|||||||
debridLinkApiKeyDailyLimitBytes: Record<string, number>;
|
debridLinkApiKeyDailyLimitBytes: Record<string, number>;
|
||||||
debridLinkApiKeyDailyUsageBytes: Record<string, number>;
|
debridLinkApiKeyDailyUsageBytes: Record<string, number>;
|
||||||
debridLinkApiKeyTotalUsageBytes: Record<string, number>;
|
debridLinkApiKeyTotalUsageBytes: Record<string, number>;
|
||||||
megaDebridDisabledAccountIds: string[];
|
megaDebridDisabledAccountIds: string[];
|
||||||
|
megaDebridApiDisabledAccountIds: string[];
|
||||||
|
megaDebridWebDisabledAccountIds: string[];
|
||||||
megaDebridAccountDailyLimitBytes: Record<string, number>;
|
megaDebridAccountDailyLimitBytes: Record<string, number>;
|
||||||
megaDebridAccountDailyUsageBytes: Record<string, number>;
|
megaDebridAccountDailyUsageBytes: Record<string, number>;
|
||||||
megaDebridAccountTotalUsageBytes: Record<string, number>;
|
megaDebridAccountTotalUsageBytes: Record<string, number>;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { applyAccountDialogToSettings, AccountDialogState } from "../src/renderer/App";
|
import { applyAccountDialogToSettings, createAccountDialogState, AccountDialogState } from "../src/renderer/App";
|
||||||
import { defaultSettings } from "../src/main/constants";
|
import { defaultSettings } from "../src/main/constants";
|
||||||
|
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||||
|
import { isMegaDebridAccountDisabled } from "../src/shared/provider-daily-limits";
|
||||||
|
|
||||||
function megaDialog(kind: "megadebrid-api" | "megadebrid-web"): AccountDialogState {
|
function megaDialog(kind: "megadebrid-api" | "megadebrid-web"): AccountDialogState {
|
||||||
return {
|
return {
|
||||||
@@ -27,10 +29,53 @@ describe("applyAccountDialogToSettings — keeps the user's Mega preferApi choic
|
|||||||
expect(next.megaDebridPreferApi).toBe(false);
|
expect(next.megaDebridPreferApi).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not flip megaDebridPreferApi to false when editing the Web account", () => {
|
it("does not flip megaDebridPreferApi to false when editing the Web account", () => {
|
||||||
const settings = { ...defaultSettings(), megaDebridApiEnabled: true, megaDebridWebEnabled: true, megaDebridPreferApi: true };
|
const settings = { ...defaultSettings(), megaDebridApiEnabled: true, megaDebridWebEnabled: true, megaDebridPreferApi: true };
|
||||||
const next = applyAccountDialogToSettings(settings, megaDialog("megadebrid-web"));
|
const next = applyAccountDialogToSettings(settings, megaDialog("megadebrid-web"));
|
||||||
expect(next.megaDebridWebEnabled).toBe(true);
|
expect(next.megaDebridWebEnabled).toBe(true);
|
||||||
expect(next.megaDebridPreferApi).toBe(true);
|
expect(next.megaDebridPreferApi).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
it("adds a Web account without copying it into the API account pool", () => {
|
||||||
|
const settings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
megaCredentials: "api@example.test:api-pass",
|
||||||
|
megaDebridApiCredentials: "api@example.test:api-pass",
|
||||||
|
megaDebridWebCredentials: "",
|
||||||
|
megaDebridApiEnabled: true,
|
||||||
|
megaDebridWebEnabled: false
|
||||||
|
};
|
||||||
|
const dialog = createAccountDialogState("create", "megadebrid-web", settings);
|
||||||
|
|
||||||
|
expect(dialog.megaAccounts).toEqual([]);
|
||||||
|
|
||||||
|
const next = applyAccountDialogToSettings(settings, {
|
||||||
|
...dialog,
|
||||||
|
megaAccounts: [{ login: "web@example.test", password: "web-pass" }]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(next.megaDebridApiCredentials).toBe("api@example.test:api-pass");
|
||||||
|
expect(next.megaDebridWebCredentials).toBe("web@example.test:web-pass");
|
||||||
|
expect(next.megaDebridApiEnabled).toBe(true);
|
||||||
|
expect(next.megaDebridWebEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables an API account without disabling the matching Web account", () => {
|
||||||
|
const accountId = getMegaDebridAccountId("shared@example.test");
|
||||||
|
const settings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
megaCredentials: "shared@example.test:api-pass",
|
||||||
|
megaDebridApiCredentials: "shared@example.test:api-pass",
|
||||||
|
megaDebridWebCredentials: "shared@example.test:web-pass",
|
||||||
|
megaDebridApiEnabled: true,
|
||||||
|
megaDebridWebEnabled: true
|
||||||
|
};
|
||||||
|
const next = applyAccountDialogToSettings(settings, {
|
||||||
|
...createAccountDialogState("edit", "megadebrid-api", settings),
|
||||||
|
megaDisabledIds: [accountId]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(isMegaDebridAccountDisabled(next, accountId, "api")).toBe(true);
|
||||||
|
expect(isMegaDebridAccountDisabled(next, accountId, "web")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+301
-294
@@ -1,298 +1,305 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { defaultSettings } from "../src/main/constants";
|
import { defaultSettings } from "../src/main/constants";
|
||||||
import {
|
import {
|
||||||
applyAccountEdit,
|
applyAccountEdit,
|
||||||
buildAccountEditCheckSettings,
|
buildAccountEditCheckSettings,
|
||||||
createAccountEditState,
|
createAccountEditState,
|
||||||
removeAccountTarget,
|
removeAccountTarget,
|
||||||
validateAccountEdit,
|
validateAccountEdit,
|
||||||
validateAccountEditStatuses,
|
validateAccountEditStatuses,
|
||||||
type AccountEditTarget
|
type AccountEditTarget
|
||||||
} from "../src/renderer/account-edit";
|
} from "../src/renderer/account-edit";
|
||||||
import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys";
|
import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys";
|
||||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||||
|
|
||||||
const GIB = 1024 * 1024 * 1024;
|
const GIB = 1024 * 1024 * 1024;
|
||||||
|
|
||||||
function megaTarget(login: string): AccountEditTarget {
|
function megaTarget(login: string): AccountEditTarget {
|
||||||
return {
|
return {
|
||||||
type: "mega",
|
type: "mega",
|
||||||
rowKey: `mega-${getMegaDebridAccountId(login)}`,
|
rowKey: `mega-${getMegaDebridAccountId(login)}`,
|
||||||
kind: "megadebrid-api",
|
kind: "megadebrid-api",
|
||||||
service: "megadebrid-api",
|
service: "megadebrid-api",
|
||||||
accountId: getMegaDebridAccountId(login)
|
accountId: getMegaDebridAccountId(login)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function debridLinkTarget(token: string): AccountEditTarget {
|
function debridLinkTarget(token: string): AccountEditTarget {
|
||||||
return {
|
return {
|
||||||
type: "debridlink",
|
type: "debridlink",
|
||||||
rowKey: `dl-${getDebridLinkApiKeyId(token)}`,
|
rowKey: `dl-${getDebridLinkApiKeyId(token)}`,
|
||||||
kind: "debridlink-api",
|
kind: "debridlink-api",
|
||||||
service: "debridlink",
|
service: "debridlink",
|
||||||
keyId: getDebridLinkApiKeyId(token)
|
keyId: getDebridLinkApiKeyId(token)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("account-specific editing", () => {
|
describe("account-specific editing", () => {
|
||||||
it("changes only the selected Mega-Debrid account and preserves sibling order and mode settings", () => {
|
it("changes only the selected Mega-Debrid account and preserves sibling order and mode settings", () => {
|
||||||
const oldId = getMegaDebridAccountId("second@example.com");
|
const oldId = getMegaDebridAccountId("second@example.com");
|
||||||
const newId = getMegaDebridAccountId("renamed@example.com");
|
const newId = getMegaDebridAccountId("renamed@example.com");
|
||||||
const firstId = getMegaDebridAccountId("first@example.com");
|
const firstId = getMegaDebridAccountId("first@example.com");
|
||||||
|
const webId = getMegaDebridAccountId("web@example.com");
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass\nthird@example.com:third-pass",
|
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass\nthird@example.com:third-pass\nweb@example.com:web-pass",
|
||||||
megaLogin: "first@example.com",
|
megaDebridApiCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass\nthird@example.com:third-pass",
|
||||||
megaPassword: "first-pass",
|
megaDebridWebCredentials: "web@example.com:web-pass",
|
||||||
megaDebridApiEnabled: true,
|
megaLogin: "first@example.com",
|
||||||
megaDebridWebEnabled: true,
|
megaPassword: "first-pass",
|
||||||
megaDebridPreferApi: false,
|
megaDebridApiEnabled: true,
|
||||||
megaDebridDisabledAccountIds: [oldId, firstId],
|
megaDebridWebEnabled: true,
|
||||||
megaDebridAccountDailyLimitBytes: { [oldId]: 15 * GIB, [firstId]: 9 * GIB },
|
megaDebridPreferApi: false,
|
||||||
megaDebridAccountDailyUsageBytes: { [oldId]: 4 * GIB, [firstId]: 2 * GIB },
|
megaDebridDisabledAccountIds: [oldId, firstId, webId],
|
||||||
megaDebridAccountTotalUsageBytes: { [oldId]: 40 * GIB, [firstId]: 20 * GIB },
|
megaDebridApiDisabledAccountIds: [oldId, firstId],
|
||||||
debridAccountStatuses: {
|
megaDebridWebDisabledAccountIds: [webId],
|
||||||
[oldId]: { accountId: oldId, provider: "megadebrid" as const, label: "Account 2", maskedLogin: "se***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 },
|
megaDebridAccountDailyLimitBytes: { [oldId]: 15 * GIB, [firstId]: 9 * GIB },
|
||||||
[firstId]: { accountId: firstId, provider: "megadebrid" as const, label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
|
megaDebridAccountDailyUsageBytes: { [oldId]: 4 * GIB, [firstId]: 2 * GIB },
|
||||||
}
|
megaDebridAccountTotalUsageBytes: { [oldId]: 40 * GIB, [firstId]: 20 * GIB },
|
||||||
};
|
debridAccountStatuses: {
|
||||||
const state = {
|
[oldId]: { accountId: oldId, provider: "megadebrid" as const, label: "Account 2", maskedLogin: "se***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 },
|
||||||
...createAccountEditState(megaTarget("second@example.com"), settings),
|
[firstId]: { accountId: firstId, provider: "megadebrid" as const, label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
|
||||||
login: "renamed@example.com",
|
}
|
||||||
password: "new-pass",
|
};
|
||||||
dailyLimitGb: "25,5"
|
const state = {
|
||||||
};
|
...createAccountEditState(megaTarget("second@example.com"), settings),
|
||||||
|
login: "renamed@example.com",
|
||||||
expect(validateAccountEdit(state, settings)).toBeNull();
|
password: "new-pass",
|
||||||
const next = applyAccountEdit(settings, state);
|
dailyLimitGb: "25,5"
|
||||||
|
};
|
||||||
expect(next.megaCredentials).toBe("first@example.com:first-pass\nrenamed@example.com:new-pass\nthird@example.com:third-pass");
|
|
||||||
expect(next.megaLogin).toBe("first@example.com");
|
expect(validateAccountEdit(state, settings)).toBeNull();
|
||||||
expect(next.megaPassword).toBe("first-pass");
|
const next = applyAccountEdit(settings, state);
|
||||||
expect(next.megaDebridApiEnabled).toBe(true);
|
|
||||||
expect(next.megaDebridWebEnabled).toBe(true);
|
expect(next.megaCredentials).toBe("first@example.com:first-pass\nrenamed@example.com:new-pass\nthird@example.com:third-pass\nweb@example.com:web-pass");
|
||||||
expect(next.megaDebridPreferApi).toBe(false);
|
expect(next.megaDebridApiCredentials).toBe("first@example.com:first-pass\nrenamed@example.com:new-pass\nthird@example.com:third-pass");
|
||||||
expect(next.megaDebridDisabledAccountIds).toEqual([firstId, newId]);
|
expect(next.megaDebridWebCredentials).toBe("web@example.com:web-pass");
|
||||||
expect(next.megaDebridAccountDailyLimitBytes).toEqual({ [firstId]: 9 * GIB, [newId]: Math.floor(25.5 * GIB) });
|
expect(next.megaLogin).toBe("first@example.com");
|
||||||
expect(next.megaDebridAccountDailyUsageBytes).toEqual({ [firstId]: 2 * GIB });
|
expect(next.megaPassword).toBe("first-pass");
|
||||||
expect(next.megaDebridAccountTotalUsageBytes).toEqual({ [firstId]: 20 * GIB });
|
expect(next.megaDebridApiEnabled).toBe(true);
|
||||||
expect(next.debridAccountStatuses).toEqual({
|
expect(next.megaDebridWebEnabled).toBe(true);
|
||||||
[firstId]: { accountId: firstId, provider: "megadebrid", label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
|
expect(next.megaDebridPreferApi).toBe(false);
|
||||||
});
|
expect(next.megaDebridDisabledAccountIds).toEqual([firstId, newId, webId]);
|
||||||
});
|
expect(next.megaDebridAccountDailyLimitBytes).toEqual({ [firstId]: 9 * GIB, [newId]: Math.floor(25.5 * GIB) });
|
||||||
|
expect(next.megaDebridAccountDailyUsageBytes).toEqual({ [firstId]: 2 * GIB });
|
||||||
it("keeps Mega-Debrid usage when only the password changes", () => {
|
expect(next.megaDebridAccountTotalUsageBytes).toEqual({ [firstId]: 20 * GIB });
|
||||||
const id = getMegaDebridAccountId("user@example.com");
|
expect(next.debridAccountStatuses).toEqual({
|
||||||
const settings = {
|
[firstId]: { accountId: firstId, provider: "megadebrid", label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
|
||||||
...defaultSettings(),
|
});
|
||||||
megaCredentials: "user@example.com:old-pass",
|
});
|
||||||
megaLogin: "user@example.com",
|
|
||||||
megaPassword: "old-pass",
|
it("keeps Mega-Debrid usage when only the password changes", () => {
|
||||||
megaDebridAccountDailyLimitBytes: { [id]: 8 * GIB },
|
const id = getMegaDebridAccountId("user@example.com");
|
||||||
megaDebridAccountDailyUsageBytes: { [id]: 3 * GIB },
|
const settings = {
|
||||||
megaDebridAccountTotalUsageBytes: { [id]: 33 * GIB }
|
...defaultSettings(),
|
||||||
};
|
megaCredentials: "user@example.com:old-pass",
|
||||||
const state = { ...createAccountEditState(megaTarget("user@example.com"), settings), password: "new-pass" };
|
megaLogin: "user@example.com",
|
||||||
const next = applyAccountEdit(settings, state);
|
megaPassword: "old-pass",
|
||||||
|
megaDebridAccountDailyLimitBytes: { [id]: 8 * GIB },
|
||||||
expect(next.megaCredentials).toBe("user@example.com:new-pass");
|
megaDebridAccountDailyUsageBytes: { [id]: 3 * GIB },
|
||||||
expect(next.megaDebridAccountDailyUsageBytes).toEqual({ [id]: 3 * GIB });
|
megaDebridAccountTotalUsageBytes: { [id]: 33 * GIB }
|
||||||
expect(next.megaDebridAccountTotalUsageBytes).toEqual({ [id]: 33 * GIB });
|
};
|
||||||
});
|
const state = { ...createAccountEditState(megaTarget("user@example.com"), settings), password: "new-pass" };
|
||||||
|
const next = applyAccountEdit(settings, state);
|
||||||
it("preserves exact account limits when the rounded display value is left unchanged", () => {
|
|
||||||
const megaLogin = "user@example.com";
|
expect(next.megaCredentials).toBe("user@example.com:new-pass");
|
||||||
const megaId = getMegaDebridAccountId(megaLogin);
|
expect(next.megaDebridAccountDailyUsageBytes).toEqual({ [id]: 3 * GIB });
|
||||||
const key = "debrid-link-token";
|
expect(next.megaDebridAccountTotalUsageBytes).toEqual({ [id]: 33 * GIB });
|
||||||
const keyId = getDebridLinkApiKeyId(key);
|
});
|
||||||
const exactLimit = Math.floor(10.05 * GIB);
|
|
||||||
const settings = {
|
it("preserves exact account limits when the rounded display value is left unchanged", () => {
|
||||||
...defaultSettings(),
|
const megaLogin = "user@example.com";
|
||||||
megaCredentials: `${megaLogin}:pass`,
|
const megaId = getMegaDebridAccountId(megaLogin);
|
||||||
megaLogin,
|
const key = "debrid-link-token";
|
||||||
megaPassword: "pass",
|
const keyId = getDebridLinkApiKeyId(key);
|
||||||
megaDebridAccountDailyLimitBytes: { [megaId]: exactLimit },
|
const exactLimit = Math.floor(10.05 * GIB);
|
||||||
debridLinkApiKeys: key,
|
const settings = {
|
||||||
debridLinkApiKeyDailyLimitBytes: { [keyId]: exactLimit }
|
...defaultSettings(),
|
||||||
};
|
megaCredentials: `${megaLogin}:pass`,
|
||||||
|
megaLogin,
|
||||||
const megaNext = applyAccountEdit(settings, createAccountEditState(megaTarget(megaLogin), settings));
|
megaPassword: "pass",
|
||||||
const debridLinkNext = applyAccountEdit(settings, createAccountEditState(debridLinkTarget(key), settings));
|
megaDebridAccountDailyLimitBytes: { [megaId]: exactLimit },
|
||||||
|
debridLinkApiKeys: key,
|
||||||
expect(megaNext.megaDebridAccountDailyLimitBytes[megaId]).toBe(exactLimit);
|
debridLinkApiKeyDailyLimitBytes: { [keyId]: exactLimit }
|
||||||
expect(debridLinkNext.debridLinkApiKeyDailyLimitBytes[keyId]).toBe(exactLimit);
|
};
|
||||||
});
|
|
||||||
|
const megaNext = applyAccountEdit(settings, createAccountEditState(megaTarget(megaLogin), settings));
|
||||||
it("rejects whitespace-only Mega-Debrid passwords and empty or mismatched check results", () => {
|
const debridLinkNext = applyAccountEdit(settings, createAccountEditState(debridLinkTarget(key), settings));
|
||||||
const login = "user@example.com";
|
|
||||||
const settings = {
|
expect(megaNext.megaDebridAccountDailyLimitBytes[megaId]).toBe(exactLimit);
|
||||||
...defaultSettings(),
|
expect(debridLinkNext.debridLinkApiKeyDailyLimitBytes[keyId]).toBe(exactLimit);
|
||||||
megaCredentials: `${login}:pass`,
|
});
|
||||||
megaLogin: login,
|
|
||||||
megaPassword: "pass"
|
it("rejects whitespace-only Mega-Debrid passwords and empty or mismatched check results", () => {
|
||||||
};
|
const login = "user@example.com";
|
||||||
const state = { ...createAccountEditState(megaTarget(login), settings), password: " " };
|
const settings = {
|
||||||
|
...defaultSettings(),
|
||||||
expect(validateAccountEdit(state, settings)).toMatch(/Passwort/i);
|
megaCredentials: `${login}:pass`,
|
||||||
expect(validateAccountEditStatuses(state, [])).toMatch(/keinen Account/i);
|
megaLogin: login,
|
||||||
expect(validateAccountEditStatuses(state, [{
|
megaPassword: "pass"
|
||||||
accountId: "mda_other",
|
};
|
||||||
provider: "megadebrid",
|
const state = { ...createAccountEditState(megaTarget(login), settings), password: " " };
|
||||||
label: "Account 1",
|
|
||||||
maskedLogin: "ot***er",
|
expect(validateAccountEdit(state, settings)).toMatch(/Passwort/i);
|
||||||
valid: true,
|
expect(validateAccountEditStatuses(state, [])).toMatch(/keinen Account/i);
|
||||||
isPremium: true,
|
expect(validateAccountEditStatuses(state, [{
|
||||||
premiumUntilMs: null,
|
accountId: "mda_other",
|
||||||
message: "OK",
|
provider: "megadebrid",
|
||||||
checkedAt: 1
|
label: "Account 1",
|
||||||
}])).toMatch(/falschen Account/i);
|
maskedLogin: "ot***er",
|
||||||
});
|
valid: true,
|
||||||
|
isPremium: true,
|
||||||
it("rejects duplicate Mega-Debrid logins and missing row targets", () => {
|
premiumUntilMs: null,
|
||||||
const settings = {
|
message: "OK",
|
||||||
...defaultSettings(),
|
checkedAt: 1
|
||||||
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass"
|
}])).toMatch(/falschen Account/i);
|
||||||
};
|
});
|
||||||
const duplicate = {
|
|
||||||
...createAccountEditState(megaTarget("second@example.com"), settings),
|
it("rejects duplicate Mega-Debrid logins and missing row targets", () => {
|
||||||
login: "first@example.com"
|
const settings = {
|
||||||
};
|
...defaultSettings(),
|
||||||
|
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass"
|
||||||
expect(validateAccountEdit(duplicate, settings)).toMatch(/bereits vorhanden/i);
|
};
|
||||||
expect(() => createAccountEditState(megaTarget("missing@example.com"), settings)).toThrow(/nicht gefunden/i);
|
const duplicate = {
|
||||||
});
|
...createAccountEditState(megaTarget("second@example.com"), settings),
|
||||||
|
login: "first@example.com"
|
||||||
it("replaces only the selected Debrid-Link key and migrates its own metadata", () => {
|
};
|
||||||
const keyA = "token-a";
|
|
||||||
const keyB = "token-b";
|
expect(validateAccountEdit(duplicate, settings)).toMatch(/bereits vorhanden/i);
|
||||||
const keyC = "token-c";
|
expect(() => createAccountEditState(megaTarget("missing@example.com"), settings)).toThrow(/nicht gefunden/i);
|
||||||
const newKey = "token-b-new";
|
});
|
||||||
const idA = getDebridLinkApiKeyId(keyA);
|
|
||||||
const idB = getDebridLinkApiKeyId(keyB);
|
it("replaces only the selected Debrid-Link key and migrates its own metadata", () => {
|
||||||
const idNew = getDebridLinkApiKeyId(newKey);
|
const keyA = "token-a";
|
||||||
const settings = {
|
const keyB = "token-b";
|
||||||
...defaultSettings(),
|
const keyC = "token-c";
|
||||||
debridLinkApiKeys: `${keyA}\n${keyB}\n${keyC}`,
|
const newKey = "token-b-new";
|
||||||
debridLinkDisabledKeyIds: [idB, idA],
|
const idA = getDebridLinkApiKeyId(keyA);
|
||||||
debridLinkApiKeyDailyLimitBytes: { [idA]: 5 * GIB, [idB]: 10 * GIB },
|
const idB = getDebridLinkApiKeyId(keyB);
|
||||||
debridLinkApiKeyDailyUsageBytes: { [idA]: 2 * GIB, [idB]: 4 * GIB },
|
const idNew = getDebridLinkApiKeyId(newKey);
|
||||||
debridLinkApiKeyTotalUsageBytes: { [idA]: 12 * GIB, [idB]: 24 * GIB }
|
const settings = {
|
||||||
};
|
...defaultSettings(),
|
||||||
const state = {
|
debridLinkApiKeys: `${keyA}\n${keyB}\n${keyC}`,
|
||||||
...createAccountEditState(debridLinkTarget(keyB), settings),
|
debridLinkDisabledKeyIds: [idB, idA],
|
||||||
token: newKey,
|
debridLinkApiKeyDailyLimitBytes: { [idA]: 5 * GIB, [idB]: 10 * GIB },
|
||||||
dailyLimitGb: "12"
|
debridLinkApiKeyDailyUsageBytes: { [idA]: 2 * GIB, [idB]: 4 * GIB },
|
||||||
};
|
debridLinkApiKeyTotalUsageBytes: { [idA]: 12 * GIB, [idB]: 24 * GIB }
|
||||||
const next = applyAccountEdit(settings, state);
|
};
|
||||||
|
const state = {
|
||||||
expect(next.debridLinkApiKeys).toBe(`${keyA}\n${newKey}\n${keyC}`);
|
...createAccountEditState(debridLinkTarget(keyB), settings),
|
||||||
expect(next.debridLinkDisabledKeyIds).toEqual([idA, idNew]);
|
token: newKey,
|
||||||
expect(next.debridLinkApiKeyDailyLimitBytes).toEqual({ [idA]: 5 * GIB, [idNew]: 12 * GIB });
|
dailyLimitGb: "12"
|
||||||
expect(next.debridLinkApiKeyDailyUsageBytes).toEqual({ [idA]: 2 * GIB });
|
};
|
||||||
expect(next.debridLinkApiKeyTotalUsageBytes).toEqual({ [idA]: 12 * GIB });
|
const next = applyAccountEdit(settings, state);
|
||||||
});
|
|
||||||
|
expect(next.debridLinkApiKeys).toBe(`${keyA}\n${newKey}\n${keyC}`);
|
||||||
it("edits a single login without changing unrelated credentials", () => {
|
expect(next.debridLinkDisabledKeyIds).toEqual([idA, idNew]);
|
||||||
const settings = {
|
expect(next.debridLinkApiKeyDailyLimitBytes).toEqual({ [idA]: 5 * GIB, [idNew]: 12 * GIB });
|
||||||
...defaultSettings(),
|
expect(next.debridLinkApiKeyDailyUsageBytes).toEqual({ [idA]: 2 * GIB });
|
||||||
ddownloadLogin: "old@example.com",
|
expect(next.debridLinkApiKeyTotalUsageBytes).toEqual({ [idA]: 12 * GIB });
|
||||||
ddownloadPassword: "old-pass",
|
});
|
||||||
linkSnappyLogin: "keep@example.com",
|
|
||||||
linkSnappyPassword: "keep-pass"
|
it("edits a single login without changing unrelated credentials", () => {
|
||||||
};
|
const settings = {
|
||||||
const target: AccountEditTarget = {
|
...defaultSettings(),
|
||||||
type: "single",
|
ddownloadLogin: "old@example.com",
|
||||||
rowKey: "svc-ddownload",
|
ddownloadPassword: "old-pass",
|
||||||
kind: "ddownload-login",
|
linkSnappyLogin: "keep@example.com",
|
||||||
service: "ddownload",
|
linkSnappyPassword: "keep-pass"
|
||||||
provider: "ddownload"
|
};
|
||||||
};
|
const target: AccountEditTarget = {
|
||||||
const state = {
|
type: "single",
|
||||||
...createAccountEditState(target, settings),
|
rowKey: "svc-ddownload",
|
||||||
login: "new@example.com",
|
kind: "ddownload-login",
|
||||||
password: "new-pass",
|
service: "ddownload",
|
||||||
dailyLimitGb: "7"
|
provider: "ddownload"
|
||||||
};
|
};
|
||||||
const next = applyAccountEdit(settings, state);
|
const state = {
|
||||||
|
...createAccountEditState(target, settings),
|
||||||
expect(next.ddownloadLogin).toBe("new@example.com");
|
login: "new@example.com",
|
||||||
expect(next.ddownloadPassword).toBe("new-pass");
|
password: "new-pass",
|
||||||
expect(next.linkSnappyLogin).toBe("keep@example.com");
|
dailyLimitGb: "7"
|
||||||
expect(next.linkSnappyPassword).toBe("keep-pass");
|
};
|
||||||
expect(next.providerDailyLimitBytes.ddownload).toBe(7 * GIB);
|
const next = applyAccountEdit(settings, state);
|
||||||
});
|
|
||||||
|
expect(next.ddownloadLogin).toBe("new@example.com");
|
||||||
it("rejects whitespace-only passwords for direct login accounts", () => {
|
expect(next.ddownloadPassword).toBe("new-pass");
|
||||||
const settings = {
|
expect(next.linkSnappyLogin).toBe("keep@example.com");
|
||||||
...defaultSettings(),
|
expect(next.linkSnappyPassword).toBe("keep-pass");
|
||||||
ddownloadLogin: "user@example.com",
|
expect(next.providerDailyLimitBytes.ddownload).toBe(7 * GIB);
|
||||||
ddownloadPassword: "password",
|
});
|
||||||
linkSnappyLogin: "member@example.com",
|
|
||||||
linkSnappyPassword: "secret"
|
it("rejects whitespace-only passwords for direct login accounts", () => {
|
||||||
};
|
const settings = {
|
||||||
const ddownloadTarget: AccountEditTarget = {
|
...defaultSettings(),
|
||||||
type: "single",
|
ddownloadLogin: "user@example.com",
|
||||||
rowKey: "svc-ddownload",
|
ddownloadPassword: "password",
|
||||||
kind: "ddownload-login",
|
linkSnappyLogin: "member@example.com",
|
||||||
service: "ddownload",
|
linkSnappyPassword: "secret"
|
||||||
provider: "ddownload"
|
};
|
||||||
};
|
const ddownloadTarget: AccountEditTarget = {
|
||||||
const linkSnappyTarget: AccountEditTarget = {
|
type: "single",
|
||||||
type: "single",
|
rowKey: "svc-ddownload",
|
||||||
rowKey: "svc-linksnappy",
|
kind: "ddownload-login",
|
||||||
kind: "linksnappy-login",
|
service: "ddownload",
|
||||||
service: "linksnappy",
|
provider: "ddownload"
|
||||||
provider: "linksnappy"
|
};
|
||||||
};
|
const linkSnappyTarget: AccountEditTarget = {
|
||||||
|
type: "single",
|
||||||
expect(validateAccountEdit({ ...createAccountEditState(ddownloadTarget, settings), password: " " }, settings)).toMatch(/Passwort/i);
|
rowKey: "svc-linksnappy",
|
||||||
expect(validateAccountEdit({ ...createAccountEditState(linkSnappyTarget, settings), password: "\t" }, settings)).toMatch(/Passwort/i);
|
kind: "linksnappy-login",
|
||||||
});
|
service: "linksnappy",
|
||||||
|
provider: "linksnappy"
|
||||||
it("builds a targeted check snapshot without invalid sibling accounts", () => {
|
};
|
||||||
const settings = {
|
|
||||||
...defaultSettings(),
|
expect(validateAccountEdit({ ...createAccountEditState(ddownloadTarget, settings), password: " " }, settings)).toMatch(/Passwort/i);
|
||||||
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass",
|
expect(validateAccountEdit({ ...createAccountEditState(linkSnappyTarget, settings), password: "\t" }, settings)).toMatch(/Passwort/i);
|
||||||
megaLogin: "first@example.com",
|
});
|
||||||
megaPassword: "first-pass",
|
|
||||||
debridLinkApiKeys: "one\ntwo"
|
it("builds a targeted check snapshot without invalid sibling accounts", () => {
|
||||||
};
|
const settings = {
|
||||||
const target = megaTarget("second@example.com");
|
...defaultSettings(),
|
||||||
const state = createAccountEditState(target, settings);
|
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass",
|
||||||
const checkSettings = buildAccountEditCheckSettings(settings, state);
|
megaLogin: "first@example.com",
|
||||||
|
megaPassword: "first-pass",
|
||||||
expect(checkSettings.megaCredentials).toBe("second@example.com:second-pass");
|
debridLinkApiKeys: "one\ntwo"
|
||||||
expect(checkSettings.megaLogin).toBe("second@example.com");
|
};
|
||||||
expect(checkSettings.megaPassword).toBe("second-pass");
|
const target = megaTarget("second@example.com");
|
||||||
expect(checkSettings.debridLinkApiKeys).toBe("");
|
const state = createAccountEditState(target, settings);
|
||||||
});
|
const checkSettings = buildAccountEditCheckSettings(settings, state);
|
||||||
|
|
||||||
it("removes only the selected Mega-Debrid account and all metadata belonging to it", () => {
|
expect(checkSettings.megaCredentials).toBe("second@example.com:second-pass");
|
||||||
const removeId = getMegaDebridAccountId("second@example.com");
|
expect(checkSettings.megaLogin).toBe("second@example.com");
|
||||||
const keepId = getMegaDebridAccountId("first@example.com");
|
expect(checkSettings.megaPassword).toBe("second-pass");
|
||||||
const settings = {
|
expect(checkSettings.debridLinkApiKeys).toBe("");
|
||||||
...defaultSettings(),
|
});
|
||||||
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass",
|
|
||||||
megaLogin: "first@example.com",
|
it("removes only the selected Mega-Debrid account and all metadata belonging to it", () => {
|
||||||
megaPassword: "first-pass",
|
const removeId = getMegaDebridAccountId("second@example.com");
|
||||||
megaDebridDisabledAccountIds: [removeId, keepId],
|
const keepId = getMegaDebridAccountId("first@example.com");
|
||||||
megaDebridAccountDailyLimitBytes: { [removeId]: 2, [keepId]: 1 },
|
const settings = {
|
||||||
megaDebridAccountDailyUsageBytes: { [removeId]: 4, [keepId]: 3 },
|
...defaultSettings(),
|
||||||
megaDebridAccountTotalUsageBytes: { [removeId]: 6, [keepId]: 5 },
|
megaCredentials: "first@example.com:first-pass\nsecond@example.com:second-pass",
|
||||||
debridAccountStatuses: {
|
megaLogin: "first@example.com",
|
||||||
[removeId]: { accountId: removeId, provider: "megadebrid" as const, label: "Account 2", maskedLogin: "se***om", valid: true, isPremium: false, premiumUntilMs: null, message: "Free", checkedAt: 1 },
|
megaPassword: "first-pass",
|
||||||
[keepId]: { accountId: keepId, provider: "megadebrid" as const, label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
|
megaDebridDisabledAccountIds: [removeId, keepId],
|
||||||
}
|
megaDebridAccountDailyLimitBytes: { [removeId]: 2, [keepId]: 1 },
|
||||||
};
|
megaDebridAccountDailyUsageBytes: { [removeId]: 4, [keepId]: 3 },
|
||||||
const next = removeAccountTarget(settings, megaTarget("second@example.com"));
|
megaDebridAccountTotalUsageBytes: { [removeId]: 6, [keepId]: 5 },
|
||||||
|
debridAccountStatuses: {
|
||||||
expect(next.megaCredentials).toBe("first@example.com:first-pass");
|
[removeId]: { accountId: removeId, provider: "megadebrid" as const, label: "Account 2", maskedLogin: "se***om", valid: true, isPremium: false, premiumUntilMs: null, message: "Free", checkedAt: 1 },
|
||||||
expect(next.megaDebridDisabledAccountIds).toEqual([keepId]);
|
[keepId]: { accountId: keepId, provider: "megadebrid" as const, label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
|
||||||
expect(next.megaDebridAccountDailyLimitBytes).toEqual({ [keepId]: 1 });
|
}
|
||||||
expect(next.megaDebridAccountDailyUsageBytes).toEqual({ [keepId]: 3 });
|
};
|
||||||
expect(next.megaDebridAccountTotalUsageBytes).toEqual({ [keepId]: 5 });
|
const next = removeAccountTarget(settings, megaTarget("second@example.com"));
|
||||||
expect(next.debridAccountStatuses).toEqual({
|
|
||||||
[keepId]: { accountId: keepId, provider: "megadebrid", label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
|
expect(next.megaCredentials).toBe("first@example.com:first-pass");
|
||||||
});
|
expect(next.megaDebridDisabledAccountIds).toEqual([keepId]);
|
||||||
});
|
expect(next.megaDebridAccountDailyLimitBytes).toEqual({ [keepId]: 1 });
|
||||||
});
|
expect(next.megaDebridAccountDailyUsageBytes).toEqual({ [keepId]: 3 });
|
||||||
|
expect(next.megaDebridAccountTotalUsageBytes).toEqual({ [keepId]: 5 });
|
||||||
|
expect(next.debridAccountStatuses).toEqual({
|
||||||
|
[keepId]: { accountId: keepId, provider: "megadebrid", label: "Account 1", maskedLogin: "fi***om", valid: true, isPremium: true, premiumUntilMs: null, message: "OK", checkedAt: 1 }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+11
-9
@@ -36,10 +36,10 @@ describe("debrid service", () => {
|
|||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
token: "rd-token",
|
token: "rd-token",
|
||||||
megaLogin: "user",
|
megaLogin: "user",
|
||||||
megaPassword: "pass",
|
megaPassword: "pass",
|
||||||
megaCredentials: "user:pass",
|
megaCredentials: "user:pass",
|
||||||
bestToken: "",
|
bestToken: "",
|
||||||
providerOrder: [] as const,
|
providerOrder: [] as const,
|
||||||
providerPrimary: "realdebrid" as const,
|
providerPrimary: "realdebrid" as const,
|
||||||
providerSecondary: "megadebrid" as const,
|
providerSecondary: "megadebrid" as const,
|
||||||
@@ -1621,16 +1621,18 @@ describe("debrid service", () => {
|
|||||||
expect(megaGetLinkCalled).toBe(false);
|
expect(megaGetLinkCalled).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses Mega Web only when it is configured as a separate fallback provider", async () => {
|
it("uses Mega Web only when it is configured as a separate fallback provider", async () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
token: "",
|
token: "",
|
||||||
bestToken: "",
|
bestToken: "",
|
||||||
allDebridToken: "",
|
allDebridToken: "",
|
||||||
megaLogin: "user",
|
megaLogin: "user",
|
||||||
megaPassword: "pass",
|
megaPassword: "pass",
|
||||||
megaCredentials: "user:pass",
|
megaCredentials: "user:pass",
|
||||||
megaDebridApiEnabled: true,
|
megaDebridApiCredentials: "user:pass",
|
||||||
|
megaDebridWebCredentials: "user:pass",
|
||||||
|
megaDebridApiEnabled: true,
|
||||||
megaDebridWebEnabled: true,
|
megaDebridWebEnabled: true,
|
||||||
providerOrder: [] as const,
|
providerOrder: [] as const,
|
||||||
providerPrimary: "megadebrid-api" as const,
|
providerPrimary: "megadebrid-api" as const,
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ describe("debug-server", () => {
|
|||||||
const fixture = await createFixture();
|
const fixture = await createFixture();
|
||||||
const response = await fetch(`${fixture.baseUrl}/health?token=${fixture.token}`, {
|
const response = await fetch(`${fixture.baseUrl}/health?token=${fixture.token}`, {
|
||||||
headers: {
|
headers: {
|
||||||
"X-Forwarded-For": "159.195.63.46"
|
"X-Forwarded-For": "203.0.113.46"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(response.ok).toBe(true);
|
expect(response.ok).toBe(true);
|
||||||
@@ -439,7 +439,7 @@ describe("debug-server", () => {
|
|||||||
const traceLogPath = getTraceLogPath();
|
const traceLogPath = getTraceLogPath();
|
||||||
expect(traceLogPath).toBeTruthy();
|
expect(traceLogPath).toBeTruthy();
|
||||||
const traceText = fs.readFileSync(traceLogPath!, "utf8");
|
const traceText = fs.readFileSync(traceLogPath!, "utf8");
|
||||||
expect(traceText).toContain("clientIp=159.195.63.46");
|
expect(traceText).toContain("clientIp=203.0.113.46");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("serves package details and package log by package query", async () => {
|
it("serves package details and package log by package query", async () => {
|
||||||
|
|||||||
@@ -252,6 +252,187 @@ describe("download manager", () => {
|
|||||||
expect(failures.has("realdebrid")).toBe(true);
|
expect(failures.has("realdebrid")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("invalidates only the Mega-Debrid Web session when Web credentials change", () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-mega-web-session-refresh-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const settings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
megaCredentials: "api-user:api-pass\nweb-user:web-pass",
|
||||||
|
megaDebridApiCredentials: "api-user:api-pass",
|
||||||
|
megaDebridWebCredentials: "web-user:web-pass",
|
||||||
|
megaDebridApiEnabled: true,
|
||||||
|
megaDebridWebEnabled: true
|
||||||
|
};
|
||||||
|
const invalidateMegaSession = vi.fn();
|
||||||
|
const manager = new DownloadManager(settings, emptySession(), createStoragePaths(path.join(root, "state")), { invalidateMegaSession });
|
||||||
|
|
||||||
|
manager.setSettings({ ...settings, megaDebridApiCredentials: "api-user:new-api-pass" });
|
||||||
|
expect(invalidateMegaSession).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
manager.setSettings({ ...settings, megaDebridApiCredentials: "api-user:new-api-pass", megaDebridWebCredentials: "web-user:new-web-pass" });
|
||||||
|
expect(invalidateMegaSession).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("releases only Mega-Debrid reset parks when a newly usable account appears", () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-mega-account-refresh-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const storagePaths = createStoragePaths(path.join(root, "state"));
|
||||||
|
const previousSettings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
megaCredentials: "old@example.test:old-secret",
|
||||||
|
megaDebridApiEnabled: true
|
||||||
|
};
|
||||||
|
const session = emptySession();
|
||||||
|
const megaPackageId = "mega-refresh-package";
|
||||||
|
const otherPackageId = "other-refresh-package";
|
||||||
|
const megaItemId = "mega-refresh-item";
|
||||||
|
const otherItemId = "other-refresh-item";
|
||||||
|
const createdAt = Date.now();
|
||||||
|
session.packageOrder = [megaPackageId, otherPackageId];
|
||||||
|
session.packages[megaPackageId] = {
|
||||||
|
id: megaPackageId,
|
||||||
|
name: "Mega refresh",
|
||||||
|
status: "downloading",
|
||||||
|
itemIds: [megaItemId],
|
||||||
|
cancelled: false,
|
||||||
|
enabled: true,
|
||||||
|
createdAt,
|
||||||
|
updatedAt: createdAt
|
||||||
|
} as any;
|
||||||
|
session.packages[otherPackageId] = {
|
||||||
|
id: otherPackageId,
|
||||||
|
name: "Other refresh",
|
||||||
|
status: "queued",
|
||||||
|
itemIds: [otherItemId],
|
||||||
|
cancelled: false,
|
||||||
|
enabled: true,
|
||||||
|
createdAt,
|
||||||
|
updatedAt: createdAt
|
||||||
|
} as any;
|
||||||
|
session.items[megaItemId] = {
|
||||||
|
id: megaItemId,
|
||||||
|
packageId: megaPackageId,
|
||||||
|
url: "https://rapidgator.net/file/mega-refresh",
|
||||||
|
provider: "megadebrid-api",
|
||||||
|
status: "queued",
|
||||||
|
retries: 1,
|
||||||
|
speedBps: 0,
|
||||||
|
downloadedBytes: 0,
|
||||||
|
totalBytes: null,
|
||||||
|
progressPercent: 0,
|
||||||
|
fileName: "mega-refresh.rar",
|
||||||
|
targetPath: "",
|
||||||
|
resumable: true,
|
||||||
|
attempts: 0,
|
||||||
|
lastError: "limit",
|
||||||
|
fullStatus: "Mega-Debrid bis Tagesreset gesperrt, Pause 3600s",
|
||||||
|
createdAt,
|
||||||
|
updatedAt: createdAt
|
||||||
|
} as any;
|
||||||
|
session.items[otherItemId] = {
|
||||||
|
...session.items[megaItemId],
|
||||||
|
id: otherItemId,
|
||||||
|
packageId: otherPackageId,
|
||||||
|
url: "https://ddownload.com/file/other-refresh",
|
||||||
|
provider: "realdebrid",
|
||||||
|
fileName: "other-refresh.rar",
|
||||||
|
fullStatus: "Netzwerk-Retry in 60s"
|
||||||
|
} as any;
|
||||||
|
session.running = true;
|
||||||
|
const manager = new DownloadManager(previousSettings, session, storagePaths);
|
||||||
|
session.running = true;
|
||||||
|
session.packages[megaPackageId].status = "downloading";
|
||||||
|
session.items[megaItemId].status = "queued";
|
||||||
|
session.items[megaItemId].fullStatus = "Mega-Debrid bis Tagesreset gesperrt, Pause 3600s";
|
||||||
|
session.items[megaItemId].provider = "megadebrid-api";
|
||||||
|
session.items[otherItemId].status = "queued";
|
||||||
|
session.items[otherItemId].fullStatus = "Netzwerk-Retry in 60s";
|
||||||
|
session.items[otherItemId].provider = "realdebrid";
|
||||||
|
const retryAfter = (manager as any).retryAfterByItem as Map<string, number>;
|
||||||
|
const retryState = (manager as any).retryStateByItem as Map<string, unknown>;
|
||||||
|
retryAfter.set(megaItemId, Date.now() + 3_600_000);
|
||||||
|
retryAfter.set(otherItemId, Date.now() + 60_000);
|
||||||
|
retryState.set(megaItemId, { unrestrictRetries: 1 });
|
||||||
|
retryState.set(otherItemId, { genericErrorRetries: 1 });
|
||||||
|
const scheduler = vi.spyOn(manager as any, "ensureScheduler").mockResolvedValue(undefined);
|
||||||
|
vi.spyOn(manager as any, "cleanupExistingExtractedArchives").mockResolvedValue(0);
|
||||||
|
|
||||||
|
manager.setSettings({
|
||||||
|
...previousSettings,
|
||||||
|
megaCredentials: "old@example.test:old-secret\nnew@example.test:new-secret",
|
||||||
|
megaDebridDisabledAccountIds: [getMegaDebridAccountId("old@example.test")]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(retryAfter.has(megaItemId)).toBe(false);
|
||||||
|
expect(retryState.has(megaItemId)).toBe(false);
|
||||||
|
expect(session.items[megaItemId].fullStatus).toBe("Wartet");
|
||||||
|
expect(session.packages[megaPackageId].status).toBe("queued");
|
||||||
|
expect(retryAfter.has(otherItemId)).toBe(true);
|
||||||
|
expect(retryState.has(otherItemId)).toBe(true);
|
||||||
|
expect(session.items[otherItemId].fullStatus).toBe("Netzwerk-Retry in 60s");
|
||||||
|
expect(scheduler).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the package status atomically when an active item is queued for retry", () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-retry-package-status-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const storagePaths = createStoragePaths(path.join(root, "state"));
|
||||||
|
initPackageLogs(storagePaths.baseDir);
|
||||||
|
initItemLogs(storagePaths.baseDir);
|
||||||
|
const session = emptySession();
|
||||||
|
const packageId = "retry-status-package";
|
||||||
|
const itemId = "retry-status-item";
|
||||||
|
const createdAt = Date.now();
|
||||||
|
session.packageOrder = [packageId];
|
||||||
|
session.packages[packageId] = {
|
||||||
|
id: packageId,
|
||||||
|
name: "Retry status",
|
||||||
|
status: "downloading",
|
||||||
|
itemIds: [itemId],
|
||||||
|
cancelled: false,
|
||||||
|
enabled: true,
|
||||||
|
createdAt,
|
||||||
|
updatedAt: createdAt
|
||||||
|
} as any;
|
||||||
|
session.items[itemId] = {
|
||||||
|
id: itemId,
|
||||||
|
packageId,
|
||||||
|
url: "https://rapidgator.net/file/retry-status",
|
||||||
|
provider: "megadebrid-api",
|
||||||
|
status: "downloading",
|
||||||
|
retries: 1,
|
||||||
|
speedBps: 0,
|
||||||
|
downloadedBytes: 0,
|
||||||
|
totalBytes: null,
|
||||||
|
progressPercent: 0,
|
||||||
|
fileName: "retry-status.rar",
|
||||||
|
targetPath: "",
|
||||||
|
resumable: true,
|
||||||
|
attempts: 1,
|
||||||
|
lastError: "",
|
||||||
|
fullStatus: "Download läuft",
|
||||||
|
createdAt,
|
||||||
|
updatedAt: createdAt
|
||||||
|
} as any;
|
||||||
|
const manager = new DownloadManager(defaultSettings(), session, storagePaths);
|
||||||
|
session.packages[packageId].status = "downloading";
|
||||||
|
const active = {
|
||||||
|
itemId,
|
||||||
|
packageId,
|
||||||
|
abortController: new AbortController(),
|
||||||
|
abortReason: "none",
|
||||||
|
resumable: true,
|
||||||
|
nonResumableCounted: false,
|
||||||
|
blockedOnDiskWrite: false,
|
||||||
|
blockedOnDiskSince: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
(manager as any).queueRetry(session.items[itemId], active, 60_000, "Mega-Debrid bis Tagesreset gesperrt, Pause 60s");
|
||||||
|
|
||||||
|
expect(session.items[itemId].status).toBe("queued");
|
||||||
|
expect(session.packages[packageId].status).toBe("queued");
|
||||||
|
});
|
||||||
|
|
||||||
it("records history duration from the first actual package start", () => {
|
it("records history duration from the first actual package start", () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-history-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-history-"));
|
||||||
tempDirs.push(root);
|
tempDirs.push(root);
|
||||||
|
|||||||
@@ -36,7 +36,12 @@ import {
|
|||||||
getPackageProgress,
|
getPackageProgress,
|
||||||
getPackageSizeProgress
|
getPackageSizeProgress
|
||||||
} from "../src/renderer/views/downloads/DownloadsTable";
|
} from "../src/renderer/views/downloads/DownloadsTable";
|
||||||
import { compactDownloadServiceLabel, normalizeDownloadServiceLabel } from "../src/renderer/download-format";
|
import {
|
||||||
|
compactDownloadServiceLabel,
|
||||||
|
extractHoster,
|
||||||
|
formatHosterLabel,
|
||||||
|
normalizeDownloadServiceLabel
|
||||||
|
} from "../src/renderer/download-format";
|
||||||
import { getRollingMetricDirection } from "../src/renderer/ui/RollingMetricValue";
|
import { getRollingMetricDirection } from "../src/renderer/ui/RollingMetricValue";
|
||||||
|
|
||||||
const now = new Date(2026, 7, 10, 12, 0, 0, 0).getTime();
|
const now = new Date(2026, 7, 10, 12, 0, 0, 0).getTime();
|
||||||
@@ -71,6 +76,13 @@ describe("Downloadtabellen-Spalten", () => {
|
|||||||
expect(css).not.toMatch(/\.downloads-table\.is-column-drag-active \[data-column-dragging="true"\]\s*\{[^}]*background:/s);
|
expect(css).not.toMatch(/\.downloads-table\.is-column-drag-active \[data-column-dragging="true"\]\s*\{[^}]*background:/s);
|
||||||
expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.downloads-table\.is-column-drag-active \[data-download-column\][^{]*\{[^}]*transition-duration:\s*220ms !important;/s);
|
expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)[\s\S]*\.downloads-table\.is-column-drag-active \[data-download-column\][^{]*\{[^}]*transition-duration:\s*220ms !important;/s);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("changes package collapse state only through user actions", () => {
|
||||||
|
const source = fs.readFileSync(path.join(process.cwd(), "src/renderer/App.tsx"), "utf8");
|
||||||
|
|
||||||
|
expect(source).not.toContain("autoExpandedPkgsRef");
|
||||||
|
expect(source).not.toMatch(/isExtracting[\s\S]{0,800}setCollapsedPackages/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("rollende Downloadkennzahlen", () => {
|
describe("rollende Downloadkennzahlen", () => {
|
||||||
@@ -175,12 +187,26 @@ describe("responsive Downloadstatus und Servicebezeichnungen", () => {
|
|||||||
expect(compactDownloadStatus("Entpacken 1% (1/1) · Tonspur: Deutsch")).toBe("Entpacken - 1%");
|
expect(compactDownloadStatus("Entpacken 1% (1/1) · Tonspur: Deutsch")).toBe("Entpacken - 1%");
|
||||||
expect(compactDownloadStatus("0/11 · Entpacken 53% (1/1) · scn2-httpv7-S01E102.rar")).toBe("Entpacken - 53%");
|
expect(compactDownloadStatus("0/11 · Entpacken 53% (1/1) · scn2-httpv7-S01E102.rar")).toBe("Entpacken - 53%");
|
||||||
expect(compactDownloadStatus("Extracting 53% (1/1) · archive.rar")).toBe("Extracting - 53%");
|
expect(compactDownloadStatus("Extracting 53% (1/1) · archive.rar")).toBe("Extracting - 53%");
|
||||||
|
expect(compactDownloadStatus("Passwort knacken: 75% (3/4) · sau-geheim.part1.rar")).toBe("Passwort knacken: 75% (3/4)");
|
||||||
expect(compactDownloadStatus("Passwort gefunden · archive.part1.rar")).toBe("Passwort gefunden");
|
expect(compactDownloadStatus("Passwort gefunden · archive.part1.rar")).toBe("Passwort gefunden");
|
||||||
expect(compactDownloadStatus("Entpacken - Ausstehend · archive.part1.rar")).toBe("Entpacken - Ausstehend");
|
expect(compactDownloadStatus("Entpacken - Ausstehend · archive.part1.rar")).toBe("Entpacken - Ausstehend");
|
||||||
expect(compactDownloadStatus("Entpack-Fehler [archive.part1.rar]: Unerwartetes Dateiende")).toBe("Entpack-Fehler");
|
expect(compactDownloadStatus("Entpack-Fehler [archive.part1.rar]: Unerwartetes Dateiende")).toBe("Entpack-Fehler");
|
||||||
expect(compactDownloadStatus("Extraction error [archive.part1.rar]: Unexpected end of file")).toBe("Extraction error");
|
expect(compactDownloadStatus("Extraction error [archive.part1.rar]: Unexpected end of file")).toBe("Extraction error");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("normalizes every supported RapidGator domain to one hoster identity", () => {
|
||||||
|
const hosters = [
|
||||||
|
extractHoster("https://rapidgator.net/file/one"),
|
||||||
|
extractHoster("https://rg.to/file/two"),
|
||||||
|
extractHoster("https://cdn.rg.to/file/three"),
|
||||||
|
extractHoster("https://rapidgator.asia/file/four")
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(hosters).toEqual(["rapidgator", "rapidgator", "rapidgator", "rapidgator"]);
|
||||||
|
expect(new Set(hosters).size).toBe(1);
|
||||||
|
expect(formatHosterLabel(hosters[1])).toEqual(expect.objectContaining({ compact: "RG", title: "RapidGator", iconSrc: expect.any(String) }));
|
||||||
|
});
|
||||||
|
|
||||||
it("removes duplicated access-mode wording from service labels", () => {
|
it("removes duplicated access-mode wording from service labels", () => {
|
||||||
expect(normalizeDownloadServiceLabel("Mega-Debrid Web (Web Account)")).toBe("Mega-Debrid (Web)");
|
expect(normalizeDownloadServiceLabel("Mega-Debrid Web (Web Account)")).toBe("Mega-Debrid (Web)");
|
||||||
expect(normalizeDownloadServiceLabel("Mega-Debrid API (API Account)")).toBe("Mega-Debrid (API)");
|
expect(normalizeDownloadServiceLabel("Mega-Debrid API (API Account)")).toBe("Mega-Debrid (API)");
|
||||||
@@ -690,7 +716,7 @@ describe("downloads view", () => {
|
|||||||
expect(css).toMatch(/\.downloads-link-state\.online\s*\{[^}]*background:\s*var\(--ui-success\);/s);
|
expect(css).toMatch(/\.downloads-link-state\.online\s*\{[^}]*background:\s*var\(--ui-success\);/s);
|
||||||
expect(css).toMatch(/\.downloads-status-cell\s*\{[^}]*container-type:\s*inline-size;/s);
|
expect(css).toMatch(/\.downloads-status-cell\s*\{[^}]*container-type:\s*inline-size;/s);
|
||||||
expect(css).toMatch(/\.downloads-service-cell\s*\{[^}]*container-type:\s*inline-size;/s);
|
expect(css).toMatch(/\.downloads-service-cell\s*\{[^}]*container-type:\s*inline-size;/s);
|
||||||
expect(css).toMatch(/\.downloads-cell-slot\s*>\s*:is\(\.downloads-status-cell, \.downloads-service-cell\)\s*\{[^}]*justify-content:\s*flex-start;[^}]*text-align:\s*left;/s);
|
expect(css).toMatch(/\.downloads-cell-slot\s*>\s*:is\(\.downloads-status-cell, \.downloads-service-cell\)\s*\{[^}]*justify-content:\s*center;[^}]*text-align:\s*center;/s);
|
||||||
expect(css).toMatch(/:is\(\.downloads-status-full, \.downloads-status-compact, \.downloads-service-full, \.downloads-service-compact\)\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;[^}]*text-overflow:\s*ellipsis;[^}]*white-space:\s*nowrap;/s);
|
expect(css).toMatch(/:is\(\.downloads-status-full, \.downloads-status-compact, \.downloads-service-full, \.downloads-service-compact\)\s*\{[^}]*min-width:\s*0;[^}]*overflow:\s*hidden;[^}]*text-overflow:\s*ellipsis;[^}]*white-space:\s*nowrap;/s);
|
||||||
expect(css).toMatch(/@container\s*\(max-width:\s*150px\)[\s\S]*\.downloads-status-full[^{]*\{[^}]*display:\s*none;[\s\S]*\.downloads-status-compact[^{]*\{[^}]*display:\s*block;/s);
|
expect(css).toMatch(/@container\s*\(max-width:\s*150px\)[\s\S]*\.downloads-status-full[^{]*\{[^}]*display:\s*none;[\s\S]*\.downloads-status-compact[^{]*\{[^}]*display:\s*block;/s);
|
||||||
expect(css).toMatch(/@container\s*\(max-width:\s*150px\)[\s\S]*\.downloads-service-full[^{]*\{[^}]*display:\s*none;[\s\S]*\.downloads-service-compact[^{]*\{[^}]*display:\s*block;/s);
|
expect(css).toMatch(/@container\s*\(max-width:\s*150px\)[\s\S]*\.downloads-service-full[^{]*\{[^}]*display:\s*none;[\s\S]*\.downloads-service-compact[^{]*\{[^}]*display:\s*block;/s);
|
||||||
|
|||||||
@@ -10,13 +10,95 @@ describe("mega-web-fallback", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("MegaWebFallback class", () => {
|
describe("MegaWebFallback class", () => {
|
||||||
it("returns null when credentials are empty", async () => {
|
it("returns null when credentials are empty", async () => {
|
||||||
const fallback = new MegaWebFallback(() => ({ login: "", password: "" }));
|
const fallback = new MegaWebFallback(() => ({ login: "", password: "" }));
|
||||||
const result = await fallback.unrestrict("https://mega.debrid/test");
|
const result = await fallback.unrestrict("https://mega.debrid/test");
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("logs in, fetches HTML, parses code, and polls AJAX for direct url", async () => {
|
it("does not restore an invalidated session when an older login finishes later", async () => {
|
||||||
|
let finishLogin: (cookie: string) => void = () => {};
|
||||||
|
const loginResult = new Promise<string>((resolve) => {
|
||||||
|
finishLogin = resolve;
|
||||||
|
});
|
||||||
|
const fallback = new MegaWebFallback(() => ({ login: "old-user", password: "old-pass" }));
|
||||||
|
const internals = fallback as unknown as {
|
||||||
|
ensureSession: (key: string, login: string, password: string) => Promise<string>;
|
||||||
|
login: (login: string, password: string) => Promise<string>;
|
||||||
|
sessions: Map<string, { cookie: string; setAt: number }>;
|
||||||
|
};
|
||||||
|
vi.spyOn(internals, "login").mockReturnValue(loginResult);
|
||||||
|
|
||||||
|
const pending = internals.ensureSession("old-user", "old-user", "old-pass");
|
||||||
|
await Promise.resolve();
|
||||||
|
fallback.invalidateSession();
|
||||||
|
finishLogin("stale-old-cookie");
|
||||||
|
|
||||||
|
await expect(pending).resolves.toBe("stale-old-cookie");
|
||||||
|
expect(internals.sessions.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not cache an old session when an invalidated request retries login", async () => {
|
||||||
|
const fallback = new MegaWebFallback(() => ({ login: "old-user", password: "old-pass" }));
|
||||||
|
const internals = fallback as unknown as {
|
||||||
|
login: (login: string, password: string) => Promise<string>;
|
||||||
|
generate: (link: string, cookie: string) => Promise<{ directUrl: string; fileName: string } | null>;
|
||||||
|
sessions: Map<string, { cookie: string; setAt: number }>;
|
||||||
|
};
|
||||||
|
vi.spyOn(internals, "login")
|
||||||
|
.mockResolvedValueOnce("first-stale-cookie")
|
||||||
|
.mockResolvedValueOnce("second-stale-cookie");
|
||||||
|
vi.spyOn(internals, "generate")
|
||||||
|
.mockImplementationOnce(async () => {
|
||||||
|
fallback.invalidateSession();
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({ directUrl: "https://mega.direct/retry", fileName: "retry.bin" });
|
||||||
|
|
||||||
|
const result = await fallback.unrestrict("https://mega.debrid/retry", undefined, { login: "old-user", password: "old-pass" });
|
||||||
|
|
||||||
|
expect(result?.directUrl).toBe("https://mega.direct/retry");
|
||||||
|
expect(internals.sessions.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not cache old credentials from a queued request after invalidation", async () => {
|
||||||
|
let releaseFirstLogin: () => void = () => {};
|
||||||
|
let markFirstLoginStarted: () => void = () => {};
|
||||||
|
const firstLoginGate = new Promise<void>((resolve) => {
|
||||||
|
releaseFirstLogin = resolve;
|
||||||
|
});
|
||||||
|
const firstLoginStarted = new Promise<void>((resolve) => {
|
||||||
|
markFirstLoginStarted = resolve;
|
||||||
|
});
|
||||||
|
const fallback = new MegaWebFallback(() => ({ login: "old-user", password: "old-pass" }));
|
||||||
|
const internals = fallback as unknown as {
|
||||||
|
login: (login: string, password: string) => Promise<string>;
|
||||||
|
generate: (link: string, cookie: string) => Promise<{ directUrl: string; fileName: string }>;
|
||||||
|
sessions: Map<string, { cookie: string; setAt: number }>;
|
||||||
|
};
|
||||||
|
let loginCount = 0;
|
||||||
|
vi.spyOn(internals, "login").mockImplementation(async () => {
|
||||||
|
loginCount += 1;
|
||||||
|
if (loginCount === 1) {
|
||||||
|
markFirstLoginStarted();
|
||||||
|
await firstLoginGate;
|
||||||
|
}
|
||||||
|
return `old-cookie-${loginCount}`;
|
||||||
|
});
|
||||||
|
vi.spyOn(internals, "generate").mockResolvedValue({ directUrl: "https://mega.direct/queued", fileName: "queued.bin" });
|
||||||
|
|
||||||
|
const first = fallback.unrestrict("https://mega.debrid/first", undefined, { login: "old-user", password: "old-pass" });
|
||||||
|
await firstLoginStarted;
|
||||||
|
const queued = fallback.unrestrict("https://mega.debrid/queued", undefined, { login: "old-user", password: "old-pass" });
|
||||||
|
fallback.invalidateSession();
|
||||||
|
releaseFirstLogin();
|
||||||
|
|
||||||
|
await expect(Promise.all([first, queued])).resolves.toHaveLength(2);
|
||||||
|
expect(loginCount).toBe(2);
|
||||||
|
expect(internals.sessions.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("logs in, fetches HTML, parses code, and polls AJAX for direct url", async () => {
|
||||||
let fetchCallCount = 0;
|
let fetchCallCount = 0;
|
||||||
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
||||||
const urlStr = String(url);
|
const urlStr = String(url);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import type { DownloadItem, PackageEntry } from "../src/shared/types";
|
import type { DownloadItem, PackageEntry } from "../src/shared/types";
|
||||||
import { sortPackagesForDisplay } from "../src/renderer/package-order";
|
import { sortPackagesForDisplay } from "../src/renderer/package-order";
|
||||||
|
|
||||||
function createPackage(id: string, itemIds: string[]): PackageEntry {
|
function createPackage(id: string, itemIds: string[], downloadStartedAt = 0): PackageEntry {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -15,7 +15,8 @@ function createPackage(id: string, itemIds: string[]): PackageEntry {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
priority: "normal",
|
priority: "normal",
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now
|
updatedAt: now,
|
||||||
|
downloadStartedAt
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +91,7 @@ describe("sortPackagesForDisplay", () => {
|
|||||||
expect(orderAfter).toEqual(orderBefore);
|
expect(orderAfter).toEqual(orderBefore);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps package order untouched when auto sort is disabled", () => {
|
it("keeps package order untouched when auto sort is disabled", () => {
|
||||||
const packages = [
|
const packages = [
|
||||||
createPackage("pkg-a", ["a1"]),
|
createPackage("pkg-a", ["a1"]),
|
||||||
createPackage("pkg-b", ["b1"]),
|
createPackage("pkg-b", ["b1"]),
|
||||||
@@ -104,6 +105,35 @@ describe("sortPackagesForDisplay", () => {
|
|||||||
|
|
||||||
const sorted = sortPackagesForDisplay(packages, items, true, false);
|
const sorted = sortPackagesForDisplay(packages, items, true, false);
|
||||||
|
|
||||||
expect(sorted.map((pkg) => pkg.id)).toEqual(["pkg-a", "pkg-b", "pkg-c"]);
|
expect(sorted.map((pkg) => pkg.id)).toEqual(["pkg-a", "pkg-b", "pkg-c"]);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
it("keeps every active package in activation order when a new package starts", () => {
|
||||||
|
const packages = [
|
||||||
|
createPackage("pkg-new", ["new-item"], 200),
|
||||||
|
createPackage("pkg-existing", ["existing-item"], 100)
|
||||||
|
];
|
||||||
|
const items: Record<string, DownloadItem> = {
|
||||||
|
"new-item": createItem("new-item", "pkg-new", "downloading", 100),
|
||||||
|
"existing-item": createItem("existing-item", "pkg-existing", "downloading", 200)
|
||||||
|
};
|
||||||
|
const sorted = sortPackagesForDisplay(packages, items, true, true);
|
||||||
|
|
||||||
|
expect(sorted.map((pkg) => pkg.id)).toEqual(["pkg-existing", "pkg-new"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps queue order for active packages without a recorded start time", () => {
|
||||||
|
const packages = [
|
||||||
|
createPackage("pkg-a", ["a1"]),
|
||||||
|
createPackage("pkg-b", ["b1"]),
|
||||||
|
createPackage("pkg-c", ["c1"])
|
||||||
|
];
|
||||||
|
const items: Record<string, DownloadItem> = {
|
||||||
|
a1: createItem("a1", "pkg-a", "completed", 500),
|
||||||
|
b1: createItem("b1", "pkg-b", "downloading", 200),
|
||||||
|
c1: createItem("c1", "pkg-c", "downloading", 100)
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(sortPackagesForDisplay(packages, items, true, true).map((pkg) => pkg.id)).toEqual(["pkg-b", "pkg-c", "pkg-a"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { isValidElement, type ReactElement, type ReactNode } from "react";
|
|||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { defaultSettings } from "../src/main/constants";
|
import { defaultSettings } from "../src/main/constants";
|
||||||
|
import { buildAccountAddFields, createAccountDialogState } from "../src/renderer/App";
|
||||||
import {
|
import {
|
||||||
applyAccountEdit,
|
applyAccountEdit,
|
||||||
createAccountEditState,
|
createAccountEditState,
|
||||||
@@ -905,6 +906,24 @@ describe("account workspace", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("settings App integration", () => {
|
describe("settings App integration", () => {
|
||||||
|
it("keeps new Mega-Debrid credentials empty and never exposes stored accounts as an API key", () => {
|
||||||
|
const settings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
megaCredentials: "first@example.test:first-secret\nsecond@example.test:second-secret",
|
||||||
|
debridLinkApiKeys: "existing-debrid-link-key"
|
||||||
|
};
|
||||||
|
const megaDialog = createAccountDialogState("create", "megadebrid-api", settings);
|
||||||
|
const megaFields = buildAccountAddFields(megaDialog);
|
||||||
|
const debridLinkFields = buildAccountAddFields(createAccountDialogState("create", "debridlink-api", settings));
|
||||||
|
|
||||||
|
expect(megaDialog.megaNewLogin).toBe("");
|
||||||
|
expect(megaDialog.megaNewPassword).toBe("");
|
||||||
|
expect(megaDialog.token).toBe("");
|
||||||
|
expect(megaFields.map((field) => field.id)).toEqual(["megaNewLogin", "megaNewPassword", "dailyLimitGb"]);
|
||||||
|
expect(megaFields.map((field) => field.label)).not.toContain("Token / API-Key");
|
||||||
|
expect(debridLinkFields.find((field) => field.id === "token")).toEqual(expect.objectContaining({ value: "" }));
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps specific persistence revision-safe when the draft changes in flight", () => {
|
it("keeps specific persistence revision-safe when the draft changes in flight", () => {
|
||||||
const block = sourceBlock(appSource, "const persistSpecificSettings", "const runAccountQuickAction");
|
const block = sourceBlock(appSource, "const persistSpecificSettings", "const runAccountQuickAction");
|
||||||
expect(block).toContain("revisionAtStart");
|
expect(block).toContain("revisionAtStart");
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ describe("bandwidth chart palette", () => {
|
|||||||
const collector = readFileSync(new URL("../src/renderer/views/collector/collector.css", import.meta.url), "utf8");
|
const collector = readFileSync(new URL("../src/renderer/views/collector/collector.css", import.meta.url), "utf8");
|
||||||
|
|
||||||
expect(theme).toMatch(/:focus-visible\s*\{[^}]*outline:\s*2px solid var\(--ui-focus\);/s);
|
expect(theme).toMatch(/:focus-visible\s*\{[^}]*outline:\s*2px solid var\(--ui-focus\);/s);
|
||||||
expect(shell).toContain("color: var(--ui-primary-text);");
|
expect(shell).toContain("color: var(--ui-update-text);");
|
||||||
expect(collector.match(/color:\s*var\(--ui-primary-text\);/g)).toHaveLength(3);
|
expect(collector.match(/color:\s*var\(--ui-primary-text\);/g)).toHaveLength(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+108
-15
@@ -2,7 +2,8 @@ import fs from "node:fs";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
||||||
|
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||||
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
||||||
import { AppSettings } from "../src/shared/types";
|
import { AppSettings } from "../src/shared/types";
|
||||||
import { defaultSettings } from "../src/main/constants";
|
import { defaultSettings } from "../src/main/constants";
|
||||||
@@ -44,11 +45,32 @@ describe("settings storage", () => {
|
|||||||
expect(normalizeSettings({ ...normalized, columnOrder: ["name", "speed"] }).columnOrder).toEqual(["name", "speed"]);
|
expect(normalizeSettings({ ...normalized, columnOrder: ["name", "speed"] }).columnOrder).toEqual(["name", "speed"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses English for new installations and preserves only supported languages", () => {
|
it("uses English for new installations and preserves only supported languages", () => {
|
||||||
expect(defaultSettings().language).toBe("en");
|
expect(defaultSettings().language).toBe("en");
|
||||||
expect(normalizeSettings({ ...defaultSettings(), language: "de" }).language).toBe("de");
|
expect(normalizeSettings({ ...defaultSettings(), language: "de" }).language).toBe("de");
|
||||||
expect(normalizeSettings({ ...defaultSettings(), language: "fr" as "en" }).language).toBe("en");
|
expect(normalizeSettings({ ...defaultSettings(), language: "fr" as "en" }).language).toBe("en");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("migrates a legacy shared Mega-Debrid pool into only the preferred mode", () => {
|
||||||
|
const legacy = { ...defaultSettings() } as Partial<AppSettings>;
|
||||||
|
legacy.megaCredentials = "legacy@example.test:legacy-pass";
|
||||||
|
legacy.megaLogin = "legacy@example.test";
|
||||||
|
legacy.megaPassword = "legacy-pass";
|
||||||
|
legacy.megaDebridApiEnabled = true;
|
||||||
|
legacy.megaDebridWebEnabled = true;
|
||||||
|
legacy.megaDebridPreferApi = true;
|
||||||
|
delete legacy.megaDebridApiCredentials;
|
||||||
|
delete legacy.megaDebridWebCredentials;
|
||||||
|
delete legacy.megaDebridApiDisabledAccountIds;
|
||||||
|
delete legacy.megaDebridWebDisabledAccountIds;
|
||||||
|
|
||||||
|
const normalized = normalizeSettings(legacy as AppSettings);
|
||||||
|
|
||||||
|
expect(normalized.megaDebridApiCredentials).toBe("legacy@example.test:legacy-pass");
|
||||||
|
expect(normalized.megaDebridWebCredentials).toBe("");
|
||||||
|
expect(normalized.megaDebridApiEnabled).toBe(true);
|
||||||
|
expect(normalized.megaDebridWebEnabled).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps German for existing settings files created before language selection existed", () => {
|
it("keeps German for existing settings files created before language selection existed", () => {
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||||
@@ -109,17 +131,23 @@ describe("settings storage", () => {
|
|||||||
saveSettings(paths, {
|
saveSettings(paths, {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
rememberToken: false,
|
rememberToken: false,
|
||||||
token: "rd-token",
|
token: "rd-token",
|
||||||
megaLogin: "mega-user",
|
megaLogin: "mega-user",
|
||||||
megaPassword: "mega-pass",
|
megaPassword: "mega-pass",
|
||||||
bestToken: "best-token",
|
megaCredentials: "mega-user:mega-pass",
|
||||||
|
megaDebridApiCredentials: "mega-user:mega-pass",
|
||||||
|
megaDebridWebCredentials: "web-user:web-pass",
|
||||||
|
bestToken: "best-token",
|
||||||
allDebridToken: "all-token"
|
allDebridToken: "all-token"
|
||||||
});
|
});
|
||||||
|
|
||||||
const raw = JSON.parse(fs.readFileSync(paths.configFile, "utf8")) as Record<string, unknown>;
|
const raw = JSON.parse(fs.readFileSync(paths.configFile, "utf8")) as Record<string, unknown>;
|
||||||
expect(raw.token).toBe("");
|
expect(raw.token).toBe("");
|
||||||
expect(raw.megaLogin).toBe("");
|
expect(raw.megaLogin).toBe("");
|
||||||
expect(raw.megaPassword).toBe("");
|
expect(raw.megaPassword).toBe("");
|
||||||
|
expect(raw.megaCredentials).toBe("");
|
||||||
|
expect(raw.megaDebridApiCredentials).toBe("");
|
||||||
|
expect(raw.megaDebridWebCredentials).toBe("");
|
||||||
expect(raw.bestToken).toBe("");
|
expect(raw.bestToken).toBe("");
|
||||||
expect(raw.allDebridToken).toBe("");
|
expect(raw.allDebridToken).toBe("");
|
||||||
|
|
||||||
@@ -127,7 +155,10 @@ describe("settings storage", () => {
|
|||||||
expect(loaded.rememberToken).toBe(false);
|
expect(loaded.rememberToken).toBe(false);
|
||||||
expect(loaded.token).toBe("");
|
expect(loaded.token).toBe("");
|
||||||
expect(loaded.megaLogin).toBe("");
|
expect(loaded.megaLogin).toBe("");
|
||||||
expect(loaded.megaPassword).toBe("");
|
expect(loaded.megaPassword).toBe("");
|
||||||
|
expect(loaded.megaCredentials).toBe("");
|
||||||
|
expect(loaded.megaDebridApiCredentials).toBe("");
|
||||||
|
expect(loaded.megaDebridWebCredentials).toBe("");
|
||||||
expect(loaded.bestToken).toBe("");
|
expect(loaded.bestToken).toBe("");
|
||||||
expect(loaded.allDebridToken).toBe("");
|
expect(loaded.allDebridToken).toBe("");
|
||||||
});
|
});
|
||||||
@@ -275,7 +306,7 @@ describe("settings storage", () => {
|
|||||||
expect(webNormalized.hosterRouting.rapidgator).toBe("megadebrid-web");
|
expect(webNormalized.hosterRouting.rapidgator).toBe("megadebrid-web");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("migriert eine pre-v1.6.90-Config (Mega-Creds, beide Enable-Flags fehlen) zu aktiviertem Mega-Debrid statt es still auf false zu setzen", () => {
|
it("migriert eine pre-v1.6.90-Config (Mega-Creds, beide Enable-Flags fehlen) zu aktiviertem Mega-Debrid statt es still auf false zu setzen", () => {
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||||
tempDirs.push(dir);
|
tempDirs.push(dir);
|
||||||
const paths = createStoragePaths(dir);
|
const paths = createStoragePaths(dir);
|
||||||
@@ -304,10 +335,72 @@ describe("settings storage", () => {
|
|||||||
fs.writeFileSync(paths2.configFile, JSON.stringify(legacyWeb), "utf8");
|
fs.writeFileSync(paths2.configFile, JSON.stringify(legacyWeb), "utf8");
|
||||||
const loadedWeb = loadSettings(paths2);
|
const loadedWeb = loadSettings(paths2);
|
||||||
expect(loadedWeb.megaDebridApiEnabled).toBe(false);
|
expect(loadedWeb.megaDebridApiEnabled).toBe(false);
|
||||||
expect(loadedWeb.megaDebridWebEnabled).toBe(true);
|
expect(loadedWeb.megaDebridWebEnabled).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("re-aktiviert KEINE bewusst deaktivierten Mega-Flags und migriert nicht ohne Mega-Creds", () => {
|
it("migrates an explicitly Web-only legacy account even when API remains preferred", () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
const paths = createStoragePaths(dir);
|
||||||
|
fs.writeFileSync(paths.configFile, JSON.stringify({
|
||||||
|
rememberToken: true,
|
||||||
|
megaCredentials: "web-user:web-pass",
|
||||||
|
megaDebridPreferApi: true,
|
||||||
|
megaDebridApiEnabled: false,
|
||||||
|
megaDebridWebEnabled: true
|
||||||
|
}), "utf8");
|
||||||
|
|
||||||
|
const loaded = loadSettings(paths);
|
||||||
|
|
||||||
|
expect(loaded.megaDebridApiCredentials).toBe("");
|
||||||
|
expect(loaded.megaDebridWebCredentials).toBe("web-user:web-pass");
|
||||||
|
expect(loaded.megaDebridApiEnabled).toBe(false);
|
||||||
|
expect(loaded.megaDebridWebEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates legacy disabled Mega-Debrid accounts into the selected mode", () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
const paths = createStoragePaths(dir);
|
||||||
|
const accountId = getMegaDebridAccountId("disabled-user");
|
||||||
|
fs.writeFileSync(paths.configFile, JSON.stringify({
|
||||||
|
rememberToken: true,
|
||||||
|
megaCredentials: "disabled-user:disabled-pass",
|
||||||
|
megaDebridPreferApi: true,
|
||||||
|
megaDebridApiEnabled: true,
|
||||||
|
megaDebridWebEnabled: false,
|
||||||
|
megaDebridDisabledAccountIds: [accountId]
|
||||||
|
}), "utf8");
|
||||||
|
|
||||||
|
const loaded = loadSettings(paths);
|
||||||
|
|
||||||
|
expect(loaded.megaDebridApiDisabledAccountIds).toEqual([accountId]);
|
||||||
|
expect(loaded.megaDebridWebDisabledAccountIds).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves explicit mode-specific disabled Mega-Debrid accounts", () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
const paths = createStoragePaths(dir);
|
||||||
|
const accountId = getMegaDebridAccountId("shared-user");
|
||||||
|
fs.writeFileSync(paths.configFile, JSON.stringify({
|
||||||
|
rememberToken: true,
|
||||||
|
megaDebridApiCredentials: "shared-user:api-pass",
|
||||||
|
megaDebridWebCredentials: "shared-user:web-pass",
|
||||||
|
megaDebridApiEnabled: true,
|
||||||
|
megaDebridWebEnabled: true,
|
||||||
|
megaDebridDisabledAccountIds: [accountId],
|
||||||
|
megaDebridApiDisabledAccountIds: [],
|
||||||
|
megaDebridWebDisabledAccountIds: [accountId]
|
||||||
|
}), "utf8");
|
||||||
|
|
||||||
|
const loaded = loadSettings(paths);
|
||||||
|
|
||||||
|
expect(loaded.megaDebridApiDisabledAccountIds).toEqual([]);
|
||||||
|
expect(loaded.megaDebridWebDisabledAccountIds).toEqual([accountId]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-aktiviert KEINE bewusst deaktivierten Mega-Flags und migriert nicht ohne Mega-Creds", () => {
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||||
tempDirs.push(dir);
|
tempDirs.push(dir);
|
||||||
const paths = createStoragePaths(dir);
|
const paths = createStoragePaths(dir);
|
||||||
|
|||||||
+186
-174
@@ -1,144 +1,144 @@
|
|||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { runLatestUpdateCheck, shouldApplyUpdateCheckResult } from "../src/renderer/App";
|
import { runLatestUpdateCheck, shouldApplyUpdateCheckResult } from "../src/renderer/App";
|
||||||
import type { UpdateCheckResult } from "../src/shared/types";
|
import type { UpdateCheckResult } from "../src/shared/types";
|
||||||
import { AppHeader } from "../src/renderer/shell/AppHeader";
|
import { AppHeader } from "../src/renderer/shell/AppHeader";
|
||||||
import { getUpdateDialogFocusTarget, UpdateExperience } from "../src/renderer/shell/UpdateExperience";
|
import { getUpdateDialogFocusTarget, UpdateExperience } from "../src/renderer/shell/UpdateExperience";
|
||||||
|
|
||||||
const callbacks = {
|
const callbacks = {
|
||||||
onOpen: () => {},
|
onOpen: () => {},
|
||||||
onClose: () => {},
|
onClose: () => {},
|
||||||
onInstall: () => {},
|
onInstall: () => {},
|
||||||
onLater: () => {}
|
onLater: () => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("update experience", () => {
|
describe("update experience", () => {
|
||||||
it("renders the available update and prompt as one accessible experience", () => {
|
it("renders the available update and prompt as one accessible experience", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<UpdateExperience
|
<UpdateExperience
|
||||||
available
|
available
|
||||||
currentVersion="v2.0.12"
|
currentVersion="v2.0.12"
|
||||||
latestTag="v9.9.9"
|
latestTag="v9.9.9"
|
||||||
open
|
open
|
||||||
progress={0}
|
progress={0}
|
||||||
releaseNotes="Changes"
|
releaseNotes="Changes"
|
||||||
state="prompt"
|
state="prompt"
|
||||||
{...callbacks}
|
{...callbacks}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(html).toContain("aria-label=\"Update verfügbar\"");
|
expect(html).toContain("aria-label=\"Update verfügbar\"");
|
||||||
expect(html).toContain("role=\"tooltip\"");
|
expect(html).toContain("role=\"tooltip\"");
|
||||||
expect(html).toContain("Eine neue Version ist bereit. Klicke hier, um sie zu installieren.");
|
expect(html).toContain("Eine neue Version ist bereit. Klicke hier, um sie zu installieren.");
|
||||||
expect(html).toContain("role=\"dialog\"");
|
expect(html).toContain("role=\"dialog\"");
|
||||||
expect(html).toContain("aria-modal=\"true\"");
|
expect(html).toContain("aria-modal=\"true\"");
|
||||||
expect(html).toContain("Update installieren");
|
expect(html).toContain("Update installieren");
|
||||||
expect(html).toContain("Jetzt aktualisieren");
|
expect(html).toContain("Jetzt aktualisieren");
|
||||||
expect(html).toContain("Später");
|
expect(html).toContain("Später");
|
||||||
expect(html).toContain("Changes");
|
expect(html).toContain("Changes");
|
||||||
expect(html).toContain("<details");
|
expect(html).toContain("<details");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps the update affordance but removes the dialog when the prompt is closed", () => {
|
it("keeps the update affordance but removes the dialog when the prompt is closed", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<UpdateExperience
|
<UpdateExperience
|
||||||
available
|
available
|
||||||
currentVersion="v2.0.12"
|
currentVersion="v2.0.12"
|
||||||
latestTag="v9.9.9"
|
latestTag="v9.9.9"
|
||||||
open={false}
|
open={false}
|
||||||
progress={0}
|
progress={0}
|
||||||
releaseNotes="Changes"
|
releaseNotes="Changes"
|
||||||
state="prompt"
|
state="prompt"
|
||||||
{...callbacks}
|
{...callbacks}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(html).toContain("aria-label=\"Update verfügbar\"");
|
expect(html).toContain("aria-label=\"Update verfügbar\"");
|
||||||
expect(html).not.toContain("role=\"dialog\"");
|
expect(html).not.toContain("role=\"dialog\"");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders active progress without controls that could close the installation", () => {
|
it("renders active progress without controls that could close the installation", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<UpdateExperience
|
<UpdateExperience
|
||||||
available
|
available
|
||||||
currentVersion="v2.0.12"
|
currentVersion="v2.0.12"
|
||||||
latestTag="v9.9.9"
|
latestTag="v9.9.9"
|
||||||
open
|
open
|
||||||
progress={{ percent: 47, text: "Update-Download: 47% (47 MB / 100 MB)" }}
|
progress={{ percent: 47, text: "Update-Download: 47% (47 MB / 100 MB)" }}
|
||||||
releaseNotes=""
|
releaseNotes=""
|
||||||
state="downloading"
|
state="downloading"
|
||||||
{...callbacks}
|
{...callbacks}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(html).toContain("Update-Download: 47% (47 MB / 100 MB)");
|
expect(html).toContain("Update-Download: 47% (47 MB / 100 MB)");
|
||||||
expect(html).toContain("aria-valuenow=\"47\"");
|
expect(html).toContain("aria-valuenow=\"47\"");
|
||||||
expect(html).not.toContain("Später");
|
expect(html).not.toContain("Später");
|
||||||
expect(html).not.toContain("Jetzt aktualisieren");
|
expect(html).not.toContain("Jetzt aktualisieren");
|
||||||
expect(html).not.toContain("aria-label=\"Schließen\"");
|
expect(html).not.toContain("aria-label=\"Schließen\"");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves the original installation error in the reusable dialog", () => {
|
it("preserves the original installation error in the reusable dialog", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<UpdateExperience
|
<UpdateExperience
|
||||||
available
|
available
|
||||||
currentVersion="v2.0.12"
|
currentVersion="v2.0.12"
|
||||||
latestTag="v9.9.9"
|
latestTag="v9.9.9"
|
||||||
open
|
open
|
||||||
progress={{ percent: null, text: "Update-Fehler: Originale Prüfsummenmeldung" }}
|
progress={{ percent: null, text: "Update-Fehler: Originale Prüfsummenmeldung" }}
|
||||||
releaseNotes=""
|
releaseNotes=""
|
||||||
state="error"
|
state="error"
|
||||||
{...callbacks}
|
{...callbacks}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(html).toContain("Update-Fehler: Originale Prüfsummenmeldung");
|
expect(html).toContain("Update-Fehler: Originale Prüfsummenmeldung");
|
||||||
expect(html).toContain("aria-label=\"Schließen\"");
|
expect(html).toContain("aria-label=\"Schließen\"");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders nothing when no update is available and no dialog is active", () => {
|
it("renders nothing when no update is available and no dialog is active", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<UpdateExperience
|
<UpdateExperience
|
||||||
available={false}
|
available={false}
|
||||||
currentVersion="v2.0.12"
|
currentVersion="v2.0.12"
|
||||||
latestTag=""
|
latestTag=""
|
||||||
open={false}
|
open={false}
|
||||||
progress={0}
|
progress={0}
|
||||||
releaseNotes=""
|
releaseNotes=""
|
||||||
state="prompt"
|
state="prompt"
|
||||||
{...callbacks}
|
{...callbacks}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(html).toBe("");
|
expect(html).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("places the update affordance in the accessible global header action group", () => {
|
it("places the update affordance in the accessible global header action group", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<AppHeader
|
<AppHeader
|
||||||
activeView="downloads"
|
activeView="downloads"
|
||||||
actions={(
|
actions={(
|
||||||
<UpdateExperience
|
<UpdateExperience
|
||||||
available
|
available
|
||||||
currentVersion="v2.0.12"
|
currentVersion="v2.0.12"
|
||||||
latestTag="v9.9.9"
|
latestTag="v9.9.9"
|
||||||
open={false}
|
open={false}
|
||||||
progress={0}
|
progress={0}
|
||||||
releaseNotes=""
|
releaseNotes=""
|
||||||
state="prompt"
|
state="prompt"
|
||||||
{...callbacks}
|
{...callbacks}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onViewChange={() => {}}
|
onViewChange={() => {}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(html).toContain("role=\"group\"");
|
expect(html).toContain("role=\"group\"");
|
||||||
expect(html).toContain("aria-label=\"Globale Aktionen\"");
|
expect(html).toContain("aria-label=\"Globale Aktionen\"");
|
||||||
expect(html).toContain("aria-label=\"Update verfügbar\"");
|
expect(html).toContain("aria-label=\"Update verfügbar\"");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses the specified transient and modal elevation tokens", () => {
|
it("uses the specified transient and modal elevation tokens", () => {
|
||||||
const css = readFileSync(new URL("../src/renderer/shell/shell.css", import.meta.url), "utf8");
|
const css = readFileSync(new URL("../src/renderer/shell/shell.css", import.meta.url), "utf8");
|
||||||
|
|
||||||
@@ -146,37 +146,49 @@ describe("update experience", () => {
|
|||||||
expect(css).toMatch(/\.md-update-dialog\s*\{[^}]*box-shadow:\s*0 12px 40px rgb\(0 0 0 \/ 45%\)/s);
|
expect(css).toMatch(/\.md-update-dialog\s*\{[^}]*box-shadow:\s*0 12px 40px rgb\(0 0 0 \/ 45%\)/s);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps forward and reverse tabbing inside the update dialog", () => {
|
it("uses a light-blue update affordance and a bounded scrollable changelog", () => {
|
||||||
expect(getUpdateDialogFocusTarget(false, -1, 4)).toBe(0);
|
const css = readFileSync(new URL("../src/renderer/shell/shell.css", import.meta.url), "utf8");
|
||||||
expect(getUpdateDialogFocusTarget(true, -1, 4)).toBe(3);
|
const theme = readFileSync(new URL("../src/renderer/theme.css", import.meta.url), "utf8");
|
||||||
expect(getUpdateDialogFocusTarget(false, 3, 4)).toBe(0);
|
|
||||||
expect(getUpdateDialogFocusTarget(true, 0, 4)).toBe(3);
|
expect(theme).toMatch(/--ui-update:\s*#BAD0FC;/);
|
||||||
expect(getUpdateDialogFocusTarget(false, 1, 4)).toBeNull();
|
expect(theme).toMatch(/--ui-update-hover:\s*#8AA5DC;/);
|
||||||
expect(getUpdateDialogFocusTarget(false, -1, 0)).toBeNull();
|
expect(theme).toMatch(/--ui-update-text:\s*#181A1F;/);
|
||||||
|
expect(css).toMatch(/\.md-update-trigger\s*\{[^}]*background:\s*var\(--ui-update\);[^}]*color:\s*var\(--ui-update-text\);/s);
|
||||||
|
expect(css).toMatch(/\.md-update-trigger:hover\s*\{[^}]*background:\s*var\(--ui-update-hover\);/s);
|
||||||
|
expect(css).toMatch(/\.md-update-release-notes pre\s*\{[^}]*max-height:\s*min\(360px, 45vh\);[^}]*overflow-y:\s*auto;/s);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects stale update-check completions without discarding the latest state", () => {
|
it("keeps forward and reverse tabbing inside the update dialog", () => {
|
||||||
expect(shouldApplyUpdateCheckResult(4, 4)).toBe(true);
|
expect(getUpdateDialogFocusTarget(false, -1, 4)).toBe(0);
|
||||||
expect(shouldApplyUpdateCheckResult(3, 4)).toBe(false);
|
expect(getUpdateDialogFocusTarget(true, -1, 4)).toBe(3);
|
||||||
expect(shouldApplyUpdateCheckResult(4, 5)).toBe(false);
|
expect(getUpdateDialogFocusTarget(false, 3, 4)).toBe(0);
|
||||||
});
|
expect(getUpdateDialogFocusTarget(true, 0, 4)).toBe(3);
|
||||||
|
expect(getUpdateDialogFocusTarget(false, 1, 4)).toBeNull();
|
||||||
it("applies only the latest result when update checks complete out of order", async () => {
|
expect(getUpdateDialogFocusTarget(false, -1, 0)).toBeNull();
|
||||||
const generation = { current: 0 };
|
});
|
||||||
const applied: string[] = [];
|
|
||||||
let finishStartup: ((result: UpdateCheckResult) => void) | undefined;
|
it("rejects stale update-check completions without discarding the latest state", () => {
|
||||||
let finishManual: ((result: UpdateCheckResult) => void) | undefined;
|
expect(shouldApplyUpdateCheckResult(4, 4)).toBe(true);
|
||||||
const startup = new Promise<UpdateCheckResult>((resolve) => { finishStartup = resolve; });
|
expect(shouldApplyUpdateCheckResult(3, 4)).toBe(false);
|
||||||
const manual = new Promise<UpdateCheckResult>((resolve) => { finishManual = resolve; });
|
expect(shouldApplyUpdateCheckResult(4, 5)).toBe(false);
|
||||||
const apply = (result: UpdateCheckResult): void => { applied.push(result.latestTag); };
|
});
|
||||||
|
|
||||||
const startupRun = runLatestUpdateCheck(generation, () => startup, apply);
|
it("applies only the latest result when update checks complete out of order", async () => {
|
||||||
const manualRun = runLatestUpdateCheck(generation, () => manual, apply);
|
const generation = { current: 0 };
|
||||||
finishManual?.({ updateAvailable: true, currentVersion: "2.0.12", latestVersion: "9.9.9", latestTag: "v9.9.9", releaseUrl: "https://example.test/v9.9.9" });
|
const applied: string[] = [];
|
||||||
await manualRun;
|
let finishStartup: ((result: UpdateCheckResult) => void) | undefined;
|
||||||
finishStartup?.({ updateAvailable: false, currentVersion: "2.0.12", latestVersion: "2.0.12", latestTag: "v2.0.12", releaseUrl: "https://example.test/v2.0.12" });
|
let finishManual: ((result: UpdateCheckResult) => void) | undefined;
|
||||||
await startupRun;
|
const startup = new Promise<UpdateCheckResult>((resolve) => { finishStartup = resolve; });
|
||||||
|
const manual = new Promise<UpdateCheckResult>((resolve) => { finishManual = resolve; });
|
||||||
expect(applied).toEqual(["v9.9.9"]);
|
const apply = (result: UpdateCheckResult): void => { applied.push(result.latestTag); };
|
||||||
});
|
|
||||||
});
|
const startupRun = runLatestUpdateCheck(generation, () => startup, apply);
|
||||||
|
const manualRun = runLatestUpdateCheck(generation, () => manual, apply);
|
||||||
|
finishManual?.({ updateAvailable: true, currentVersion: "2.0.12", latestVersion: "9.9.9", latestTag: "v9.9.9", releaseUrl: "https://example.test/v9.9.9" });
|
||||||
|
await manualRun;
|
||||||
|
finishStartup?.({ updateAvailable: false, currentVersion: "2.0.12", latestVersion: "2.0.12", latestTag: "v2.0.12", releaseUrl: "https://example.test/v2.0.12" });
|
||||||
|
await startupRun;
|
||||||
|
|
||||||
|
expect(applied).toEqual(["v9.9.9"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+48
-2
@@ -79,7 +79,7 @@ describe("update", () => {
|
|||||||
expect(result.updateAvailable).toBe(false);
|
expect(result.updateAvailable).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("picks setup executable asset from release list", async () => {
|
it("picks setup executable asset from release list", async () => {
|
||||||
globalThis.fetch = (async (): Promise<Response> => new Response(
|
globalThis.fetch = (async (): Promise<Response> => new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
tag_name: "v9.9.9",
|
tag_name: "v9.9.9",
|
||||||
@@ -106,7 +106,53 @@ describe("update", () => {
|
|||||||
expect(result.updateAvailable).toBe(true);
|
expect(result.updateAvailable).toBe(true);
|
||||||
expect(result.setupAssetUrl).toBe("https://example.invalid/setup.exe");
|
expect(result.setupAssetUrl).toBe("https://example.invalid/setup.exe");
|
||||||
expect(result.setupAssetName).toBe("Real-Debrid-Downloader-Setup-9.9.9.exe");
|
expect(result.setupAssetName).toBe("Real-Debrid-Downloader-Setup-9.9.9.exe");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("combines every stable release note newer than the installed version", async () => {
|
||||||
|
const [major = 2, minor = 0, patch = 0] = parseVersionParts(APP_VERSION);
|
||||||
|
const version = (offset: number): string => `${major}.${minor}.${patch + offset}`;
|
||||||
|
const requestedUrls: string[] = [];
|
||||||
|
|
||||||
|
globalThis.fetch = (async (input: RequestInfo | URL): Promise<Response> => {
|
||||||
|
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||||
|
requestedUrls.push(url);
|
||||||
|
if (url.endsWith("/releases/latest")) {
|
||||||
|
return new Response(JSON.stringify({
|
||||||
|
tag_name: `v${version(3)}`,
|
||||||
|
html_url: `https://github.com/owner/repo/releases/tag/v${version(3)}`,
|
||||||
|
body: "Latest changes",
|
||||||
|
assets: []
|
||||||
|
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify([
|
||||||
|
{ tag_name: `v${version(3)}`, body: "Latest changes", draft: false, prerelease: false },
|
||||||
|
{ tag_name: `v${version(2)}`, body: "Middle changes", draft: false, prerelease: false },
|
||||||
|
{ tag_name: `v${version(1)}`, body: "First missed changes", draft: false, prerelease: false },
|
||||||
|
{ tag_name: `v${version(4)}`, body: "Draft changes", draft: true, prerelease: false },
|
||||||
|
{ tag_name: `v${version(5)}`, body: "Prerelease changes", draft: false, prerelease: true },
|
||||||
|
{ tag_name: `v${version(0)}`, body: "Installed changes", draft: false, prerelease: false },
|
||||||
|
{ tag_name: `v${version(-1)}`, body: "Older changes", draft: false, prerelease: false }
|
||||||
|
]), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||||
|
}) as typeof fetch;
|
||||||
|
|
||||||
|
const result = await checkGitHubUpdate("owner/repo");
|
||||||
|
|
||||||
|
expect(requestedUrls).toEqual([
|
||||||
|
"https://api.github.com/repos/owner/repo/releases/latest",
|
||||||
|
"https://api.github.com/repos/owner/repo/releases?per_page=100&page=1"
|
||||||
|
]);
|
||||||
|
expect(result.releaseNotes).toBe([
|
||||||
|
`v${version(3)}`,
|
||||||
|
"Latest changes",
|
||||||
|
"",
|
||||||
|
`v${version(2)}`,
|
||||||
|
"Middle changes",
|
||||||
|
"",
|
||||||
|
`v${version(1)}`,
|
||||||
|
"First missed changes"
|
||||||
|
].join("\n"));
|
||||||
|
});
|
||||||
|
|
||||||
it("uses silent NSIS install flags with auto-run after update", () => {
|
it("uses silent NSIS install flags with auto-run after update", () => {
|
||||||
expect(buildInstallerLaunchArgs()).toEqual(["/S", "--updated", "--force-run"]);
|
expect(buildInstallerLaunchArgs()).toEqual(["/S", "--updated", "--force-run"]);
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ function createSettings(): AppSettings {
|
|||||||
megaPassword: "visual-password",
|
megaPassword: "visual-password",
|
||||||
language: "de",
|
language: "de",
|
||||||
megaCredentials: `${megaLogin}:visual-password`,
|
megaCredentials: `${megaLogin}:visual-password`,
|
||||||
|
megaDebridApiCredentials: `${megaLogin}:visual-password`,
|
||||||
|
megaDebridWebCredentials: "",
|
||||||
megaDebridApiEnabled: true,
|
megaDebridApiEnabled: true,
|
||||||
megaDebridWebEnabled: true,
|
megaDebridWebEnabled: true,
|
||||||
megaDebridPreferApi: true,
|
megaDebridPreferApi: true,
|
||||||
@@ -173,7 +175,9 @@ function createSettings(): AppSettings {
|
|||||||
[debridLinkKeys[0].id]: 1099511627776,
|
[debridLinkKeys[0].id]: 1099511627776,
|
||||||
[debridLinkKeys[1].id]: 549755813888
|
[debridLinkKeys[1].id]: 549755813888
|
||||||
},
|
},
|
||||||
megaDebridDisabledAccountIds: [],
|
megaDebridDisabledAccountIds: [],
|
||||||
|
megaDebridApiDisabledAccountIds: [],
|
||||||
|
megaDebridWebDisabledAccountIds: [],
|
||||||
megaDebridAccountDailyLimitBytes: {
|
megaDebridAccountDailyLimitBytes: {
|
||||||
[megaAccountId]: 322122547200
|
[megaAccountId]: 322122547200
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user