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:
@@ -46,9 +46,11 @@ export function defaultSettings(): AppSettings {
|
||||
language: "en",
|
||||
token: "",
|
||||
realDebridUseWebLogin: false,
|
||||
megaLogin: "",
|
||||
megaPassword: "",
|
||||
megaCredentials: "",
|
||||
megaLogin: "",
|
||||
megaPassword: "",
|
||||
megaCredentials: "",
|
||||
megaDebridApiCredentials: "",
|
||||
megaDebridWebCredentials: "",
|
||||
megaDebridApiEnabled: false,
|
||||
megaDebridWebEnabled: false,
|
||||
megaDebridPreferApi: true,
|
||||
@@ -133,7 +135,9 @@ export function defaultSettings(): AppSettings {
|
||||
debridLinkApiKeyDailyLimitBytes: {},
|
||||
debridLinkApiKeyDailyUsageBytes: {},
|
||||
debridLinkApiKeyTotalUsageBytes: {},
|
||||
megaDebridDisabledAccountIds: [],
|
||||
megaDebridDisabledAccountIds: [],
|
||||
megaDebridApiDisabledAccountIds: [],
|
||||
megaDebridWebDisabledAccountIds: [],
|
||||
megaDebridAccountDailyLimitBytes: {},
|
||||
megaDebridAccountDailyUsageBytes: {},
|
||||
megaDebridAccountTotalUsageBytes: {},
|
||||
|
||||
+41
-43
@@ -1,5 +1,6 @@
|
||||
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 { isDebridLinkApiKeyDailyLimitReached, isMegaDebridAccountDisabled, isMegaDebridAccountDailyLimitReached, isProviderDailyLimitReached } from "../shared/provider-daily-limits";
|
||||
import { isMegaDebridResolveFailure, germanMegaDebridResolveReason } from "../shared/mega-debrid-errors";
|
||||
@@ -557,17 +558,7 @@ const PROVIDER_LABELS: Record<DebridProvider, string> = {
|
||||
linksnappy: "LinkSnappy"
|
||||
};
|
||||
|
||||
function extractHosterFromUrl(url: string): string {
|
||||
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 {
|
||||
interface ProviderUnrestrictedLink extends UnrestrictedLink {
|
||||
provider: DebridProvider;
|
||||
providerLabel: string;
|
||||
}
|
||||
@@ -595,7 +586,9 @@ function cloneSettings(settings: AppSettings): AppSettings {
|
||||
debridLinkApiKeyDailyLimitBytes: { ...(settings.debridLinkApiKeyDailyLimitBytes || {}) },
|
||||
debridLinkApiKeyDailyUsageBytes: { ...(settings.debridLinkApiKeyDailyUsageBytes || {}) },
|
||||
debridLinkApiKeyTotalUsageBytes: { ...(settings.debridLinkApiKeyTotalUsageBytes || {}) },
|
||||
megaDebridDisabledAccountIds: [...(settings.megaDebridDisabledAccountIds || [])],
|
||||
megaDebridDisabledAccountIds: [...(settings.megaDebridDisabledAccountIds || [])],
|
||||
megaDebridApiDisabledAccountIds: [...(settings.megaDebridApiDisabledAccountIds || [])],
|
||||
megaDebridWebDisabledAccountIds: [...(settings.megaDebridWebDisabledAccountIds || [])],
|
||||
megaDebridAccountDailyLimitBytes: { ...(settings.megaDebridAccountDailyLimitBytes || {}) },
|
||||
megaDebridAccountDailyUsageBytes: { ...(settings.megaDebridAccountDailyUsageBytes || {}) },
|
||||
megaDebridAccountTotalUsageBytes: { ...(settings.megaDebridAccountTotalUsageBytes || {}) }
|
||||
@@ -612,26 +605,27 @@ export function getAvailableDebridLinkApiKeys(settings: AppSettings, epochMs = D
|
||||
);
|
||||
}
|
||||
|
||||
export function getAvailableMegaDebridAccounts(settings: AppSettings, epochMs = Date.now()): MegaDebridAccountEntry[] {
|
||||
return getMegaDebridAccountList(settings).filter(
|
||||
(entry) => !isMegaDebridAccountDisabled(settings, entry.id) && !isMegaDebridAccountDailyLimitReached(settings, entry.id, epochMs)
|
||||
);
|
||||
}
|
||||
|
||||
function getMegaDebridAccountList(settings: AppSettings): MegaDebridAccountEntry[] {
|
||||
const multiAccounts = parseMegaDebridAccounts(settings.megaCredentials || "");
|
||||
if (multiAccounts.length > 0) {
|
||||
return multiAccounts;
|
||||
}
|
||||
if (settings.megaLogin?.trim() && settings.megaPassword?.trim()) {
|
||||
return parseMegaDebridAccounts(settings.megaLogin.trim(), settings.megaPassword.trim());
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function hasMegaDebridCredentials(settings: AppSettings): boolean {
|
||||
return getMegaDebridAccountList(settings).length > 0;
|
||||
}
|
||||
export function getAvailableMegaDebridAccounts(settings: AppSettings, mode: MegaDebridAccountMode, epochMs = Date.now()): MegaDebridAccountEntry[] {
|
||||
return getMegaDebridAccountList(settings, mode).filter(
|
||||
(entry) => !isMegaDebridAccountDisabled(settings, entry.id, mode) && !isMegaDebridAccountDailyLimitReached(settings, entry.id, epochMs)
|
||||
);
|
||||
}
|
||||
|
||||
function getMegaDebridAccountList(settings: AppSettings, mode: MegaDebridAccountMode): MegaDebridAccountEntry[] {
|
||||
const multiAccounts = getMegaDebridAccountsForMode(settings, mode);
|
||||
if (multiAccounts.length > 0) {
|
||||
return multiAccounts;
|
||||
}
|
||||
const hasDedicatedPools = typeof settings.megaDebridApiCredentials === "string" || typeof settings.megaDebridWebCredentials === "string";
|
||||
if (!hasDedicatedPools && settings.megaLogin?.trim() && settings.megaPassword?.trim()) {
|
||||
return parseMegaDebridAccounts(settings.megaLogin.trim(), settings.megaPassword.trim());
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function hasMegaDebridCredentials(settings: AppSettings): boolean {
|
||||
return parseMegaDebridAccounts(mergeMegaDebridCredentialPools(settings.megaDebridApiCredentials || "", settings.megaDebridWebCredentials || "") || settings.megaCredentials || "").length > 0;
|
||||
}
|
||||
|
||||
function isMegaDebridModeEnabled(settings: AppSettings, mode: "api" | "web"): boolean {
|
||||
if (mode === "api") {
|
||||
@@ -2039,12 +2033,12 @@ class MegaDebridClient {
|
||||
megaWebUnrestrict: MegaWebUnrestrictor | undefined,
|
||||
signal?: AbortSignal
|
||||
): Promise<UnrestrictedLink> {
|
||||
const accounts = getMegaDebridAccountList(settings);
|
||||
const accounts = getMegaDebridAccountList(settings, mode);
|
||||
if (accounts.length === 0) {
|
||||
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)");
|
||||
}
|
||||
|
||||
@@ -2087,7 +2081,7 @@ class MegaDebridClient {
|
||||
const accountLabel = ` (${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`);
|
||||
logAccountRotation("INFO", providerName, rotationLabel, "SKIP_DISABLED", { reason: "manually disabled" });
|
||||
continue;
|
||||
@@ -2172,7 +2166,7 @@ class MegaDebridClient {
|
||||
if (candidate.account.id === account.id) {
|
||||
return count;
|
||||
}
|
||||
if (isMegaDebridAccountDisabled(settings, candidate.account.id)) {
|
||||
if (isMegaDebridAccountDisabled(settings, candidate.account.id, mode)) {
|
||||
return count;
|
||||
}
|
||||
if (isMegaDebridAccountDailyLimitReached(settings, candidate.account.id)) {
|
||||
@@ -2254,7 +2248,7 @@ class MegaDebridClient {
|
||||
let nextLabel = "ENDE";
|
||||
for (let nextPos = orderPos + 1; nextPos < orderedEntries.length; nextPos += 1) {
|
||||
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})`;
|
||||
break;
|
||||
}
|
||||
@@ -3824,9 +3818,10 @@ export class DebridService {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (effectiveProvider === "megadebrid-api" || effectiveProvider === "megadebrid-web") {
|
||||
const configuredAccounts = getMegaDebridAccountList(settings);
|
||||
if (configuredAccounts.length > 0 && getAvailableMegaDebridAccounts(settings).length === 0) {
|
||||
if (effectiveProvider === "megadebrid-api" || effectiveProvider === "megadebrid-web") {
|
||||
const mode = effectiveProvider === "megadebrid-web" ? "web" : "api";
|
||||
const configuredAccounts = getMegaDebridAccountList(settings, mode);
|
||||
if (configuredAccounts.length > 0 && getAvailableMegaDebridAccounts(settings, mode).length === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -3842,8 +3837,11 @@ export class DebridService {
|
||||
if (effectiveProvider === "debridlink" && parseDebridLinkApiKeys(settings.debridLinkApiKeys).length > 0 && getAvailableDebridLinkApiKeys(settings).length === 0) {
|
||||
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) {
|
||||
return "Mega-Debrid nicht verfuegbar (alle aktiven Accounts deaktiviert oder ausgeschopft)";
|
||||
if (effectiveProvider === "megadebrid-api" || effectiveProvider === "megadebrid-web") {
|
||||
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`;
|
||||
}
|
||||
|
||||
@@ -21,8 +21,10 @@ import {
|
||||
StartConflictEntry,
|
||||
StartConflictResolutionResult,
|
||||
UiSnapshot, DebridAccountStatus } from "../shared/types";
|
||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||
import { isMegaDebridTransientResolveFailure, germanMegaDebridResolveReason } from "../shared/mega-debrid-errors";
|
||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||
import { extractHosterFromUrl } from "../shared/hoster";
|
||||
import { isMegaDebridTransientResolveFailure, germanMegaDebridResolveReason } from "../shared/mega-debrid-errors";
|
||||
import { getMegaDebridAccountsForMode } from "../shared/mega-debrid-accounts";
|
||||
import {
|
||||
addDebridLinkApiKeyDailyUsageBytes,
|
||||
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);
|
||||
}
|
||||
|
||||
function extractHosterKey(link: string): string {
|
||||
try {
|
||||
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 extractHosterKey(link: string): string {
|
||||
return extractHosterFromUrl(link);
|
||||
}
|
||||
|
||||
function isLargeBinaryLikePath(filePath: string): boolean {
|
||||
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 {
|
||||
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.totalCompletedFilesAllTime = Math.max(next.totalCompletedFilesAllTime || 0, this.settings.totalCompletedFilesAllTime || 0);
|
||||
const now = nowMs();
|
||||
@@ -2184,8 +2199,11 @@ export class DownloadManager extends EventEmitter {
|
||||
this.invalidateSettingsSnapshotCache();
|
||||
this.runtimePersistedTotalMs = this.settings.totalRuntimeAllTimeMs || 0;
|
||||
this.runtimePersistedAt = now;
|
||||
this.ensureProviderDailyUsageFresh(nowMs());
|
||||
this.debridService.setSettings(next);
|
||||
this.ensureProviderDailyUsageFresh(nowMs());
|
||||
if (previousMegaWebCredentials !== nextMegaWebCredentials) {
|
||||
this.invalidateMegaSessionFn?.();
|
||||
}
|
||||
this.debridService.setSettings(next);
|
||||
this.allDebridHostInfoCache.clear();
|
||||
|
||||
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.linkSnappyLogin + "|" + previous.linkSnappyPassword, next: next.linkSnappyLogin + "|" + next.linkSnappyPassword, providers: ["linksnappy"] },
|
||||
{ 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;
|
||||
for (const change of credChanges) {
|
||||
@@ -2232,9 +2254,13 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (clearedProviderFailures > 0) {
|
||||
logger.info(`Settings-Update: ${clearedProviderFailures} Provider-Failure(s) gecleart wegen geaenderter Credentials`);
|
||||
}
|
||||
if (clearedProviderFailures > 0) {
|
||||
logger.info(`Settings-Update: ${clearedProviderFailures} Provider-Failure(s) gecleart wegen geaenderter Credentials`);
|
||||
}
|
||||
|
||||
if (!opts?.settingsOnlyImport && megaPoolChanged) {
|
||||
this.releaseMegaDebridResetParks();
|
||||
}
|
||||
|
||||
if (!opts?.settingsOnlyImport) {
|
||||
this.resolveExistingQueuedOpaqueFilenames();
|
||||
@@ -2243,8 +2269,43 @@ export class DownloadManager extends EventEmitter {
|
||||
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 {
|
||||
return this.settings;
|
||||
@@ -8019,12 +8080,12 @@ export class DownloadManager extends EventEmitter {
|
||||
if (effectiveProvider === "realdebrid") {
|
||||
return Boolean(this.settings.realDebridUseWebLogin || this.settings.token.trim());
|
||||
}
|
||||
if (effectiveProvider === "megadebrid-api") {
|
||||
const hasMegaCreds = Boolean(this.settings.megaCredentials.trim() || (this.settings.megaLogin.trim() && this.settings.megaPassword.trim()));
|
||||
return Boolean(hasMegaCreds && (resolveMegaDebridProvider(this.settings, "megadebrid") === "megadebrid-api" || this.settings.megaDebridApiEnabled));
|
||||
}
|
||||
if (effectiveProvider === "megadebrid-web") {
|
||||
const hasMegaCreds = Boolean(this.settings.megaCredentials.trim() || (this.settings.megaLogin.trim() && this.settings.megaPassword.trim()));
|
||||
if (effectiveProvider === "megadebrid-api") {
|
||||
const hasMegaCreds = getMegaDebridAccountsForMode(this.settings, "api").length > 0;
|
||||
return Boolean(hasMegaCreds && (resolveMegaDebridProvider(this.settings, "megadebrid") === "megadebrid-api" || this.settings.megaDebridApiEnabled));
|
||||
}
|
||||
if (effectiveProvider === "megadebrid-web") {
|
||||
const hasMegaCreds = getMegaDebridAccountsForMode(this.settings, "web").length > 0;
|
||||
return Boolean(hasMegaCreds && (resolveMegaDebridProvider(this.settings, "megadebrid") === "megadebrid-web" || this.settings.megaDebridWebEnabled));
|
||||
}
|
||||
if (effectiveProvider === "bestdebrid") {
|
||||
@@ -8209,7 +8270,7 @@ export class DownloadManager extends EventEmitter {
|
||||
private getSerializedValidatingLimit(provider: DebridProvider | null): number {
|
||||
if (provider === "megadebrid-web" || provider === "megadebrid-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}`))
|
||||
.length;
|
||||
return Math.max(1, usableAccounts);
|
||||
@@ -8793,9 +8854,11 @@ export class DownloadManager extends EventEmitter {
|
||||
genericRetries: Number(active.genericErrorRetries || 0),
|
||||
freshRetryUsed: Boolean(active.freshRetryUsed),
|
||||
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(
|
||||
item: DownloadItem,
|
||||
|
||||
@@ -226,7 +226,9 @@ export class MegaWebFallback {
|
||||
|
||||
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) {
|
||||
this.getCredentials = getCredentials;
|
||||
@@ -241,18 +243,19 @@ export class MegaWebFallback {
|
||||
const creds = (account && account.login.trim() && account.password.trim())
|
||||
? account
|
||||
: this.getCredentials();
|
||||
if (!creds.login.trim() || !creds.password.trim()) {
|
||||
return null;
|
||||
}
|
||||
const key = creds.login.trim().toLowerCase();
|
||||
return this.runExclusive(async () => {
|
||||
throwIfAborted(overallSignal);
|
||||
let cookie = await this.ensureSession(key, creds.login, creds.password, overallSignal);
|
||||
|
||||
let generated = await this.generate(link, cookie, overallSignal);
|
||||
if (!generated) {
|
||||
this.sessions.delete(key);
|
||||
cookie = await this.ensureSession(key, creds.login, creds.password, overallSignal);
|
||||
if (!creds.login.trim() || !creds.password.trim()) {
|
||||
return null;
|
||||
}
|
||||
const key = creds.login.trim().toLowerCase();
|
||||
const sessionGeneration = this.sessionGeneration;
|
||||
return this.runExclusive(async () => {
|
||||
throwIfAborted(overallSignal);
|
||||
let cookie = await this.ensureSession(key, creds.login, creds.password, overallSignal, sessionGeneration);
|
||||
|
||||
let generated = await this.generate(link, cookie, overallSignal);
|
||||
if (!generated) {
|
||||
this.sessions.delete(key);
|
||||
cookie = await this.ensureSession(key, creds.login, creds.password, overallSignal, sessionGeneration);
|
||||
generated = await this.generate(link, cookie, overallSignal);
|
||||
if (!generated) {
|
||||
return null;
|
||||
@@ -267,19 +270,28 @@ export class MegaWebFallback {
|
||||
}, 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);
|
||||
if (existing && existing.cookie && Date.now() - existing.setAt <= 20 * 60 * 1000) {
|
||||
return existing.cookie;
|
||||
}
|
||||
const cookie = await this.login(login, password, signal);
|
||||
this.sessions.set(key, { cookie, setAt: Date.now() });
|
||||
return cookie;
|
||||
}
|
||||
|
||||
public invalidateSession(): void {
|
||||
this.sessions.clear();
|
||||
}
|
||||
const cookie = await this.login(login, password, signal);
|
||||
if (generation === this.sessionGeneration) {
|
||||
this.sessions.set(key, { cookie, setAt: Date.now() });
|
||||
}
|
||||
return cookie;
|
||||
}
|
||||
|
||||
public invalidateSession(): void {
|
||||
this.sessionGeneration += 1;
|
||||
this.sessions.clear();
|
||||
}
|
||||
|
||||
private async runExclusive<T>(job: () => Promise<T>, key: string, signal?: AbortSignal): Promise<T> {
|
||||
const queuedAt = Date.now();
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountIds } from "../shared/mega-debrid-accounts";
|
||||
import type { AppSettings } from "../shared/types";
|
||||
|
||||
export function overlayLiveUsageCounters(target: AppSettings, liveSettings: AppSettings, liveTotalRuntimeMs: number): void {
|
||||
const debridLinkKeyIds = new Set(getDebridLinkApiKeyIds(target.debridLinkApiKeys));
|
||||
const megaAccountIds = new Set(getMegaDebridAccountIds(target.megaCredentials || "", target.megaPassword || ""));
|
||||
const validAccountIds = new Set([...debridLinkKeyIds, ...megaAccountIds]);
|
||||
target.totalDownloadedAllTime = Math.max(target.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0);
|
||||
target.totalCompletedFilesAllTime = Math.max(target.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0);
|
||||
target.totalRuntimeAllTimeMs = Math.max(target.totalRuntimeAllTimeMs || 0, liveTotalRuntimeMs);
|
||||
target.providerDailyUsageDay = liveSettings.providerDailyUsageDay;
|
||||
target.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) };
|
||||
target.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) };
|
||||
target.debridLinkApiKeyDailyUsageBytes = Object.fromEntries(
|
||||
Object.entries(liveSettings.debridLinkApiKeyDailyUsageBytes || {}).filter(([keyId]) => debridLinkKeyIds.has(keyId))
|
||||
);
|
||||
target.debridLinkApiKeyTotalUsageBytes = Object.fromEntries(
|
||||
Object.entries(liveSettings.debridLinkApiKeyTotalUsageBytes || {}).filter(([keyId]) => debridLinkKeyIds.has(keyId))
|
||||
);
|
||||
target.megaDebridAccountDailyUsageBytes = Object.fromEntries(
|
||||
Object.entries(liveSettings.megaDebridAccountDailyUsageBytes || {}).filter(([accountId]) => megaAccountIds.has(accountId))
|
||||
);
|
||||
target.megaDebridAccountTotalUsageBytes = Object.fromEntries(
|
||||
Object.entries(liveSettings.megaDebridAccountTotalUsageBytes || {}).filter(([accountId]) => megaAccountIds.has(accountId))
|
||||
);
|
||||
target.debridAccountStatuses = Object.fromEntries(
|
||||
Object.entries(liveSettings.debridAccountStatuses || {}).filter(([accountId]) => validAccountIds.has(accountId))
|
||||
);
|
||||
}
|
||||
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountIds, mergeMegaDebridCredentialPools } from "../shared/mega-debrid-accounts";
|
||||
import type { AppSettings } from "../shared/types";
|
||||
|
||||
export function overlayLiveUsageCounters(target: AppSettings, liveSettings: AppSettings, liveTotalRuntimeMs: number): void {
|
||||
const debridLinkKeyIds = new Set(getDebridLinkApiKeyIds(target.debridLinkApiKeys));
|
||||
const megaAccountIds = new Set(getMegaDebridAccountIds(mergeMegaDebridCredentialPools(target.megaDebridApiCredentials || "", target.megaDebridWebCredentials || "") || target.megaCredentials || "", target.megaPassword || ""));
|
||||
const validAccountIds = new Set([...debridLinkKeyIds, ...megaAccountIds]);
|
||||
target.totalDownloadedAllTime = Math.max(target.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0);
|
||||
target.totalCompletedFilesAllTime = Math.max(target.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0);
|
||||
target.totalRuntimeAllTimeMs = Math.max(target.totalRuntimeAllTimeMs || 0, liveTotalRuntimeMs);
|
||||
target.providerDailyUsageDay = liveSettings.providerDailyUsageDay;
|
||||
target.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) };
|
||||
target.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) };
|
||||
target.debridLinkApiKeyDailyUsageBytes = Object.fromEntries(
|
||||
Object.entries(liveSettings.debridLinkApiKeyDailyUsageBytes || {}).filter(([keyId]) => debridLinkKeyIds.has(keyId))
|
||||
);
|
||||
target.debridLinkApiKeyTotalUsageBytes = Object.fromEntries(
|
||||
Object.entries(liveSettings.debridLinkApiKeyTotalUsageBytes || {}).filter(([keyId]) => debridLinkKeyIds.has(keyId))
|
||||
);
|
||||
target.megaDebridAccountDailyUsageBytes = Object.fromEntries(
|
||||
Object.entries(liveSettings.megaDebridAccountDailyUsageBytes || {}).filter(([accountId]) => megaAccountIds.has(accountId))
|
||||
);
|
||||
target.megaDebridAccountTotalUsageBytes = Object.fromEntries(
|
||||
Object.entries(liveSettings.megaDebridAccountTotalUsageBytes || {}).filter(([accountId]) => megaAccountIds.has(accountId))
|
||||
);
|
||||
target.debridAccountStatuses = Object.fromEntries(
|
||||
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 path from "node:path";
|
||||
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 { getProviderUsageDayKey } from "../shared/provider-daily-limits";
|
||||
import { defaultSettings } from "./constants";
|
||||
@@ -381,21 +381,49 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
||||
const defaults = defaultSettings();
|
||||
const directorySettings = migrateLegacyDefaultDirectories(settings, defaults);
|
||||
const currentUsageDay = getProviderUsageDayKey();
|
||||
const megaLogin = asText(settings.megaLogin);
|
||||
const megaPassword = asText(settings.megaPassword);
|
||||
let megaCredentials = String(settings.megaCredentials ?? "").replace(/\r\n|\r/g, "\n").trim();
|
||||
if (!megaCredentials && megaLogin && megaPassword) {
|
||||
megaCredentials = `${megaLogin}:${megaPassword}`;
|
||||
}
|
||||
const megaDebridAccountIds = getMegaDebridAccountIds(megaCredentials);
|
||||
const megaDebridPreferApi = settings.megaDebridPreferApi !== undefined ? Boolean(settings.megaDebridPreferApi) : true;
|
||||
const hasMegaCreds = Boolean(megaLogin && megaPassword);
|
||||
const megaDebridApiEnabled = settings.megaDebridApiEnabled !== undefined
|
||||
? Boolean(settings.megaDebridApiEnabled)
|
||||
: (hasMegaCreds ? megaDebridPreferApi : defaults.megaDebridApiEnabled);
|
||||
const megaDebridWebEnabled = settings.megaDebridWebEnabled !== undefined
|
||||
? Boolean(settings.megaDebridWebEnabled)
|
||||
: (hasMegaCreds ? !megaDebridPreferApi : defaults.megaDebridWebEnabled);
|
||||
const legacyMegaLogin = asText(settings.megaLogin);
|
||||
const legacyMegaPassword = asText(settings.megaPassword);
|
||||
let legacyMegaCredentials = String(settings.megaCredentials ?? "").replace(/\r\n|\r/g, "\n").trim();
|
||||
if (!legacyMegaCredentials && legacyMegaLogin && legacyMegaPassword) {
|
||||
legacyMegaCredentials = `${legacyMegaLogin}:${legacyMegaPassword}`;
|
||||
}
|
||||
const megaDebridPreferApi = settings.megaDebridPreferApi !== undefined ? Boolean(settings.megaDebridPreferApi) : true;
|
||||
const hasLegacyMegaCredentials = getMegaDebridAccountIds(legacyMegaCredentials).length > 0;
|
||||
const requestedMegaDebridApiEnabled = settings.megaDebridApiEnabled !== undefined
|
||||
? Boolean(settings.megaDebridApiEnabled)
|
||||
: (hasLegacyMegaCredentials ? megaDebridPreferApi : defaults.megaDebridApiEnabled);
|
||||
const requestedMegaDebridWebEnabled = settings.megaDebridWebEnabled !== undefined
|
||||
? Boolean(settings.megaDebridWebEnabled)
|
||||
: (hasLegacyMegaCredentials ? !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 providerDailyUsageDay = /^\d{4}-\d{2}-\d{2}$/.test(providerDailyUsageDayRaw)
|
||||
? providerDailyUsageDayRaw
|
||||
@@ -429,8 +457,10 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
||||
token: asText(settings.token),
|
||||
realDebridUseWebLogin: Boolean(settings.realDebridUseWebLogin),
|
||||
megaLogin,
|
||||
megaPassword,
|
||||
megaCredentials,
|
||||
megaPassword,
|
||||
megaCredentials,
|
||||
megaDebridApiCredentials,
|
||||
megaDebridWebCredentials,
|
||||
megaDebridApiEnabled,
|
||||
megaDebridWebEnabled,
|
||||
megaDebridPreferApi,
|
||||
@@ -527,7 +557,9 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
||||
debridLinkApiKeyDailyLimitBytes,
|
||||
debridLinkApiKeyDailyUsageBytes: providerDailyUsageDay === currentUsageDay ? debridLinkApiKeyDailyUsageBytes : {},
|
||||
debridLinkApiKeyTotalUsageBytes,
|
||||
megaDebridDisabledAccountIds: normalizeStringList(settings.megaDebridDisabledAccountIds, megaDebridAccountIds),
|
||||
megaDebridDisabledAccountIds,
|
||||
megaDebridApiDisabledAccountIds,
|
||||
megaDebridWebDisabledAccountIds,
|
||||
megaDebridAccountDailyLimitBytes: normalizeNamedByteMap(settings.megaDebridAccountDailyLimitBytes, megaDebridAccountIds),
|
||||
megaDebridAccountDailyUsageBytes: providerDailyUsageDay === currentUsageDay
|
||||
? normalizeNamedByteMap(settings.megaDebridAccountDailyUsageBytes, megaDebridAccountIds)
|
||||
@@ -580,9 +612,11 @@ function sanitizeCredentialPersistence(settings: AppSettings): AppSettings {
|
||||
...settings,
|
||||
token: "",
|
||||
realDebridUseWebLogin: settings.realDebridUseWebLogin,
|
||||
megaLogin: "",
|
||||
megaPassword: "",
|
||||
megaCredentials: "",
|
||||
megaLogin: "",
|
||||
megaPassword: "",
|
||||
megaCredentials: "",
|
||||
megaDebridApiCredentials: "",
|
||||
megaDebridWebCredentials: "",
|
||||
bestToken: "",
|
||||
bestDebridUseWebLogin: settings.bestDebridUseWebLogin,
|
||||
allDebridToken: "",
|
||||
@@ -668,11 +702,14 @@ function normalizeAudioStripSummary(raw: unknown): AudioStripSummary | undefined
|
||||
};
|
||||
}
|
||||
|
||||
function migrateLegacyMegaEnableFlags(parsed: AppSettings): AppSettings {
|
||||
if (parsed.megaDebridApiEnabled !== undefined || parsed.megaDebridWebEnabled !== undefined) {
|
||||
return parsed;
|
||||
}
|
||||
const hasMegaCreds = Boolean(asText(parsed.megaLogin) && asText(parsed.megaPassword));
|
||||
function migrateLegacyMegaEnableFlags(parsed: AppSettings): AppSettings {
|
||||
if (parsed.megaDebridApiEnabled !== undefined || parsed.megaDebridWebEnabled !== undefined) {
|
||||
return parsed;
|
||||
}
|
||||
const hasMegaCreds = Boolean(
|
||||
getMegaDebridAccountIds(asText(parsed.megaCredentials), asText(parsed.megaPassword)).length
|
||||
|| (asText(parsed.megaLogin) && asText(parsed.megaPassword))
|
||||
);
|
||||
if (!hasMegaCreds) {
|
||||
return parsed;
|
||||
}
|
||||
@@ -680,16 +717,24 @@ function migrateLegacyMegaEnableFlags(parsed: AppSettings): AppSettings {
|
||||
return { ...parsed, megaDebridApiEnabled: preferApi, megaDebridWebEnabled: !preferApi };
|
||||
}
|
||||
|
||||
function readSettingsFile(filePath: string): AppSettings | null {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as AppSettings;
|
||||
const migratedLanguage = (parsed as Partial<AppSettings>).language === undefined ? "de" : parsed.language;
|
||||
const merged = normalizeSettings({
|
||||
function readSettingsFile(filePath: string): AppSettings | null {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as AppSettings;
|
||||
const migratedLanguage = (parsed as Partial<AppSettings>).language === undefined ? "de" : parsed.language;
|
||||
const migrated = migrateLegacyMegaEnableFlags(parsed);
|
||||
const mergedInput = {
|
||||
...defaultSettings(),
|
||||
...migrateLegacyMegaEnableFlags(parsed),
|
||||
...migrated,
|
||||
language: migratedLanguage,
|
||||
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);
|
||||
} catch (error) {
|
||||
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]);
|
||||
}
|
||||
|
||||
async function readJsonBody(response: Response, timeoutMs: number): Promise<Record<string, unknown> | null> {
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
||||
async function readJsonValue(response: Response, timeoutMs: number): Promise<unknown> {
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
const timeoutPromise = new Promise<never>((_resolve, reject) => {
|
||||
timer = setTimeout(() => {
|
||||
void response.body?.cancel().catch(() => undefined);
|
||||
reject(new Error(`timeout:${timeoutMs}`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
try {
|
||||
const data = await Promise.race([
|
||||
response.json().catch(() => null) as Promise<unknown>,
|
||||
timeoutPromise,
|
||||
]);
|
||||
if (!data || typeof data !== "object" || Array.isArray(data)) return null;
|
||||
return data as Record<string, unknown>;
|
||||
} finally {
|
||||
if (timer) clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return await Promise.race([
|
||||
response.json().catch(() => null) as Promise<unknown>,
|
||||
timeoutPromise,
|
||||
]);
|
||||
} 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> {
|
||||
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`);
|
||||
}
|
||||
|
||||
async function fetchRelease(repo: string, endpoint: string): Promise<{
|
||||
async function fetchRelease(repo: string, endpoint: string): Promise<{
|
||||
ok: boolean;
|
||||
status: number;
|
||||
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 };
|
||||
} finally {
|
||||
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[] {
|
||||
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);
|
||||
}
|
||||
|
||||
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 latestVersion = latestTag.replace(/^v/i, "") || APP_VERSION;
|
||||
const releaseUrl = String(payload.html_url || fallbackUrl);
|
||||
@@ -425,8 +453,73 @@ function parseReleasePayload(payload: Record<string, unknown>, fallbackUrl: stri
|
||||
setupAssetName: setup?.name || "",
|
||||
setupAssetDigest: setup?.digest || "",
|
||||
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[] {
|
||||
const seen = new Set<string>();
|
||||
@@ -804,7 +897,7 @@ export function buildInstallerLaunchArgs(): string[] {
|
||||
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 fallbackUrl = `${WEB_BASE}/${safeRepo}/releases/latest`;
|
||||
const fallback: UpdateCheckResult = {
|
||||
@@ -817,15 +910,20 @@ export async function checkGitHubUpdate(repo: string): Promise<UpdateCheckResult
|
||||
|
||||
try {
|
||||
const { ok, status, payload } = await fetchRelease(safeRepo, "releases/latest");
|
||||
if (!ok || !payload) {
|
||||
const reason = String((payload?.message as string) || `HTTP ${status}`);
|
||||
return { ...fallback, error: reason };
|
||||
}
|
||||
return parseReleasePayload(payload, fallbackUrl);
|
||||
} catch (error) {
|
||||
return { ...fallback, error: compactErrorText(error) };
|
||||
}
|
||||
}
|
||||
if (!ok || !payload) {
|
||||
const reason = String((payload?.message as string) || `HTTP ${status}`);
|
||||
return { ...fallback, error: reason };
|
||||
}
|
||||
const result = parseReleasePayload(payload, fallbackUrl);
|
||||
if (!result.updateAvailable) return result;
|
||||
return {
|
||||
...result,
|
||||
releaseNotes: await resolveMissingReleaseNotes(safeRepo, payload, result.latestVersion),
|
||||
};
|
||||
} catch (error) {
|
||||
return { ...fallback, error: compactErrorText(error) };
|
||||
}
|
||||
}
|
||||
|
||||
export async function installLatestUpdate(
|
||||
repo: string,
|
||||
|
||||
+180
-172
@@ -1,7 +1,7 @@
|
||||
import { DragEvent, ReactElement, memo, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
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 {
|
||||
AllDebridHostInfo,
|
||||
AppSettings,
|
||||
@@ -445,10 +445,9 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
|
||||
kind: "megadebrid-api",
|
||||
service: "megadebrid-api",
|
||||
serviceLabel: "Mega-Debrid",
|
||||
title: "Mega-Debrid API",
|
||||
modeLabel: "API",
|
||||
pickerDescription: "Login:Passwort-Paare für Mega-Debrid (API). Mehrere Accounts zeilenweise für Multi-Account.",
|
||||
needsToken: true
|
||||
title: "Mega-Debrid API",
|
||||
modeLabel: "API",
|
||||
pickerDescription: "Login:Passwort-Paare für Mega-Debrid (API). Mehrere Accounts zeilenweise für Multi-Account."
|
||||
},
|
||||
{
|
||||
kind: "megadebrid-web",
|
||||
@@ -456,8 +455,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
|
||||
serviceLabel: "Mega-Debrid",
|
||||
title: "Mega-Debrid Web-Login",
|
||||
modeLabel: "Web-Login",
|
||||
pickerDescription: "Login:Passwort-Paare für Mega-Debrid (Web). Mehrere Accounts zeilenweise für Multi-Account.",
|
||||
needsToken: true
|
||||
pickerDescription: "Login:Passwort-Paare für Mega-Debrid (Web). Mehrere Accounts zeilenweise für Multi-Account."
|
||||
},
|
||||
{
|
||||
kind: "bestdebrid-api",
|
||||
@@ -621,20 +619,22 @@ function getAccountCredentialLabel(kind: AccountKind): string {
|
||||
}
|
||||
}
|
||||
|
||||
function hasMegaDebridCredentials(settings: AppSettings): boolean {
|
||||
if (parseMegaDebridAccounts(settings.megaCredentials || "").length > 0) return true;
|
||||
return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim());
|
||||
}
|
||||
function hasMegaDebridCredentials(settings: AppSettings, mode?: MegaDebridAccountMode): boolean {
|
||||
if (mode) {
|
||||
return getMegaDebridAccountsForMode(settings, mode).length > 0;
|
||||
}
|
||||
return getMegaDebridAccountsForMode(settings, "api").length > 0 || getMegaDebridAccountsForMode(settings, "web").length > 0;
|
||||
}
|
||||
|
||||
function getConfiguredProvidersFromSettings(settings: AppSettings): DebridProvider[] {
|
||||
const list: DebridProvider[] = [];
|
||||
if (settings.token.trim() || settings.realDebridUseWebLogin) {
|
||||
list.push("realdebrid");
|
||||
}
|
||||
if (hasMegaDebridCredentials(settings) && settings.megaDebridApiEnabled) {
|
||||
if (hasMegaDebridCredentials(settings, "api") && settings.megaDebridApiEnabled) {
|
||||
list.push("megadebrid-api");
|
||||
}
|
||||
if (hasMegaDebridCredentials(settings) && settings.megaDebridWebEnabled) {
|
||||
if (hasMegaDebridCredentials(settings, "web") && settings.megaDebridWebEnabled) {
|
||||
list.push("megadebrid-web");
|
||||
}
|
||||
if (settings.bestDebridUseWebLogin || settings.bestToken.trim()) {
|
||||
@@ -682,9 +682,9 @@ function getConfiguredAccountKind(settings: AppSettings, service: AccountService
|
||||
if (settings.realDebridUseWebLogin) return "realdebrid-web";
|
||||
return settings.token.trim() ? "realdebrid-api" : null;
|
||||
case "megadebrid-api":
|
||||
return hasMegaDebridCredentials(settings) && settings.megaDebridApiEnabled ? "megadebrid-api" : null;
|
||||
return hasMegaDebridCredentials(settings, "api") && settings.megaDebridApiEnabled ? "megadebrid-api" : null;
|
||||
case "megadebrid-web":
|
||||
return hasMegaDebridCredentials(settings) && settings.megaDebridWebEnabled ? "megadebrid-web" : null;
|
||||
return hasMegaDebridCredentials(settings, "web") && settings.megaDebridWebEnabled ? "megadebrid-web" : null;
|
||||
case "bestdebrid":
|
||||
if (settings.bestDebridUseWebLogin) return "bestdebrid-web";
|
||||
return settings.bestToken.trim() ? "bestdebrid-api" : null;
|
||||
@@ -719,9 +719,10 @@ function summarizeAccount(kind: AccountKind, settings: AppSettings): string {
|
||||
return maskValue(settings.token, 3, 3);
|
||||
case "realdebrid-web":
|
||||
return "Browser-Login";
|
||||
case "megadebrid-api":
|
||||
case "megadebrid-web": {
|
||||
const megaAccounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword);
|
||||
case "megadebrid-api":
|
||||
case "megadebrid-web": {
|
||||
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[0].maskedLogin;
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
if (kind === "megadebrid-api" || kind === "megadebrid-web") {
|
||||
const accounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword);
|
||||
if (kind === "megadebrid-api" || kind === "megadebrid-web") {
|
||||
const accounts = getMegaDebridAccountsForMode(settings, kind === "megadebrid-web" ? "web" : "api");
|
||||
if (accounts.length > 1) {
|
||||
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: [] };
|
||||
if (!kind) {
|
||||
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 };
|
||||
case "realdebrid-web":
|
||||
return { mode, kind, service, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega };
|
||||
case "megadebrid-api":
|
||||
case "megadebrid-web": {
|
||||
let megaToken = (settings.megaCredentials || "").trim();
|
||||
if (!megaToken && settings.megaLogin.trim() && settings.megaPassword.trim()) {
|
||||
megaToken = `${settings.megaLogin.trim()}:${settings.megaPassword.trim()}`;
|
||||
}
|
||||
const parsed = parseMegaDebridAccounts(megaToken);
|
||||
const megaAccounts = parsed.map((a) => ({ login: a.login, password: a.password }));
|
||||
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 "megadebrid-api":
|
||||
case "megadebrid-web": {
|
||||
const megaMode = kind === "megadebrid-web" ? "web" : "api";
|
||||
const parsed = getMegaDebridAccountsForMode(settings, megaMode);
|
||||
const megaAccounts = parsed.map((a) => ({ login: a.login, password: a.password }));
|
||||
const loadedIds = new Set(parsed.map((a) => a.id));
|
||||
const megaDisabledIds = getMegaDebridDisabledAccountIdsForMode(settings, megaMode).filter((id) => loadedIds.has(id));
|
||||
return { mode, kind, service, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, megaAccounts, megaNewLogin: "", megaNewPassword: "", megaDisabledIds };
|
||||
}
|
||||
case "bestdebrid-api":
|
||||
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:
|
||||
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) {
|
||||
return settings;
|
||||
}
|
||||
@@ -876,23 +951,11 @@ export function applyAccountDialogToSettings(settings: AppSettings, dialog: Acco
|
||||
return { ...settings, token, realDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes };
|
||||
case "realdebrid-web":
|
||||
return { ...settings, token: "", realDebridUseWebLogin: true, providerDailyLimitBytes: nextProviderDailyLimitBytes };
|
||||
case "megadebrid-api": {
|
||||
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, 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 "megadebrid-api": {
|
||||
return applyMegaDebridDialogToSettings(settings, dialog, "api", nextProviderDailyLimitBytes);
|
||||
}
|
||||
case "megadebrid-web": {
|
||||
return applyMegaDebridDialogToSettings(settings, dialog, "web", nextProviderDailyLimitBytes);
|
||||
}
|
||||
case "bestdebrid-api":
|
||||
return { ...settings, bestToken: token, bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes };
|
||||
@@ -937,14 +1000,10 @@ function clearAccountServiceFromSettings(settings: AppSettings, service: Account
|
||||
switch (service) {
|
||||
case "realdebrid":
|
||||
return { ...settings, token: "", realDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes };
|
||||
case "megadebrid-api":
|
||||
return settings.megaDebridWebEnabled
|
||||
? { ...settings, megaDebridApiEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes }
|
||||
: { ...settings, megaCredentials: "", megaLogin: "", megaPassword: "", megaDebridApiEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: 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 "megadebrid-api":
|
||||
return clearMegaDebridModeFromSettings(settings, "api", nextProviderDailyLimitBytes, nextProviderDailyUsageBytes);
|
||||
case "megadebrid-web":
|
||||
return clearMegaDebridModeFromSettings(settings, "web", nextProviderDailyLimitBytes, nextProviderDailyUsageBytes);
|
||||
case "bestdebrid":
|
||||
return { ...settings, bestToken: "", bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes };
|
||||
case "alldebrid":
|
||||
@@ -1029,7 +1088,7 @@ const emptyStats = (): DownloadStats => ({
|
||||
|
||||
const emptySnapshot = (): UiSnapshot => ({
|
||||
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: [],
|
||||
archivePasswordList: "",
|
||||
rememberToken: true, providerOrder: [], providerPrimary: "realdebrid", providerSecondary: "none",
|
||||
@@ -1056,7 +1115,9 @@ const emptySnapshot = (): UiSnapshot => ({
|
||||
debridLinkApiKeyDailyLimitBytes: {},
|
||||
debridLinkApiKeyDailyUsageBytes: {},
|
||||
debridLinkApiKeyTotalUsageBytes: {},
|
||||
megaDebridDisabledAccountIds: [],
|
||||
megaDebridDisabledAccountIds: [],
|
||||
megaDebridApiDisabledAccountIds: [],
|
||||
megaDebridWebDisabledAccountIds: [],
|
||||
megaDebridAccountDailyLimitBytes: {},
|
||||
megaDebridAccountDailyUsageBytes: {},
|
||||
megaDebridAccountTotalUsageBytes: {},
|
||||
@@ -1753,8 +1814,6 @@ export function App(): ReactElement {
|
||||
const snapshotRef = useRef(snapshot);
|
||||
snapshotRef.current = snapshot;
|
||||
const tabRef = useRef(tab);
|
||||
const autoExpandedPkgsRef = useRef(new Set<string>());
|
||||
const manualCollapsedPkgsRef = useRef(new Set<string>());
|
||||
tabRef.current = tab;
|
||||
const stateFlushTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const toastTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -2270,12 +2329,7 @@ export function App(): ReactElement {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
for (const packageId of Array.from(manualCollapsedPkgsRef.current)) {
|
||||
if (!snapshot.session.packages[packageId]) {
|
||||
manualCollapsedPkgsRef.current.delete(packageId);
|
||||
}
|
||||
}
|
||||
return changed ? next : prev;
|
||||
return changed ? next : prev;
|
||||
});
|
||||
}, [downloadsTabActive, packageOrderKey, snapshot.session.packageOrder, snapshot.session.packages, totalPackageCount]);
|
||||
|
||||
@@ -2287,18 +2341,18 @@ export function App(): ReactElement {
|
||||
}, [snapshot.session.packages, snapshot.session.items]);
|
||||
|
||||
const sortRelevantItems = (snapshot.session.running && settingsDraft.autoSortPackagesByProgress && packages.length > 1)
|
||||
? snapshot.session.items
|
||||
: null;
|
||||
? snapshot.session.items
|
||||
: null;
|
||||
const visiblePackages = useMemo(() => {
|
||||
if (!sortRelevantItems) {
|
||||
return packages;
|
||||
}
|
||||
return sortPackagesForDisplay(
|
||||
packages,
|
||||
sortRelevantItems,
|
||||
true,
|
||||
true
|
||||
);
|
||||
packages,
|
||||
sortRelevantItems,
|
||||
true,
|
||||
true
|
||||
);
|
||||
}, [packages, sortRelevantItems]);
|
||||
|
||||
const downloadsViewCore = useMemo(() => buildDownloadsViewModel({
|
||||
@@ -2338,36 +2392,6 @@ export function App(): ReactElement {
|
||||
void loadAllDebridHostInfo(true);
|
||||
}, [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(() => (
|
||||
packages.length > 0 && packages.every((pkg) => collapsedPackages[pkg.id])
|
||||
), [packages, collapsedPackages]);
|
||||
@@ -2453,12 +2477,12 @@ export function App(): ReactElement {
|
||||
const option = findAccountOption(kind);
|
||||
let statusLabel = "Aktiviert";
|
||||
let note = "";
|
||||
if (kind === "megadebrid-api") {
|
||||
const megaAccountCount = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword).length;
|
||||
if (kind === "megadebrid-api") {
|
||||
const megaAccountCount = getMegaDebridAccountsForMode(settingsDraft, "api").length;
|
||||
statusLabel = megaAccountCount > 1 ? `${megaAccountCount} Accounts` : "Aktiviert";
|
||||
note = "Nur API aktiv. Kein Web-Fallback.";
|
||||
} else if (kind === "megadebrid-web") {
|
||||
const megaAccountCount = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword).length;
|
||||
} else if (kind === "megadebrid-web") {
|
||||
const megaAccountCount = getMegaDebridAccountsForMode(settingsDraft, "web").length;
|
||||
statusLabel = megaAccountCount > 1 ? `${megaAccountCount} Accounts` : "Aktiviert";
|
||||
note = "Nur Web aktiv. Kein API-Fallback.";
|
||||
} else if (kind === "realdebrid-web") {
|
||||
@@ -2569,8 +2593,10 @@ export function App(): ReactElement {
|
||||
const accountRows = useMemo(() => {
|
||||
const rows: AccountTableRow[] = [];
|
||||
for (const entry of configuredAccounts) {
|
||||
if (entry.kind === "megadebrid-api" || entry.kind === "megadebrid-web") {
|
||||
const accounts = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword || "");
|
||||
if (entry.kind === "megadebrid-api" || entry.kind === "megadebrid-web") {
|
||||
const mode = entry.kind === "megadebrid-web" ? "web" : "api";
|
||||
const accounts = getMegaDebridAccountsForMode(settingsDraft, mode);
|
||||
const disabledAccountIds = getMegaDebridDisabledAccountIdsForMode(settingsDraft, mode);
|
||||
for (const acc of accounts) {
|
||||
const used = getMegaDebridAccountDailyUsageBytes(snapshot.settings, acc.id);
|
||||
const limit = getMegaDebridAccountDailyLimitBytes(settingsDraft, acc.id);
|
||||
@@ -2583,7 +2609,7 @@ export function App(): ReactElement {
|
||||
credentialLabel: "••••••",
|
||||
accountId: acc.id,
|
||||
checkable: true,
|
||||
disabled: (settingsDraft.megaDebridDisabledAccountIds || []).includes(acc.id),
|
||||
disabled: disabledAccountIds.includes(acc.id),
|
||||
dailyUsedBytes: used,
|
||||
dailyLimitBytes: limit,
|
||||
dailyRemainingBytes: limit > 0 ? Math.max(0, limit - used) : 0,
|
||||
@@ -3095,9 +3121,23 @@ export function App(): ReactElement {
|
||||
megaCredentials: serializeMegaDebridAccounts([newMegaCredentials]),
|
||||
megaLogin: newMegaCredentials.login,
|
||||
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: ""
|
||||
}
|
||||
: { ...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 status = statuses.length === 1 && statuses[0].accountId === targetedCheck.expectedStatusId ? statuses[0] : null;
|
||||
if (!status || !status.valid) {
|
||||
@@ -3193,30 +3233,26 @@ export function App(): ReactElement {
|
||||
});
|
||||
};
|
||||
|
||||
const onToggleMegaAccountEnabled = async (login: string, currentlyDisabled: boolean): Promise<void> => {
|
||||
const accId = getMegaDebridAccountId(login.trim());
|
||||
await performQuickAction(async () => {
|
||||
const current = settingsDraft.megaDebridDisabledAccountIds || [];
|
||||
const next = currentlyDisabled ? current.filter((id) => id !== accId) : [...current, accId];
|
||||
await persistSpecificSettings({ ...settingsDraft, megaDebridDisabledAccountIds: next });
|
||||
const onToggleMegaAccountEnabled = async (kind: "megadebrid-api" | "megadebrid-web", login: string, currentlyDisabled: boolean): Promise<void> => {
|
||||
const accId = getMegaDebridAccountId(login.trim());
|
||||
await performQuickAction(async () => {
|
||||
const mode = kind === "megadebrid-web" ? "web" : "api";
|
||||
const current = getMegaDebridDisabledAccountIdsForMode(settingsDraft, mode);
|
||||
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);
|
||||
}, (error) => {
|
||||
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 confirmed = await askConfirmPrompt({ title: "Key entfernen", message: `Soll der Debrid-Link-Key ${key.masked} wirklich entfernt werden?`, confirmLabel: "Entfernen", danger: true });
|
||||
if (!confirmed) return;
|
||||
@@ -3253,7 +3289,7 @@ export function App(): ReactElement {
|
||||
const toggleAccountTableRow = (row: AccountTableRow): void => {
|
||||
setAccountContextMenu(null);
|
||||
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) {
|
||||
void onToggleDebridLinkApiKeyEnabled(row.entry, row.dlKey);
|
||||
} else {
|
||||
@@ -3273,7 +3309,9 @@ export function App(): ReactElement {
|
||||
);
|
||||
const nextDraft: AppSettings = {
|
||||
...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);
|
||||
showToast(enabled ? "Accounts aktiviert" : "Accounts deaktiviert", 2200);
|
||||
@@ -3310,7 +3348,7 @@ export function App(): ReactElement {
|
||||
const checkAccountTableRow = (row: AccountTableRow): void => {
|
||||
setAccountContextMenu(null);
|
||||
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);
|
||||
if (account) {
|
||||
void runMegaAccountCheck(account.login, account.password);
|
||||
@@ -4029,17 +4067,11 @@ export function App(): ReactElement {
|
||||
}
|
||||
}, [showToast]);
|
||||
|
||||
const onPackageToggleCollapse = useCallback((packageId: string): void => {
|
||||
setCollapsedPackages((prev) => {
|
||||
const nextCollapsed = !(prev[packageId] ?? false);
|
||||
if (nextCollapsed) {
|
||||
manualCollapsedPkgsRef.current.add(packageId);
|
||||
} else {
|
||||
manualCollapsedPkgsRef.current.delete(packageId);
|
||||
autoExpandedPkgsRef.current.delete(packageId);
|
||||
}
|
||||
return { ...prev, [packageId]: nextCollapsed };
|
||||
});
|
||||
const onPackageToggleCollapse = useCallback((packageId: string): void => {
|
||||
setCollapsedPackages((prev) => {
|
||||
const nextCollapsed = !(prev[packageId] ?? false);
|
||||
return { ...prev, [packageId]: nextCollapsed };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onPackageCancel = useCallback((packageId: string): void => {
|
||||
@@ -5014,11 +5046,6 @@ export function App(): ReactElement {
|
||||
const next = { ...current };
|
||||
for (const entry of packages) {
|
||||
next[entry.id] = targetState;
|
||||
if (targetState) manualCollapsedPkgsRef.current.add(entry.id);
|
||||
else {
|
||||
manualCollapsedPkgsRef.current.delete(entry.id);
|
||||
autoExpandedPkgsRef.current.delete(entry.id);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
@@ -5480,26 +5507,7 @@ export function App(): ReactElement {
|
||||
multi: option.kind === "megadebrid-api" || option.kind === "megadebrid-web" || option.kind === "debridlink-api",
|
||||
icon: ACCOUNT_SERVICE_ICONS[option.service]
|
||||
}));
|
||||
const accountAddFields: AccountDialogField[] = accountDialog && accountDialogOption ? [
|
||||
...((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 accountAddFields = buildAccountAddFields(accountDialog);
|
||||
const accountAddDialog = (
|
||||
<AccountAddDialog
|
||||
actions={{
|
||||
|
||||
+469
-422
@@ -1,458 +1,505 @@
|
||||
import { parseDebridLinkApiKeys, getDebridLinkApiKeyId } from "../shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountId, parseMegaDebridAccounts, serializeMegaDebridAccounts } from "../shared/mega-debrid-accounts";
|
||||
import type { AppSettings, DebridAccountStatus, DebridProvider } from "../shared/types";
|
||||
|
||||
export type AccountService = "realdebrid" | "megadebrid-api" | "megadebrid-web" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink" | "linksnappy";
|
||||
|
||||
export type AccountKind =
|
||||
| "realdebrid-api"
|
||||
| "realdebrid-web"
|
||||
| "megadebrid-api"
|
||||
| "megadebrid-web"
|
||||
| "bestdebrid-api"
|
||||
| "bestdebrid-web"
|
||||
| "alldebrid-api"
|
||||
| "alldebrid-web"
|
||||
| "ddownload-login"
|
||||
| "onefichier-api"
|
||||
| "debridlink-api"
|
||||
| "linksnappy-login";
|
||||
|
||||
export type SingleAccountKind = Exclude<AccountKind, "megadebrid-api" | "megadebrid-web" | "debridlink-api">;
|
||||
|
||||
export type AccountEditTarget =
|
||||
| {
|
||||
type: "single";
|
||||
rowKey: string;
|
||||
kind: SingleAccountKind;
|
||||
service: AccountService;
|
||||
provider: DebridProvider;
|
||||
}
|
||||
| {
|
||||
type: "mega";
|
||||
rowKey: string;
|
||||
kind: "megadebrid-api" | "megadebrid-web";
|
||||
service: "megadebrid-api" | "megadebrid-web";
|
||||
accountId: string;
|
||||
}
|
||||
| {
|
||||
type: "debridlink";
|
||||
rowKey: string;
|
||||
kind: "debridlink-api";
|
||||
service: "debridlink";
|
||||
keyId: string;
|
||||
};
|
||||
|
||||
export interface AccountEditState {
|
||||
target: AccountEditTarget;
|
||||
login: string;
|
||||
password: string;
|
||||
token: string;
|
||||
dailyLimitGb: string;
|
||||
originalDailyLimitBytes: number;
|
||||
}
|
||||
|
||||
import { parseDebridLinkApiKeys, getDebridLinkApiKeyId } from "../shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountId, getMegaDebridAccountsForMode, getMegaDebridCredentialsForMode, getMegaDebridDisabledAccountIdsForMode, mergeMegaDebridCredentialPools, parseMegaDebridAccounts, serializeMegaDebridAccounts, type MegaDebridAccountMode } from "../shared/mega-debrid-accounts";
|
||||
import type { AppSettings, DebridAccountStatus, DebridProvider } from "../shared/types";
|
||||
|
||||
export type AccountService = "realdebrid" | "megadebrid-api" | "megadebrid-web" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink" | "linksnappy";
|
||||
|
||||
export type AccountKind =
|
||||
| "realdebrid-api"
|
||||
| "realdebrid-web"
|
||||
| "megadebrid-api"
|
||||
| "megadebrid-web"
|
||||
| "bestdebrid-api"
|
||||
| "bestdebrid-web"
|
||||
| "alldebrid-api"
|
||||
| "alldebrid-web"
|
||||
| "ddownload-login"
|
||||
| "onefichier-api"
|
||||
| "debridlink-api"
|
||||
| "linksnappy-login";
|
||||
|
||||
export type SingleAccountKind = Exclude<AccountKind, "megadebrid-api" | "megadebrid-web" | "debridlink-api">;
|
||||
|
||||
export type AccountEditTarget =
|
||||
| {
|
||||
type: "single";
|
||||
rowKey: string;
|
||||
kind: SingleAccountKind;
|
||||
service: AccountService;
|
||||
provider: DebridProvider;
|
||||
}
|
||||
| {
|
||||
type: "mega";
|
||||
rowKey: string;
|
||||
kind: "megadebrid-api" | "megadebrid-web";
|
||||
service: "megadebrid-api" | "megadebrid-web";
|
||||
accountId: string;
|
||||
}
|
||||
| {
|
||||
type: "debridlink";
|
||||
rowKey: string;
|
||||
kind: "debridlink-api";
|
||||
service: "debridlink";
|
||||
keyId: string;
|
||||
};
|
||||
|
||||
export interface AccountEditState {
|
||||
target: AccountEditTarget;
|
||||
login: string;
|
||||
password: string;
|
||||
token: string;
|
||||
dailyLimitGb: string;
|
||||
originalDailyLimitBytes: number;
|
||||
}
|
||||
|
||||
const BYTES_PER_GIB = 1024 * 1024 * 1024;
|
||||
|
||||
function formatDailyLimit(limitBytes: number): string {
|
||||
if (!Number.isFinite(limitBytes) || limitBytes <= 0) {
|
||||
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 getMegaTargetMode(target: Extract<AccountEditTarget, { type: "mega" }>): MegaDebridAccountMode {
|
||||
return target.kind === "megadebrid-web" ? "web" : "api";
|
||||
}
|
||||
|
||||
function parseDailyLimit(value: string): number | null {
|
||||
const normalized = value.trim().replace(",", ".");
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(normalized);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.floor(parsed * BYTES_PER_GIB);
|
||||
}
|
||||
|
||||
function withoutRecordKeys<T>(record: Record<string, T>, ...keys: string[]): Record<string, T> {
|
||||
const blocked = new Set(keys);
|
||||
return Object.fromEntries(Object.entries(record || {}).filter(([key]) => !blocked.has(key)));
|
||||
}
|
||||
|
||||
function resolveDailyLimit(value: string, originalBytes: number): number | null {
|
||||
return value === formatDailyLimit(originalBytes) ? (originalBytes > 0 ? originalBytes : null) : parseDailyLimit(value);
|
||||
}
|
||||
|
||||
function updateTargetLimit(record: Record<string, number>, oldId: string, newId: string, value: string, originalBytes: number): Record<string, number> {
|
||||
const next = withoutRecordKeys(record || {}, oldId, newId);
|
||||
const limit = resolveDailyLimit(value, originalBytes);
|
||||
if (limit && limit > 0) {
|
||||
next[newId] = limit;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function migrateDisabledId(ids: readonly string[], oldId: string, newId: string): string[] {
|
||||
const wasDisabled = ids.includes(oldId);
|
||||
const next = ids.filter((id) => id !== oldId && id !== newId);
|
||||
if (wasDisabled) {
|
||||
next.push(newId);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function updateProviderLimit(settings: AppSettings, provider: DebridProvider, value: string, originalBytes: number): AppSettings["providerDailyLimitBytes"] {
|
||||
const next = { ...(settings.providerDailyLimitBytes || {}) };
|
||||
const limit = resolveDailyLimit(value, originalBytes);
|
||||
if (limit && limit > 0) {
|
||||
next[provider] = limit;
|
||||
} else {
|
||||
delete next[provider];
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function createSingleEditState(target: Extract<AccountEditTarget, { type: "single" }>, settings: AppSettings): AccountEditState {
|
||||
const originalDailyLimitBytes = settings.providerDailyLimitBytes?.[target.provider] || 0;
|
||||
const base = {
|
||||
target,
|
||||
login: "",
|
||||
password: "",
|
||||
token: "",
|
||||
dailyLimitGb: formatDailyLimit(originalDailyLimitBytes),
|
||||
originalDailyLimitBytes
|
||||
};
|
||||
switch (target.kind) {
|
||||
case "realdebrid-api":
|
||||
return { ...base, token: settings.token };
|
||||
case "bestdebrid-api":
|
||||
return { ...base, token: settings.bestToken };
|
||||
case "alldebrid-api":
|
||||
return { ...base, token: settings.allDebridToken };
|
||||
case "onefichier-api":
|
||||
return { ...base, token: settings.oneFichierApiKey };
|
||||
case "ddownload-login":
|
||||
return { ...base, login: settings.ddownloadLogin, password: settings.ddownloadPassword };
|
||||
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);
|
||||
}
|
||||
|
||||
function formatDailyLimit(limitBytes: number): string {
|
||||
if (!Number.isFinite(limitBytes) || limitBytes <= 0) {
|
||||
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 {
|
||||
const normalized = value.trim().replace(",", ".");
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(normalized);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.floor(parsed * BYTES_PER_GIB);
|
||||
}
|
||||
|
||||
function withoutRecordKeys<T>(record: Record<string, T>, ...keys: string[]): Record<string, T> {
|
||||
const blocked = new Set(keys);
|
||||
return Object.fromEntries(Object.entries(record || {}).filter(([key]) => !blocked.has(key)));
|
||||
}
|
||||
|
||||
function resolveDailyLimit(value: string, originalBytes: number): number | null {
|
||||
return value === formatDailyLimit(originalBytes) ? (originalBytes > 0 ? originalBytes : null) : parseDailyLimit(value);
|
||||
}
|
||||
|
||||
function updateTargetLimit(record: Record<string, number>, oldId: string, newId: string, value: string, originalBytes: number): Record<string, number> {
|
||||
const next = withoutRecordKeys(record || {}, oldId, newId);
|
||||
const limit = resolveDailyLimit(value, originalBytes);
|
||||
if (limit && limit > 0) {
|
||||
next[newId] = limit;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function migrateDisabledId(ids: readonly string[], oldId: string, newId: string): string[] {
|
||||
const wasDisabled = ids.includes(oldId);
|
||||
const next = ids.filter((id) => id !== oldId && id !== newId);
|
||||
if (wasDisabled) {
|
||||
next.push(newId);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function updateProviderLimit(settings: AppSettings, provider: DebridProvider, value: string, originalBytes: number): AppSettings["providerDailyLimitBytes"] {
|
||||
const next = { ...(settings.providerDailyLimitBytes || {}) };
|
||||
const limit = resolveDailyLimit(value, originalBytes);
|
||||
if (limit && limit > 0) {
|
||||
next[provider] = limit;
|
||||
} else {
|
||||
delete next[provider];
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function createSingleEditState(target: Extract<AccountEditTarget, { type: "single" }>, settings: AppSettings): AccountEditState {
|
||||
const originalDailyLimitBytes = settings.providerDailyLimitBytes?.[target.provider] || 0;
|
||||
const base = {
|
||||
target,
|
||||
login: "",
|
||||
password: "",
|
||||
token: "",
|
||||
dailyLimitGb: formatDailyLimit(originalDailyLimitBytes),
|
||||
originalDailyLimitBytes
|
||||
};
|
||||
switch (target.kind) {
|
||||
case "realdebrid-api":
|
||||
return { ...base, token: settings.token };
|
||||
case "bestdebrid-api":
|
||||
return { ...base, token: settings.bestToken };
|
||||
case "alldebrid-api":
|
||||
return { ...base, token: settings.allDebridToken };
|
||||
case "onefichier-api":
|
||||
return { ...base, token: settings.oneFichierApiKey };
|
||||
case "ddownload-login":
|
||||
return { ...base, login: settings.ddownloadLogin, password: settings.ddownloadPassword };
|
||||
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") {
|
||||
const account = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword || "")
|
||||
.find((entry) => entry.id === target.accountId);
|
||||
if (!account) {
|
||||
throw new Error("Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.");
|
||||
}
|
||||
return {
|
||||
target,
|
||||
login: account.login,
|
||||
password: account.password,
|
||||
token: "",
|
||||
dailyLimitGb: formatDailyLimit(settings.megaDebridAccountDailyLimitBytes?.[account.id] || 0),
|
||||
originalDailyLimitBytes: settings.megaDebridAccountDailyLimitBytes?.[account.id] || 0
|
||||
};
|
||||
}
|
||||
const key = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "").find((entry) => entry.id === target.keyId);
|
||||
if (!key) {
|
||||
throw new Error("Der ausgewählte Debrid-Link-Key wurde nicht gefunden.");
|
||||
}
|
||||
return {
|
||||
target,
|
||||
login: "",
|
||||
password: "",
|
||||
token: key.token,
|
||||
dailyLimitGb: formatDailyLimit(settings.debridLinkApiKeyDailyLimitBytes?.[key.id] || 0),
|
||||
originalDailyLimitBytes: settings.debridLinkApiKeyDailyLimitBytes?.[key.id] || 0
|
||||
};
|
||||
}
|
||||
|
||||
function validateDailyLimit(value: string): string | null {
|
||||
const normalized = value.trim().replace(",", ".");
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(normalized);
|
||||
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 {
|
||||
const limitError = validateDailyLimit(state.dailyLimitGb);
|
||||
if (limitError) {
|
||||
return limitError;
|
||||
}
|
||||
if (state.target.type === "mega") {
|
||||
const target = state.target;
|
||||
const login = state.login.trim();
|
||||
if (!login || !state.password.trim()) {
|
||||
return "Login und Passwort werden benötigt.";
|
||||
}
|
||||
if (/[:\r\n]/.test(login)) {
|
||||
return "Der Login darf keinen Doppelpunkt oder Zeilenumbruch enthalten.";
|
||||
}
|
||||
if (/[\r\n]/.test(state.password)) {
|
||||
return "Das Passwort darf keinen Zeilenumbruch enthalten.";
|
||||
}
|
||||
const accounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword || "");
|
||||
if (!accounts.some((entry) => entry.id === target.accountId)) {
|
||||
return "Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.";
|
||||
}
|
||||
if (accounts.some((entry) => entry.id !== target.accountId && entry.login.toLowerCase() === login.toLowerCase())) {
|
||||
return "Dieser Mega-Debrid-Login ist bereits vorhanden.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (state.target.type === "debridlink") {
|
||||
const target = state.target;
|
||||
const token = state.token.trim();
|
||||
if (!token) {
|
||||
return "Der API-Key wird benötigt.";
|
||||
}
|
||||
if (/[,\r\n]/.test(token)) {
|
||||
return "Beim Bearbeiten ist genau ein API-Key erlaubt.";
|
||||
}
|
||||
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "");
|
||||
if (!keys.some((entry) => entry.id === target.keyId)) {
|
||||
return "Der ausgewählte Debrid-Link-Key wurde nicht gefunden.";
|
||||
}
|
||||
if (keys.some((entry) => entry.id !== target.keyId && entry.token === token)) {
|
||||
return "Dieser Debrid-Link-Key ist bereits vorhanden.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (["realdebrid-api", "bestdebrid-api", "alldebrid-api", "onefichier-api"].includes(state.target.kind) && !state.token.trim()) {
|
||||
return "Der Zugangstoken wird benötigt.";
|
||||
}
|
||||
if (["ddownload-login", "linksnappy-login"].includes(state.target.kind)) {
|
||||
if (!state.login.trim() || !state.password.trim()) {
|
||||
return "Login und Passwort werden benötigt.";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function applySingleEdit(settings: AppSettings, state: AccountEditState & { target: Extract<AccountEditTarget, { type: "single" }> }): AppSettings {
|
||||
const providerDailyLimitBytes = updateProviderLimit(settings, state.target.provider, state.dailyLimitGb, state.originalDailyLimitBytes);
|
||||
const token = state.token.trim();
|
||||
const login = state.login.trim();
|
||||
switch (state.target.kind) {
|
||||
case "realdebrid-api":
|
||||
return { ...settings, token, realDebridUseWebLogin: false, providerDailyLimitBytes };
|
||||
case "realdebrid-web":
|
||||
return { ...settings, token: "", realDebridUseWebLogin: true, providerDailyLimitBytes };
|
||||
case "bestdebrid-api":
|
||||
return { ...settings, bestToken: token, bestDebridUseWebLogin: false, providerDailyLimitBytes };
|
||||
case "bestdebrid-web":
|
||||
return { ...settings, bestToken: "", bestDebridUseWebLogin: true, providerDailyLimitBytes };
|
||||
case "alldebrid-api":
|
||||
return { ...settings, allDebridToken: token, allDebridUseWebLogin: false, providerDailyLimitBytes };
|
||||
case "alldebrid-web":
|
||||
return { ...settings, allDebridToken: "", allDebridUseWebLogin: true, providerDailyLimitBytes };
|
||||
case "ddownload-login":
|
||||
return { ...settings, ddownloadLogin: login, ddownloadPassword: state.password, providerDailyLimitBytes };
|
||||
case "onefichier-api":
|
||||
return { ...settings, oneFichierApiKey: token, providerDailyLimitBytes };
|
||||
case "linksnappy-login":
|
||||
return { ...settings, linkSnappyLogin: login, linkSnappyPassword: state.password, providerDailyLimitBytes };
|
||||
}
|
||||
}
|
||||
|
||||
const account = getMegaDebridAccountsForMode(settings, getMegaTargetMode(target))
|
||||
.find((entry) => entry.id === target.accountId);
|
||||
if (!account) {
|
||||
throw new Error("Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.");
|
||||
}
|
||||
return {
|
||||
target,
|
||||
login: account.login,
|
||||
password: account.password,
|
||||
token: "",
|
||||
dailyLimitGb: formatDailyLimit(settings.megaDebridAccountDailyLimitBytes?.[account.id] || 0),
|
||||
originalDailyLimitBytes: settings.megaDebridAccountDailyLimitBytes?.[account.id] || 0
|
||||
};
|
||||
}
|
||||
const key = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "").find((entry) => entry.id === target.keyId);
|
||||
if (!key) {
|
||||
throw new Error("Der ausgewählte Debrid-Link-Key wurde nicht gefunden.");
|
||||
}
|
||||
return {
|
||||
target,
|
||||
login: "",
|
||||
password: "",
|
||||
token: key.token,
|
||||
dailyLimitGb: formatDailyLimit(settings.debridLinkApiKeyDailyLimitBytes?.[key.id] || 0),
|
||||
originalDailyLimitBytes: settings.debridLinkApiKeyDailyLimitBytes?.[key.id] || 0
|
||||
};
|
||||
}
|
||||
|
||||
function validateDailyLimit(value: string): string | null {
|
||||
const normalized = value.trim().replace(",", ".");
|
||||
if (!normalized) {
|
||||
return null;
|
||||
}
|
||||
const parsed = Number(normalized);
|
||||
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 {
|
||||
const limitError = validateDailyLimit(state.dailyLimitGb);
|
||||
if (limitError) {
|
||||
return limitError;
|
||||
}
|
||||
if (state.target.type === "mega") {
|
||||
const target = state.target;
|
||||
const login = state.login.trim();
|
||||
if (!login || !state.password.trim()) {
|
||||
return "Login und Passwort werden benötigt.";
|
||||
}
|
||||
if (/[:\r\n]/.test(login)) {
|
||||
return "Der Login darf keinen Doppelpunkt oder Zeilenumbruch enthalten.";
|
||||
}
|
||||
if (/[\r\n]/.test(state.password)) {
|
||||
return "Das Passwort darf keinen Zeilenumbruch enthalten.";
|
||||
}
|
||||
const accounts = getMegaDebridAccountsForMode(settings, getMegaTargetMode(target));
|
||||
if (!accounts.some((entry) => entry.id === target.accountId)) {
|
||||
return "Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.";
|
||||
}
|
||||
if (accounts.some((entry) => entry.id !== target.accountId && entry.login.toLowerCase() === login.toLowerCase())) {
|
||||
return "Dieser Mega-Debrid-Login ist bereits vorhanden.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (state.target.type === "debridlink") {
|
||||
const target = state.target;
|
||||
const token = state.token.trim();
|
||||
if (!token) {
|
||||
return "Der API-Key wird benötigt.";
|
||||
}
|
||||
if (/[,\r\n]/.test(token)) {
|
||||
return "Beim Bearbeiten ist genau ein API-Key erlaubt.";
|
||||
}
|
||||
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "");
|
||||
if (!keys.some((entry) => entry.id === target.keyId)) {
|
||||
return "Der ausgewählte Debrid-Link-Key wurde nicht gefunden.";
|
||||
}
|
||||
if (keys.some((entry) => entry.id !== target.keyId && entry.token === token)) {
|
||||
return "Dieser Debrid-Link-Key ist bereits vorhanden.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (["realdebrid-api", "bestdebrid-api", "alldebrid-api", "onefichier-api"].includes(state.target.kind) && !state.token.trim()) {
|
||||
return "Der Zugangstoken wird benötigt.";
|
||||
}
|
||||
if (["ddownload-login", "linksnappy-login"].includes(state.target.kind)) {
|
||||
if (!state.login.trim() || !state.password.trim()) {
|
||||
return "Login und Passwort werden benötigt.";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function applySingleEdit(settings: AppSettings, state: AccountEditState & { target: Extract<AccountEditTarget, { type: "single" }> }): AppSettings {
|
||||
const providerDailyLimitBytes = updateProviderLimit(settings, state.target.provider, state.dailyLimitGb, state.originalDailyLimitBytes);
|
||||
const token = state.token.trim();
|
||||
const login = state.login.trim();
|
||||
switch (state.target.kind) {
|
||||
case "realdebrid-api":
|
||||
return { ...settings, token, realDebridUseWebLogin: false, providerDailyLimitBytes };
|
||||
case "realdebrid-web":
|
||||
return { ...settings, token: "", realDebridUseWebLogin: true, providerDailyLimitBytes };
|
||||
case "bestdebrid-api":
|
||||
return { ...settings, bestToken: token, bestDebridUseWebLogin: false, providerDailyLimitBytes };
|
||||
case "bestdebrid-web":
|
||||
return { ...settings, bestToken: "", bestDebridUseWebLogin: true, providerDailyLimitBytes };
|
||||
case "alldebrid-api":
|
||||
return { ...settings, allDebridToken: token, allDebridUseWebLogin: false, providerDailyLimitBytes };
|
||||
case "alldebrid-web":
|
||||
return { ...settings, allDebridToken: "", allDebridUseWebLogin: true, providerDailyLimitBytes };
|
||||
case "ddownload-login":
|
||||
return { ...settings, ddownloadLogin: login, ddownloadPassword: state.password, providerDailyLimitBytes };
|
||||
case "onefichier-api":
|
||||
return { ...settings, oneFichierApiKey: token, providerDailyLimitBytes };
|
||||
case "linksnappy-login":
|
||||
return { ...settings, linkSnappyLogin: login, linkSnappyPassword: state.password, providerDailyLimitBytes };
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (index < 0) {
|
||||
throw new Error("Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.");
|
||||
}
|
||||
const oldId = state.target.accountId;
|
||||
const login = state.login.trim();
|
||||
const newId = getMegaDebridAccountId(login);
|
||||
if (index < 0) {
|
||||
throw new Error("Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.");
|
||||
}
|
||||
const oldId = state.target.accountId;
|
||||
const login = state.login.trim();
|
||||
const newId = getMegaDebridAccountId(login);
|
||||
const nextAccounts = accounts.map((entry, accountIndex) => accountIndex === index
|
||||
? { login, password: state.password }
|
||||
: { login: entry.login, password: entry.password });
|
||||
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 {
|
||||
...settings,
|
||||
megaCredentials: serializeMegaDebridAccounts(nextAccounts),
|
||||
megaLogin: first.login,
|
||||
megaPassword: first.password,
|
||||
megaDebridDisabledAccountIds: idChanged
|
||||
? migrateDisabledId(settings.megaDebridDisabledAccountIds || [], oldId, newId)
|
||||
: [...(settings.megaDebridDisabledAccountIds || [])],
|
||||
megaDebridAccountDailyLimitBytes: updateTargetLimit(settings.megaDebridAccountDailyLimitBytes || {}, oldId, newId, state.dailyLimitGb, state.originalDailyLimitBytes),
|
||||
megaDebridAccountDailyUsageBytes: idChanged
|
||||
? withoutRecordKeys(settings.megaDebridAccountDailyUsageBytes || {}, oldId, newId)
|
||||
: { ...(settings.megaDebridAccountDailyUsageBytes || {}) },
|
||||
megaDebridAccountTotalUsageBytes: idChanged
|
||||
? withoutRecordKeys(settings.megaDebridAccountTotalUsageBytes || {}, oldId, newId)
|
||||
: { ...(settings.megaDebridAccountTotalUsageBytes || {}) },
|
||||
debridAccountStatuses: idChanged
|
||||
? withoutRecordKeys(settings.debridAccountStatuses || {}, oldId, newId)
|
||||
: { ...(settings.debridAccountStatuses || {}) }
|
||||
megaCredentials: mergedCredentials,
|
||||
megaLogin: first?.login || "",
|
||||
megaPassword: first?.password || "",
|
||||
megaDebridApiCredentials: apiCredentials,
|
||||
megaDebridWebCredentials: webCredentials,
|
||||
megaDebridApiEnabled: settings.megaDebridApiEnabled && parseMegaDebridAccounts(apiCredentials).length > 0,
|
||||
megaDebridWebEnabled: settings.megaDebridWebEnabled && parseMegaDebridAccounts(webCredentials).length > 0,
|
||||
megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])],
|
||||
megaDebridApiDisabledAccountIds: apiDisabledIds,
|
||||
megaDebridWebDisabledAccountIds: webDisabledIds,
|
||||
megaDebridAccountDailyLimitBytes: nextDailyLimits,
|
||||
megaDebridAccountDailyUsageBytes: retainOldMetadata(settings.megaDebridAccountDailyUsageBytes || {}),
|
||||
megaDebridAccountTotalUsageBytes: retainOldMetadata(settings.megaDebridAccountTotalUsageBytes || {}),
|
||||
debridAccountStatuses: retainOldMetadata(settings.debridAccountStatuses || {})
|
||||
};
|
||||
}
|
||||
|
||||
function applyDebridLinkEdit(settings: AppSettings, state: AccountEditState & { target: Extract<AccountEditTarget, { type: "debridlink" }> }): AppSettings {
|
||||
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "");
|
||||
const index = keys.findIndex((entry) => entry.id === state.target.keyId);
|
||||
if (index < 0) {
|
||||
throw new Error("Der ausgewählte Debrid-Link-Key wurde nicht gefunden.");
|
||||
}
|
||||
const oldId = state.target.keyId;
|
||||
const token = state.token.trim();
|
||||
const newId = getDebridLinkApiKeyId(token);
|
||||
const tokens = keys.map((entry, keyIndex) => keyIndex === index ? token : entry.token);
|
||||
const idChanged = oldId !== newId;
|
||||
return {
|
||||
...settings,
|
||||
debridLinkApiKeys: tokens.join("\n"),
|
||||
debridLinkDisabledKeyIds: idChanged
|
||||
? migrateDisabledId(settings.debridLinkDisabledKeyIds || [], oldId, newId)
|
||||
: [...(settings.debridLinkDisabledKeyIds || [])],
|
||||
debridLinkApiKeyDailyLimitBytes: updateTargetLimit(settings.debridLinkApiKeyDailyLimitBytes || {}, oldId, newId, state.dailyLimitGb, state.originalDailyLimitBytes),
|
||||
debridLinkApiKeyDailyUsageBytes: idChanged
|
||||
? withoutRecordKeys(settings.debridLinkApiKeyDailyUsageBytes || {}, oldId, newId)
|
||||
: { ...(settings.debridLinkApiKeyDailyUsageBytes || {}) },
|
||||
debridLinkApiKeyTotalUsageBytes: idChanged
|
||||
? withoutRecordKeys(settings.debridLinkApiKeyTotalUsageBytes || {}, oldId, newId)
|
||||
: { ...(settings.debridLinkApiKeyTotalUsageBytes || {}) },
|
||||
debridAccountStatuses: idChanged
|
||||
? withoutRecordKeys(settings.debridAccountStatuses || {}, oldId, newId)
|
||||
: { ...(settings.debridAccountStatuses || {}) }
|
||||
};
|
||||
}
|
||||
|
||||
export function applyAccountEdit(settings: AppSettings, state: AccountEditState): AppSettings {
|
||||
if (state.target.type === "single") {
|
||||
return applySingleEdit(settings, state as AccountEditState & { target: Extract<AccountEditTarget, { type: "single" }> });
|
||||
}
|
||||
if (state.target.type === "mega") {
|
||||
return applyMegaEdit(settings, state as AccountEditState & { target: Extract<AccountEditTarget, { type: "mega" }> });
|
||||
}
|
||||
return applyDebridLinkEdit(settings, state as AccountEditState & { target: Extract<AccountEditTarget, { type: "debridlink" }> });
|
||||
}
|
||||
|
||||
function clearSingleAccount(settings: AppSettings, target: Extract<AccountEditTarget, { type: "single" }>): AppSettings {
|
||||
const providerDailyLimitBytes = { ...(settings.providerDailyLimitBytes || {}) };
|
||||
const providerDailyUsageBytes = { ...(settings.providerDailyUsageBytes || {}) };
|
||||
const providerTotalUsageBytes = { ...(settings.providerTotalUsageBytes || {}) };
|
||||
delete providerDailyLimitBytes[target.provider];
|
||||
delete providerDailyUsageBytes[target.provider];
|
||||
delete providerTotalUsageBytes[target.provider];
|
||||
const base = { ...settings, providerDailyLimitBytes, providerDailyUsageBytes, providerTotalUsageBytes };
|
||||
switch (target.service) {
|
||||
case "realdebrid":
|
||||
return { ...base, token: "", realDebridUseWebLogin: false };
|
||||
case "bestdebrid":
|
||||
return { ...base, bestToken: "", bestDebridUseWebLogin: false };
|
||||
case "alldebrid":
|
||||
return { ...base, allDebridToken: "", allDebridUseWebLogin: false };
|
||||
case "ddownload":
|
||||
return { ...base, ddownloadLogin: "", ddownloadPassword: "" };
|
||||
case "onefichier":
|
||||
return { ...base, oneFichierApiKey: "" };
|
||||
case "linksnappy":
|
||||
return { ...base, linkSnappyLogin: "", linkSnappyPassword: "" };
|
||||
default:
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
export function removeAccountTarget(settings: AppSettings, target: AccountEditTarget): AppSettings {
|
||||
if (target.type === "single") {
|
||||
return clearSingleAccount(settings, target);
|
||||
|
||||
function applyDebridLinkEdit(settings: AppSettings, state: AccountEditState & { target: Extract<AccountEditTarget, { type: "debridlink" }> }): AppSettings {
|
||||
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "");
|
||||
const index = keys.findIndex((entry) => entry.id === state.target.keyId);
|
||||
if (index < 0) {
|
||||
throw new Error("Der ausgewählte Debrid-Link-Key wurde nicht gefunden.");
|
||||
}
|
||||
const oldId = state.target.keyId;
|
||||
const token = state.token.trim();
|
||||
const newId = getDebridLinkApiKeyId(token);
|
||||
const tokens = keys.map((entry, keyIndex) => keyIndex === index ? token : entry.token);
|
||||
const idChanged = oldId !== newId;
|
||||
return {
|
||||
...settings,
|
||||
debridLinkApiKeys: tokens.join("\n"),
|
||||
debridLinkDisabledKeyIds: idChanged
|
||||
? migrateDisabledId(settings.debridLinkDisabledKeyIds || [], oldId, newId)
|
||||
: [...(settings.debridLinkDisabledKeyIds || [])],
|
||||
debridLinkApiKeyDailyLimitBytes: updateTargetLimit(settings.debridLinkApiKeyDailyLimitBytes || {}, oldId, newId, state.dailyLimitGb, state.originalDailyLimitBytes),
|
||||
debridLinkApiKeyDailyUsageBytes: idChanged
|
||||
? withoutRecordKeys(settings.debridLinkApiKeyDailyUsageBytes || {}, oldId, newId)
|
||||
: { ...(settings.debridLinkApiKeyDailyUsageBytes || {}) },
|
||||
debridLinkApiKeyTotalUsageBytes: idChanged
|
||||
? withoutRecordKeys(settings.debridLinkApiKeyTotalUsageBytes || {}, oldId, newId)
|
||||
: { ...(settings.debridLinkApiKeyTotalUsageBytes || {}) },
|
||||
debridAccountStatuses: idChanged
|
||||
? withoutRecordKeys(settings.debridAccountStatuses || {}, oldId, newId)
|
||||
: { ...(settings.debridAccountStatuses || {}) }
|
||||
};
|
||||
}
|
||||
|
||||
export function applyAccountEdit(settings: AppSettings, state: AccountEditState): AppSettings {
|
||||
if (state.target.type === "single") {
|
||||
return applySingleEdit(settings, state as AccountEditState & { target: Extract<AccountEditTarget, { type: "single" }> });
|
||||
}
|
||||
if (state.target.type === "mega") {
|
||||
return applyMegaEdit(settings, state as AccountEditState & { target: Extract<AccountEditTarget, { type: "mega" }> });
|
||||
}
|
||||
return applyDebridLinkEdit(settings, state as AccountEditState & { target: Extract<AccountEditTarget, { type: "debridlink" }> });
|
||||
}
|
||||
|
||||
function clearSingleAccount(settings: AppSettings, target: Extract<AccountEditTarget, { type: "single" }>): AppSettings {
|
||||
const providerDailyLimitBytes = { ...(settings.providerDailyLimitBytes || {}) };
|
||||
const providerDailyUsageBytes = { ...(settings.providerDailyUsageBytes || {}) };
|
||||
const providerTotalUsageBytes = { ...(settings.providerTotalUsageBytes || {}) };
|
||||
delete providerDailyLimitBytes[target.provider];
|
||||
delete providerDailyUsageBytes[target.provider];
|
||||
delete providerTotalUsageBytes[target.provider];
|
||||
const base = { ...settings, providerDailyLimitBytes, providerDailyUsageBytes, providerTotalUsageBytes };
|
||||
switch (target.service) {
|
||||
case "realdebrid":
|
||||
return { ...base, token: "", realDebridUseWebLogin: false };
|
||||
case "bestdebrid":
|
||||
return { ...base, bestToken: "", bestDebridUseWebLogin: false };
|
||||
case "alldebrid":
|
||||
return { ...base, allDebridToken: "", allDebridUseWebLogin: false };
|
||||
case "ddownload":
|
||||
return { ...base, ddownloadLogin: "", ddownloadPassword: "" };
|
||||
case "onefichier":
|
||||
return { ...base, oneFichierApiKey: "" };
|
||||
case "linksnappy":
|
||||
return { ...base, linkSnappyLogin: "", linkSnappyPassword: "" };
|
||||
default:
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
export function removeAccountTarget(settings: AppSettings, target: AccountEditTarget): AppSettings {
|
||||
if (target.type === "single") {
|
||||
return clearSingleAccount(settings, target);
|
||||
}
|
||||
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)
|
||||
.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 {
|
||||
...settings,
|
||||
megaCredentials: serializeMegaDebridAccounts(accounts),
|
||||
megaLogin: first.login,
|
||||
megaPassword: first.password,
|
||||
megaDebridApiEnabled: accounts.length > 0 && settings.megaDebridApiEnabled,
|
||||
megaDebridWebEnabled: accounts.length > 0 && settings.megaDebridWebEnabled,
|
||||
megaDebridDisabledAccountIds: (settings.megaDebridDisabledAccountIds || []).filter((id) => id !== target.accountId),
|
||||
megaDebridAccountDailyLimitBytes: withoutRecordKeys(settings.megaDebridAccountDailyLimitBytes || {}, target.accountId),
|
||||
megaDebridAccountDailyUsageBytes: withoutRecordKeys(settings.megaDebridAccountDailyUsageBytes || {}, target.accountId),
|
||||
megaDebridAccountTotalUsageBytes: withoutRecordKeys(settings.megaDebridAccountTotalUsageBytes || {}, target.accountId),
|
||||
debridAccountStatuses: withoutRecordKeys(settings.debridAccountStatuses || {}, target.accountId)
|
||||
};
|
||||
}
|
||||
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "").filter((entry) => entry.id !== target.keyId);
|
||||
return {
|
||||
...settings,
|
||||
debridLinkApiKeys: keys.map((entry) => entry.token).join("\n"),
|
||||
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),
|
||||
debridAccountStatuses: withoutRecordKeys(settings.debridAccountStatuses || {}, target.keyId)
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAccountEditCheckSettings(settings: AppSettings, state: AccountEditState): AppSettings {
|
||||
megaCredentials: mergedCredentials,
|
||||
megaLogin: first?.login || "",
|
||||
megaPassword: first?.password || "",
|
||||
megaDebridApiCredentials: apiCredentials,
|
||||
megaDebridWebCredentials: webCredentials,
|
||||
megaDebridApiEnabled: parseMegaDebridAccounts(apiCredentials).length > 0 && settings.megaDebridApiEnabled,
|
||||
megaDebridWebEnabled: parseMegaDebridAccounts(webCredentials).length > 0 && settings.megaDebridWebEnabled,
|
||||
megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])],
|
||||
megaDebridApiDisabledAccountIds: apiDisabledIds,
|
||||
megaDebridWebDisabledAccountIds: webDisabledIds,
|
||||
megaDebridAccountDailyLimitBytes: keepOrRemoveMetadata(settings.megaDebridAccountDailyLimitBytes || {}),
|
||||
megaDebridAccountDailyUsageBytes: keepOrRemoveMetadata(settings.megaDebridAccountDailyUsageBytes || {}),
|
||||
megaDebridAccountTotalUsageBytes: keepOrRemoveMetadata(settings.megaDebridAccountTotalUsageBytes || {}),
|
||||
debridAccountStatuses: keepOrRemoveMetadata(settings.debridAccountStatuses || {})
|
||||
};
|
||||
}
|
||||
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "").filter((entry) => entry.id !== target.keyId);
|
||||
return {
|
||||
...settings,
|
||||
debridLinkApiKeys: keys.map((entry) => entry.token).join("\n"),
|
||||
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),
|
||||
debridAccountStatuses: withoutRecordKeys(settings.debridAccountStatuses || {}, target.keyId)
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAccountEditCheckSettings(settings: AppSettings, state: AccountEditState): AppSettings {
|
||||
if (state.target.type === "mega") {
|
||||
const login = state.login.trim();
|
||||
const credentials = serializeMegaDebridAccounts([{ login, password: state.password }]);
|
||||
const mode = getMegaTargetMode(state.target);
|
||||
return {
|
||||
...settings,
|
||||
megaCredentials: serializeMegaDebridAccounts([{ login, password: state.password }]),
|
||||
megaCredentials: credentials,
|
||||
megaLogin: login,
|
||||
megaPassword: state.password,
|
||||
megaDebridApiCredentials: mode === "api" ? credentials : "",
|
||||
megaDebridWebCredentials: mode === "web" ? credentials : "",
|
||||
megaDebridApiEnabled: mode === "api",
|
||||
megaDebridWebEnabled: mode === "web",
|
||||
megaDebridApiDisabledAccountIds: [],
|
||||
megaDebridWebDisabledAccountIds: [],
|
||||
debridLinkApiKeys: ""
|
||||
};
|
||||
}
|
||||
if (state.target.type === "debridlink") {
|
||||
return {
|
||||
};
|
||||
}
|
||||
if (state.target.type === "debridlink") {
|
||||
return {
|
||||
...settings,
|
||||
megaCredentials: "",
|
||||
megaLogin: "",
|
||||
megaPassword: "",
|
||||
megaDebridApiCredentials: "",
|
||||
megaDebridWebCredentials: "",
|
||||
debridLinkApiKeys: state.token.trim()
|
||||
};
|
||||
}
|
||||
return {
|
||||
...settings,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...settings,
|
||||
megaCredentials: "",
|
||||
megaLogin: "",
|
||||
megaPassword: "",
|
||||
megaDebridApiCredentials: "",
|
||||
megaDebridWebCredentials: "",
|
||||
debridLinkApiKeys: ""
|
||||
};
|
||||
}
|
||||
|
||||
export function validateAccountEditStatuses(state: AccountEditState, statuses: readonly DebridAccountStatus[]): string | null {
|
||||
if (state.target.type === "single") {
|
||||
return null;
|
||||
}
|
||||
if (statuses.length === 0) {
|
||||
return "Die Prüfung hat keinen Account zurückgegeben.";
|
||||
}
|
||||
if (statuses.length !== 1) {
|
||||
return "Die Prüfung hat mehr als den ausgewählten Account zurückgegeben.";
|
||||
}
|
||||
const expectedId = getAccountEditExpectedStatusId(state);
|
||||
const status = statuses[0];
|
||||
if (status.accountId !== expectedId) {
|
||||
return "Die Prüfung hat den falschen Account zurückgegeben.";
|
||||
}
|
||||
return status.valid ? null : status.message || "Zugangsdaten ungültig";
|
||||
}
|
||||
|
||||
export function getAccountEditExpectedStatusId(state: AccountEditState): string | null {
|
||||
if (state.target.type === "mega") {
|
||||
return getMegaDebridAccountId(state.login);
|
||||
}
|
||||
if (state.target.type === "debridlink") {
|
||||
return getDebridLinkApiKeyId(state.token);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function validateAccountEditStatuses(state: AccountEditState, statuses: readonly DebridAccountStatus[]): string | null {
|
||||
if (state.target.type === "single") {
|
||||
return null;
|
||||
}
|
||||
if (statuses.length === 0) {
|
||||
return "Die Prüfung hat keinen Account zurückgegeben.";
|
||||
}
|
||||
if (statuses.length !== 1) {
|
||||
return "Die Prüfung hat mehr als den ausgewählten Account zurückgegeben.";
|
||||
}
|
||||
const expectedId = getAccountEditExpectedStatusId(state);
|
||||
const status = statuses[0];
|
||||
if (status.accountId !== expectedId) {
|
||||
return "Die Prüfung hat den falschen Account zurückgegeben.";
|
||||
}
|
||||
return status.valid ? null : status.message || "Zugangsdaten ungültig";
|
||||
}
|
||||
|
||||
export function getAccountEditExpectedStatusId(state: AccountEditState): string | null {
|
||||
if (state.target.type === "mega") {
|
||||
return getMegaDebridAccountId(state.login);
|
||||
}
|
||||
if (state.target.type === "debridlink") {
|
||||
return getDebridLinkApiKeyId(state.token);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { AudioStripSummary, DebridProvider } from "../shared/types";
|
||||
import { extractHosterFromUrl } from "../shared/hoster";
|
||||
import { hosterIconSources } from "./hoster-icons";
|
||||
|
||||
export const providerLabels: Record<DebridProvider, string> = {
|
||||
@@ -48,13 +49,7 @@ export function formatDateTime(timestamp: number): string {
|
||||
}
|
||||
|
||||
export function extractHoster(url: string): string {
|
||||
try {
|
||||
const host = new URL(url).hostname.replace(/^www\./, "");
|
||||
const parts = host.split(".");
|
||||
return parts.length >= 2 ? parts[parts.length - 2] : host;
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
return extractHosterFromUrl(url);
|
||||
}
|
||||
|
||||
export function formatHosterLabel(hoster: string): { compact: string; title: string; iconSrc?: string } {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
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[] {
|
||||
const fromIndex = order.indexOf(draggedPackageId);
|
||||
@@ -26,36 +33,36 @@ export function sortPackageOrderByName(order: string[], packages: Record<string,
|
||||
return sorted;
|
||||
}
|
||||
|
||||
export function sortPackagesForDisplay(
|
||||
packages: PackageEntry[],
|
||||
itemsById: Record<string, DownloadItem>,
|
||||
running: boolean,
|
||||
autoSortPackagesByProgress: boolean
|
||||
): PackageEntry[] {
|
||||
export function sortPackagesForDisplay(
|
||||
packages: PackageEntry[],
|
||||
itemsById: Record<string, DownloadItem>,
|
||||
running: boolean,
|
||||
autoSortPackagesByProgress: boolean
|
||||
): PackageEntry[] {
|
||||
if (!running || !autoSortPackagesByProgress || packages.length <= 1) {
|
||||
return packages;
|
||||
}
|
||||
|
||||
const active: PackageEntry[] = [];
|
||||
const rest: PackageEntry[] = [];
|
||||
|
||||
// Float packages that have an active item to the top, but keep BOTH groups in
|
||||
// their original (queue) order. Earlier this sorted the active group by live
|
||||
// completedRatio/downloadedBytes — which change on every progress tick (every
|
||||
// 150-700ms), so active packages visibly reshuffled the whole time. A package
|
||||
// entering/leaving the active bucket is a real, discrete event (start/finish);
|
||||
// ranking *within* the bucket by live bytes was pure jitter nobody needs.
|
||||
for (const pkg of packages) {
|
||||
const hasActive = pkg.itemIds.some((id) => {
|
||||
const item = itemsById[id];
|
||||
return item != null && ACTIVE_PACKAGE_STATUSES.has(item.status);
|
||||
});
|
||||
(hasActive ? active : rest).push(pkg);
|
||||
}
|
||||
|
||||
if (active.length === 0 || active.length === packages.length) {
|
||||
return packages;
|
||||
}
|
||||
const active = packages
|
||||
.map((pkg, index) => ({ pkg, index }))
|
||||
.filter(({ pkg }) => isPackageActive(pkg, itemsById))
|
||||
.sort((left, right) => {
|
||||
const leftStartedAt = left.pkg.downloadStartedAt || 0;
|
||||
const rightStartedAt = right.pkg.downloadStartedAt || 0;
|
||||
if (leftStartedAt > 0 && rightStartedAt > 0 && leftStartedAt !== rightStartedAt) {
|
||||
return leftStartedAt - rightStartedAt;
|
||||
}
|
||||
if (leftStartedAt > 0 && rightStartedAt <= 0) return -1;
|
||||
if (leftStartedAt <= 0 && rightStartedAt > 0) return 1;
|
||||
return left.index - right.index;
|
||||
})
|
||||
.map(({ pkg }) => pkg);
|
||||
const activeSet = new Set(active.map((pkg) => pkg.id));
|
||||
const rest = packages.filter((pkg) => !activeSet.has(pkg.id));
|
||||
|
||||
if (active.length === 0) {
|
||||
return packages;
|
||||
}
|
||||
|
||||
return [...active, ...rest];
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.md-update-trigger {
|
||||
.md-update-trigger {
|
||||
display: inline-flex;
|
||||
height: 40px;
|
||||
min-width: 90px;
|
||||
@@ -164,8 +164,8 @@
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: var(--ui-primary);
|
||||
color: var(--ui-primary-text);
|
||||
background: var(--ui-update);
|
||||
color: var(--ui-update-text);
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
@@ -173,8 +173,8 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.md-update-trigger:hover {
|
||||
background: var(--ui-primary-hover);
|
||||
.md-update-trigger:hover {
|
||||
background: var(--ui-update-hover);
|
||||
}
|
||||
|
||||
.md-update-trigger:focus-visible,
|
||||
@@ -308,13 +308,18 @@
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.md-update-release-notes pre {
|
||||
margin: 10px 0 0;
|
||||
color: var(--ui-text-secondary);
|
||||
font: inherit;
|
||||
line-height: 20px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.md-update-release-notes pre {
|
||||
margin: 10px 0 0;
|
||||
max-height: min(360px, 45vh);
|
||||
overflow-y: auto;
|
||||
padding-right: 8px;
|
||||
color: var(--ui-text-secondary);
|
||||
font: inherit;
|
||||
line-height: 20px;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-gutter: stable;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.md-update-dialog-actions {
|
||||
display: flex;
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
--ui-primary: #D6D6D6;
|
||||
--ui-primary-hover: #E6E6E6;
|
||||
--ui-primary-text: #181A1F;
|
||||
--ui-update: #BAD0FC;
|
||||
--ui-update-hover: #8AA5DC;
|
||||
--ui-update-text: #181A1F;
|
||||
--ui-accent: #4A4A4A;
|
||||
--ui-focus: #9AB8E8;
|
||||
--ui-speed-accent: #4ADE80;
|
||||
@@ -48,6 +51,9 @@
|
||||
--ui-primary: #3A3A3A;
|
||||
--ui-primary-hover: #202020;
|
||||
--ui-primary-text: #FFFFFF;
|
||||
--ui-update: #BAD0FC;
|
||||
--ui-update-hover: #8AA5DC;
|
||||
--ui-update-text: #181A1F;
|
||||
--ui-accent: #5E5E5E;
|
||||
--ui-focus: #24558D;
|
||||
--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 (/^Passwort gefunden\b/i.test(status)) return "Passwort gefunden";
|
||||
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 (/^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);
|
||||
|
||||
@@ -420,8 +420,8 @@
|
||||
}
|
||||
|
||||
.downloads-cell-slot > :is(.downloads-status-cell, .downloads-service-cell) {
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
login: string;
|
||||
password: string;
|
||||
index: number;
|
||||
label: 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_PRIME = 0x100000001b3n;
|
||||
@@ -85,6 +100,52 @@ export function serializeMegaDebridAccounts(accounts: { login: string; password:
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function getMegaDebridAccountIds(raw: string, legacyPassword = ""): string[] {
|
||||
return parseMegaDebridAccounts(raw, legacyPassword).map((entry) => entry.id);
|
||||
}
|
||||
export function getMegaDebridAccountIds(raw: string, legacyPassword = ""): string[] {
|
||||
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 DebridLinkKeyByteMap = Record<string, number>;
|
||||
@@ -6,7 +7,7 @@ export type DebridLinkKeyByteMap = Record<string, number>;
|
||||
type ProviderDailySettings =
|
||||
Pick<AppSettings, "providerDailyLimitBytes" | "providerDailyUsageBytes" | "providerDailyUsageDay">
|
||||
& Partial<Pick<AppSettings, "debridLinkApiKeyDailyLimitBytes" | "debridLinkApiKeyDailyUsageBytes">>
|
||||
& Partial<Pick<AppSettings, "megaDebridDisabledAccountIds" | "megaDebridAccountDailyLimitBytes" | "megaDebridAccountDailyUsageBytes">>;
|
||||
& Partial<Pick<AppSettings, "megaDebridDisabledAccountIds" | "megaDebridApiDisabledAccountIds" | "megaDebridWebDisabledAccountIds" | "megaDebridAccountDailyLimitBytes" | "megaDebridAccountDailyUsageBytes">>;
|
||||
|
||||
type ProviderUsageSettings =
|
||||
ProviderDailySettings
|
||||
@@ -250,9 +251,12 @@ export function addDebridLinkApiKeyTotalUsageBytes(
|
||||
};
|
||||
}
|
||||
|
||||
export function isMegaDebridAccountDisabled(settings: ProviderDailySettings, accountId: string): boolean {
|
||||
return Array.isArray(settings.megaDebridDisabledAccountIds) && settings.megaDebridDisabledAccountIds.includes(accountId);
|
||||
}
|
||||
export function isMegaDebridAccountDisabled(settings: ProviderDailySettings, accountId: string, mode?: MegaDebridAccountMode): boolean {
|
||||
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 {
|
||||
return normalizePositiveBytes(settings.megaDebridAccountDailyLimitBytes?.[accountId]);
|
||||
|
||||
+9
-5
@@ -71,10 +71,12 @@ export interface AppSettings {
|
||||
language: AppLanguage;
|
||||
token: string;
|
||||
realDebridUseWebLogin: boolean;
|
||||
megaLogin: string;
|
||||
megaPassword: string;
|
||||
megaCredentials: string;
|
||||
megaDebridApiEnabled: boolean;
|
||||
megaLogin: string;
|
||||
megaPassword: string;
|
||||
megaCredentials: string;
|
||||
megaDebridApiCredentials: string;
|
||||
megaDebridWebCredentials: string;
|
||||
megaDebridApiEnabled: boolean;
|
||||
megaDebridWebEnabled: boolean;
|
||||
megaDebridPreferApi: boolean;
|
||||
bestToken: string;
|
||||
@@ -158,7 +160,9 @@ export interface AppSettings {
|
||||
debridLinkApiKeyDailyLimitBytes: Record<string, number>;
|
||||
debridLinkApiKeyDailyUsageBytes: Record<string, number>;
|
||||
debridLinkApiKeyTotalUsageBytes: Record<string, number>;
|
||||
megaDebridDisabledAccountIds: string[];
|
||||
megaDebridDisabledAccountIds: string[];
|
||||
megaDebridApiDisabledAccountIds: string[];
|
||||
megaDebridWebDisabledAccountIds: string[];
|
||||
megaDebridAccountDailyLimitBytes: Record<string, number>;
|
||||
megaDebridAccountDailyUsageBytes: Record<string, number>;
|
||||
megaDebridAccountTotalUsageBytes: Record<string, number>;
|
||||
|
||||
Reference in New Issue
Block a user