Compare commits

..

No commits in common. "c182bc9269edad8dc65032be451bc311e42310b4" and "672c74f98f926d4001b465c606a12c25a4ab08f6" have entirely different histories.

11 changed files with 3002 additions and 3615 deletions

View File

@ -1,91 +0,0 @@
# Mega-Debrid Multi-Account Support
> **For agentic workers:** Use superpowers:subagent-driven-development to implement this plan.
**Goal:** Multiple Mega-Debrid accounts with automatic fallback when an account hits Fair-Use limits or errors.
**Architecture:** Follow the existing Debrid-Link multi-key pattern. Store credentials as newline-separated `login:password` pairs. Account rotation uses linear iteration with cooldown/disable/daily-limit checks.
**Tech Stack:** TypeScript, Electron, React
---
### Task 1: Create mega-debrid-accounts.ts parser module
**Files:**
- Create: `src/shared/mega-debrid-accounts.ts`
- [ ] Create `MegaDebridAccountEntry` interface (id, login, password, index, label, maskedLogin)
- [ ] Create `parseMegaDebridAccounts(raw: string): MegaDebridAccountEntry[]` - split by newlines, parse `login:password` pairs, deduplicate by login, generate stable IDs via FNV-1a hash (`mda_` prefix)
- [ ] Create `getMegaDebridAccountId(login: string): string`
- [ ] Create `maskMegaDebridLogin(login: string): string`
- [ ] Create `getMegaDebridAccountLabel(index: number): string` - "Account 1", "Account 2"
- [ ] Create `serializeMegaDebridAccounts(accounts: {login: string, password: string}[]): string` - back to newline-separated format
- [ ] Backward compat: if raw string has no `:` separator, treat as legacy single-login (use megaPassword from settings)
### Task 2: Extend AppSettings with multi-account fields
**Files:**
- Modify: `src/shared/types.ts`
- [ ] Replace `megaLogin: string``megaCredentials: string` (newline-separated `login:password` pairs)
- [ ] Keep `megaPassword: string` for backward compat (migration reads it once)
- [ ] Add `megaDebridDisabledAccountIds: string[]`
- [ ] Add `megaDebridAccountDailyLimitBytes: Record<string, number>`
- [ ] Add `megaDebridAccountDailyUsageBytes: Record<string, number>`
- [ ] Add `megaDebridAccountTotalUsageBytes: Record<string, number>`
### Task 3: Add per-account daily limit functions
**Files:**
- Modify: `src/shared/provider-daily-limits.ts`
- [ ] Add `getMegaDebridAccountDailyLimitBytes(settings, accountId)`
- [ ] Add `getMegaDebridAccountDailyUsageBytes(settings, accountId, epochMs)`
- [ ] Add `isMegaDebridAccountDailyLimitReached(settings, accountId, epochMs)`
- [ ] Add `addMegaDebridAccountDailyUsageBytes(settings, accountId, bytes, epochMs)`
- [ ] Add `addMegaDebridAccountTotalUsageBytes(settings, accountId, bytes)`
- [ ] Add `isMegaDebridAccountDisabled(settings, accountId)`
### Task 4: Migrate storage from single to multi-account
**Files:**
- Modify: `src/main/storage.ts`
- [ ] In `normalizeSettings`: migrate old `megaLogin`+`megaPassword` → `megaCredentials` format (`login:password`)
- [ ] Normalize new fields with defaults
### Task 5: Implement account rotation in debrid.ts
**Files:**
- Modify: `src/main/debrid.ts`
- [ ] Add in-memory cooldown cache for Mega accounts (like `debridLinkKeyCooldowns`)
- [ ] Update `hasMegaDebridCredentials()` to check `parseMegaDebridAccounts().length > 0`
- [ ] Update Mega-Debrid API unrestrict to iterate accounts (skip disabled/limited/cooldown)
- [ ] Update Mega-Debrid Web unrestrict to iterate accounts
- [ ] Return `sourceAccountId` and `sourceAccountLabel` on success
- [ ] On failure: classify error, apply cooldown, try next account
### Task 6: Update download-manager usage tracking
**Files:**
- Modify: `src/main/download-manager.ts`
- [ ] Track per-account bytes for Mega-Debrid (like Debrid-Link key tracking)
- [ ] Update `isProviderDailyLimited` to check if ANY Mega account is available
### Task 7: Update UI for multi-account management
**Files:**
- Modify: `src/renderer/App.tsx`
- [ ] Update Mega-Debrid account dialog: textarea for credentials (`login:password` per line)
- [ ] Display account list with masked logins, enable/disable toggle, per-account daily limits
- [ ] Update account summary display to show individual accounts
### Task 8: Tests
- [ ] Unit tests for `parseMegaDebridAccounts` (parse, deduplicate, legacy compat)
- [ ] Unit tests for per-account daily limits
- [ ] Run full test suite: `npx vitest run`

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.7.110", "version": "1.7.109",
"description": "Desktop downloader", "description": "Desktop downloader",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -46,7 +46,6 @@ export function defaultSettings(): AppSettings {
realDebridUseWebLogin: false, realDebridUseWebLogin: false,
megaLogin: "", megaLogin: "",
megaPassword: "", megaPassword: "",
megaCredentials: "",
megaDebridApiEnabled: false, megaDebridApiEnabled: false,
megaDebridWebEnabled: false, megaDebridWebEnabled: false,
megaDebridPreferApi: true, megaDebridPreferApi: true,
@ -119,10 +118,6 @@ export function defaultSettings(): AppSettings {
debridLinkApiKeyDailyLimitBytes: {}, debridLinkApiKeyDailyLimitBytes: {},
debridLinkApiKeyDailyUsageBytes: {}, debridLinkApiKeyDailyUsageBytes: {},
debridLinkApiKeyTotalUsageBytes: {}, debridLinkApiKeyTotalUsageBytes: {},
megaDebridDisabledAccountIds: [],
megaDebridAccountDailyLimitBytes: {},
megaDebridAccountDailyUsageBytes: {},
megaDebridAccountTotalUsageBytes: {},
providerDailyUsageDay: getProviderUsageDayKey(), providerDailyUsageDay: getProviderUsageDayKey(),
scheduledStartEpochMs: 0 scheduledStartEpochMs: 0
}; };

View File

@ -1,7 +1,6 @@
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys"; import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
import { parseMegaDebridAccounts, type MegaDebridAccountEntry } from "../shared/mega-debrid-accounts";
import { AllDebridHostInfo, AppSettings, DebridFallbackProvider, DebridLinkHostLimitInfo, DebridProvider } from "../shared/types"; import { AllDebridHostInfo, AppSettings, DebridFallbackProvider, DebridLinkHostLimitInfo, DebridProvider } from "../shared/types";
import { isDebridLinkApiKeyDailyLimitReached, isMegaDebridAccountDisabled, isMegaDebridAccountDailyLimitReached, isProviderDailyLimitReached } from "../shared/provider-daily-limits"; import { isDebridLinkApiKeyDailyLimitReached, isProviderDailyLimitReached } from "../shared/provider-daily-limits";
import { APP_VERSION, REQUEST_RETRIES } from "./constants"; import { APP_VERSION, REQUEST_RETRIES } from "./constants";
import { logger } from "./logger"; import { logger } from "./logger";
import { RealDebridClient, UnrestrictedLink } from "./realdebrid"; import { RealDebridClient, UnrestrictedLink } from "./realdebrid";
@ -95,62 +94,6 @@ function getDebridLinkKeyCooldownState(
}; };
} }
/** Per-account cooldown cache for Mega-Debrid: accountId → expiry timestamp. */
type MegaDebridCooldownCategory = "invalid" | "rate_limit" | "quota" | "temporary" | "skip";
type MegaDebridCooldownDetail = { until: number; message: string; category: MegaDebridCooldownCategory };
const megaDebridAccountCooldowns = new Map<string, MegaDebridCooldownDetail>();
const MEGA_DEBRID_ACCOUNT_COOLDOWN_MS = 120_000; // 2 min cooldown per failed account
const MEGA_DEBRID_INVALID_ACCOUNT_COOLDOWN_MS = 60 * 60 * 1000;
export function resetMegaDebridRuntimeStateForTests(): void {
megaDebridAccountCooldowns.clear();
}
export function primeMegaDebridRuntimeCooldownForTests(accountId: string, cooldownMs: number, message = "Mega-Debrid Account im Cooldown"): void {
setMegaDebridAccountCooldownState(accountId, cooldownMs, message, "temporary");
}
function clearMegaDebridAccountCooldownState(accountId: string): void {
megaDebridAccountCooldowns.delete(accountId);
}
function setMegaDebridAccountCooldownState(
accountId: string,
cooldownMs: number,
message: string,
category: MegaDebridCooldownCategory
): void {
if (!Number.isFinite(cooldownMs) || cooldownMs <= 0) {
clearMegaDebridAccountCooldownState(accountId);
return;
}
megaDebridAccountCooldowns.set(accountId, {
until: Date.now() + Math.max(1000, Math.floor(cooldownMs)),
message,
category
});
}
export function getMegaDebridAccountCooldownState(
accountId: string,
now = Date.now()
): { until: number; remainingMs: number; message: string; category: MegaDebridCooldownCategory } | null {
const detail = megaDebridAccountCooldowns.get(accountId);
if (!detail) {
return null;
}
if (detail.until <= now) {
clearMegaDebridAccountCooldownState(accountId);
return null;
}
return {
until: detail.until,
remainingMs: detail.until - now,
message: detail.message,
category: detail.category
};
}
const LINKSNAPPY_API_BASE = "https://linksnappy.com/api"; const LINKSNAPPY_API_BASE = "https://linksnappy.com/api";
const PROVIDER_LABELS: Record<DebridProvider, string> = { const PROVIDER_LABELS: Record<DebridProvider, string> = {
@ -203,11 +146,7 @@ function cloneSettings(settings: AppSettings): AppSettings {
providerTotalUsageBytes: { ...(settings.providerTotalUsageBytes || {}) }, providerTotalUsageBytes: { ...(settings.providerTotalUsageBytes || {}) },
debridLinkApiKeyDailyLimitBytes: { ...(settings.debridLinkApiKeyDailyLimitBytes || {}) }, debridLinkApiKeyDailyLimitBytes: { ...(settings.debridLinkApiKeyDailyLimitBytes || {}) },
debridLinkApiKeyDailyUsageBytes: { ...(settings.debridLinkApiKeyDailyUsageBytes || {}) }, debridLinkApiKeyDailyUsageBytes: { ...(settings.debridLinkApiKeyDailyUsageBytes || {}) },
debridLinkApiKeyTotalUsageBytes: { ...(settings.debridLinkApiKeyTotalUsageBytes || {}) }, debridLinkApiKeyTotalUsageBytes: { ...(settings.debridLinkApiKeyTotalUsageBytes || {}) }
megaDebridDisabledAccountIds: [...(settings.megaDebridDisabledAccountIds || [])],
megaDebridAccountDailyLimitBytes: { ...(settings.megaDebridAccountDailyLimitBytes || {}) },
megaDebridAccountDailyUsageBytes: { ...(settings.megaDebridAccountDailyUsageBytes || {}) },
megaDebridAccountTotalUsageBytes: { ...(settings.megaDebridAccountTotalUsageBytes || {}) }
}; };
} }
@ -221,29 +160,8 @@ export function getAvailableDebridLinkApiKeys(settings: AppSettings, epochMs = D
); );
} }
/** Returns Mega-Debrid accounts that are not disabled and not daily-limited. */
export function getAvailableMegaDebridAccounts(settings: AppSettings, epochMs = Date.now()): MegaDebridAccountEntry[] {
return getMegaDebridAccountList(settings).filter(
(entry) => !isMegaDebridAccountDisabled(settings, entry.id) && !isMegaDebridAccountDailyLimitReached(settings, entry.id, epochMs)
);
}
/** Resolves the full list of Mega-Debrid accounts from settings (multi-account or legacy single). */
function getMegaDebridAccountList(settings: AppSettings): MegaDebridAccountEntry[] {
// Multi-account format: newline-separated "login:password" pairs in megaCredentials
const multiAccounts = parseMegaDebridAccounts(settings.megaCredentials || "");
if (multiAccounts.length > 0) {
return multiAccounts;
}
// Backward compat: single legacy megaLogin/megaPassword
if (settings.megaLogin?.trim() && settings.megaPassword?.trim()) {
return parseMegaDebridAccounts(settings.megaLogin.trim(), settings.megaPassword.trim());
}
return [];
}
function hasMegaDebridCredentials(settings: AppSettings): boolean { function hasMegaDebridCredentials(settings: AppSettings): boolean {
return getMegaDebridAccountList(settings).length > 0; return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim());
} }
function isMegaDebridModeEnabled(settings: AppSettings, mode: "api" | "web"): boolean { function isMegaDebridModeEnabled(settings: AppSettings, mode: "api" | "web"): boolean {
@ -1086,11 +1004,11 @@ class MegaDebridClient {
private allowApiFallback: boolean; private allowApiFallback: boolean;
/** Per-account API token cache: login (lowercase) → { token, timestamp } */ private static cachedApiToken = "";
private static cachedApiTokens = new Map<string, { token: string; at: number }>();
/** Per-account pending connect deduplication: login (lowercase) → promise */ private static cachedApiTokenAt = 0;
private static pendingConnects = new Map<string, Promise<string | null>>();
private static pendingConnect: Promise<string | null> | null = null;
public constructor(login: string, password: string, mode: "api" | "web", allowApiFallback: boolean, megaWebUnrestrict?: MegaWebUnrestrictor) { public constructor(login: string, password: string, mode: "api" | "web", allowApiFallback: boolean, megaWebUnrestrict?: MegaWebUnrestrictor) {
this.login = login; this.login = login;
@ -1100,33 +1018,21 @@ class MegaDebridClient {
this.megaWebUnrestrict = megaWebUnrestrict; this.megaWebUnrestrict = megaWebUnrestrict;
} }
private get cacheKey(): string {
return this.login.trim().toLowerCase();
}
private async connectApi(signal?: AbortSignal): Promise<string | null> { private async connectApi(signal?: AbortSignal): Promise<string | null> {
const key = this.cacheKey;
// Return cached token if fresh (max 20 min) // Return cached token if fresh (max 20 min)
const cached = MegaDebridClient.cachedApiTokens.get(key); if (MegaDebridClient.cachedApiToken && Date.now() - MegaDebridClient.cachedApiTokenAt < 20 * 60 * 1000) {
if (cached && cached.token && Date.now() - cached.at < 20 * 60 * 1000) { return MegaDebridClient.cachedApiToken;
return cached.token;
} }
// Deduplicate parallel connectUser calls — only one in-flight request per account // Deduplicate parallel connectUser calls — only one in-flight request at a time
const pending = MegaDebridClient.pendingConnects.get(key); if (MegaDebridClient.pendingConnect) {
if (pending) { return MegaDebridClient.pendingConnect;
return pending;
} }
const promise = this.doConnectApi(signal).finally(() => { MegaDebridClient.pendingConnect = this.doConnectApi(signal).finally(() => {
MegaDebridClient.pendingConnects.delete(key); MegaDebridClient.pendingConnect = null;
}); });
MegaDebridClient.pendingConnects.set(key, promise); return MegaDebridClient.pendingConnect;
return promise;
}
private clearTokenCache(): void {
MegaDebridClient.cachedApiTokens.delete(this.cacheKey);
} }
private async doConnectApi(signal?: AbortSignal): Promise<string | null> { private async doConnectApi(signal?: AbortSignal): Promise<string | null> {
@ -1147,7 +1053,8 @@ class MegaDebridClient {
if (!token) { if (!token) {
return null; return null;
} }
MegaDebridClient.cachedApiTokens.set(this.cacheKey, { token, at: Date.now() }); MegaDebridClient.cachedApiToken = token;
MegaDebridClient.cachedApiTokenAt = Date.now();
return token; return token;
} }
@ -1171,7 +1078,8 @@ class MegaDebridClient {
if (!response.ok) { if (!response.ok) {
// Token might be invalid, clear cache // Token might be invalid, clear cache
if (response.status === 401 || response.status === 403) { if (response.status === 401 || response.status === 403) {
this.clearTokenCache(); MegaDebridClient.cachedApiToken = "";
MegaDebridClient.cachedApiTokenAt = 0;
} }
return null; return null;
} }
@ -1179,7 +1087,8 @@ class MegaDebridClient {
if (!payload || payload.response_code !== "ok") { if (!payload || payload.response_code !== "ok") {
// Token expired — clear cache for next attempt // Token expired — clear cache for next attempt
if (payload && String(payload.response_code || "").includes("token")) { if (payload && String(payload.response_code || "").includes("token")) {
this.clearTokenCache(); MegaDebridClient.cachedApiToken = "";
MegaDebridClient.cachedApiTokenAt = 0;
} }
const errorText = String(payload?.response_text || "").trim(); const errorText = String(payload?.response_text || "").trim();
if (errorText) { if (errorText) {
@ -1264,168 +1173,6 @@ class MegaDebridClient {
return this.unrestrictViaWeb(link, signal); return this.unrestrictViaWeb(link, signal);
} }
/**
* Multi-account rotation for Mega-Debrid, following the same pattern as Debrid-Link multi-key rotation.
* Iterates through all configured accounts, skipping disabled/daily-limited/cooldown accounts.
* On success: clears cooldown, returns result with sourceAccountId/sourceAccountLabel.
* On failure: classifies error, sets cooldown, tries next account.
*/
public static async unrestrictWithAccounts(
settings: AppSettings,
mode: "api" | "web",
allowApiFallback: boolean,
link: string,
megaWebUnrestrict: MegaWebUnrestrictor | undefined,
signal?: AbortSignal
): Promise<UnrestrictedLink> {
const accounts = getMegaDebridAccountList(settings);
if (accounts.length === 0) {
throw new Error("Mega-Debrid: Kein Account konfiguriert");
}
if (getAvailableMegaDebridAccounts(settings).length === 0) {
throw new Error("Mega-Debrid: Kein aktiver Account verfuegbar (deaktiviert oder am Tageslimit)");
}
const failures: string[] = [];
let usableAccountSeen = false;
const cooldownFailures: string[] = [];
let earliestCooldownUntil = 0;
const hasMultiple = accounts.length > 1;
// Always start from first account — use first available, skip disabled/limited/cooldown.
for (let idx = 0; idx < accounts.length; idx += 1) {
const account = accounts[idx];
const accountLabel = hasMultiple ? ` (${account.label})` : "";
if (isMegaDebridAccountDisabled(settings, account.id)) {
logger.info(`Mega-Debrid${accountLabel}: uebersprungen (manuell deaktiviert), pruefe naechsten Account`);
continue;
}
if (isMegaDebridAccountDailyLimitReached(settings, account.id)) {
logger.info(`Mega-Debrid${accountLabel}: uebersprungen (lokales Tageslimit erreicht), pruefe naechsten Account`);
continue;
}
// Cooldown key includes mode so API failures don't block Web attempts
const cooldownKey = `${account.id}:${mode}`;
const accountCooldownState = getMegaDebridAccountCooldownState(cooldownKey);
if (accountCooldownState) {
logger.info(`Mega-Debrid${accountLabel}: uebersprungen (Cooldown bis ${new Date(accountCooldownState.until).toLocaleTimeString()}), pruefe naechsten Account`);
cooldownFailures.push(`Mega-Debrid${accountLabel}: ${accountCooldownState.message}`);
if (!earliestCooldownUntil || accountCooldownState.until < earliestCooldownUntil) {
earliestCooldownUntil = accountCooldownState.until;
}
continue;
}
usableAccountSeen = true;
try {
const client = new MegaDebridClient(account.login, account.password, mode, allowApiFallback, megaWebUnrestrict);
const result = await client.unrestrictLink(link, signal);
clearMegaDebridAccountCooldownState(cooldownKey);
logger.info(`Mega-Debrid${accountLabel}: Unrestrict OK -> ${result.fileName || "?"}`);
return {
...result,
sourceLabel: account.label,
sourceAccountId: account.id,
sourceAccountLabel: account.label
};
} catch (error) {
const failure = MegaDebridClient.classifyAccountFailure(error);
failures.push(`Mega-Debrid${accountLabel}: ${failure.message}`);
if (failure.cooldownMs > 0) {
setMegaDebridAccountCooldownState(cooldownKey, failure.cooldownMs, failure.message, failure.category);
} else {
clearMegaDebridAccountCooldownState(cooldownKey);
}
if (failure.fatal) {
throw new Error(`Mega-Debrid${accountLabel}: ${failure.message}`);
}
const cooldownInfo = failure.cooldownMs > 0
? `, Cooldown ${Math.ceil(failure.cooldownMs / 1000)}s`
: "";
logger.warn(`Mega-Debrid${accountLabel}: ${failure.message}${cooldownInfo}, pruefe naechsten Account`);
}
}
if (!usableAccountSeen) {
if (cooldownFailures.length > 0 && earliestCooldownUntil > Date.now()) {
const retryMs = Math.max(1000, earliestCooldownUntil - Date.now() + 1000);
throw new Error(`mega_debrid_cooldown:${retryMs}:${cooldownFailures.join(" | ")}`);
}
throw new Error("Mega-Debrid: Kein aktiver Account verfuegbar");
}
throw new Error(failures.join(" | ") || "Mega-Debrid: Kein aktiver Account verfuegbar");
}
/**
* Classify error from a single Mega-Debrid account attempt.
* Returns whether the error is fatal (stop all accounts) and how long to cool down.
*/
private static classifyAccountFailure(
error: unknown
): { fatal: boolean; cooldownMs: number; message: string; category: MegaDebridCooldownCategory } {
const errorText = compactErrorText(error).replace(/^Error:\s*/i, "");
// Abort — don't retry other accounts
if (/aborted/i.test(errorText) && !/timeout/i.test(errorText)) {
return { fatal: true, cooldownMs: 0, message: errorText, category: "temporary" };
}
// Auth/login failures — long cooldown, try next account
if (/login|password|auth|credentials|unauthorized|forbidden/i.test(errorText) || /connectUser/i.test(errorText)) {
return {
fatal: false,
cooldownMs: MEGA_DEBRID_INVALID_ACCOUNT_COOLDOWN_MS,
message: `ungueltiger Account (${errorText})`,
category: "invalid"
};
}
// Permanent hoster errors — fatal, don't try other accounts
if (/permanent ungültig|hosternotavailable|file.?not.?found|file.?unavailable|link.?is.?dead/i.test(errorText)) {
return { fatal: true, cooldownMs: 0, message: errorText, category: "skip" };
}
// Quota/limit errors — cooldown, try next account
if (/quota|limit|exceeded|bandwidth/i.test(errorText)) {
return {
fatal: false,
cooldownMs: MEGA_DEBRID_ACCOUNT_COOLDOWN_MS,
message: `Quota/Limit erreicht (${errorText})`,
category: "quota"
};
}
// Rate limit
if (/rate.?limit|too.?many|429/i.test(errorText)) {
return {
fatal: false,
cooldownMs: MEGA_DEBRID_ACCOUNT_COOLDOWN_MS,
message: `Rate-Limit (${errorText})`,
category: "rate_limit"
};
}
// Temporary/transport errors — short cooldown, try next account
if (isRetryableErrorText(errorText) || /timeout|network|fetch|socket/i.test(errorText)) {
return {
fatal: false,
cooldownMs: MEGA_DEBRID_ACCOUNT_COOLDOWN_MS,
message: errorText || "temporaerer Fehler",
category: "temporary"
};
}
// Unknown errors — short cooldown, try next account (non-fatal)
return {
fatal: false,
cooldownMs: MEGA_DEBRID_ACCOUNT_COOLDOWN_MS,
message: errorText || "unbekannter Fehler",
category: "temporary"
};
}
} }
class BestDebridClient { class BestDebridClient {
@ -2719,12 +2466,6 @@ export class DebridService {
return true; return true;
} }
} }
if (effectiveProvider === "megadebrid-api" || effectiveProvider === "megadebrid-web") {
const configuredAccounts = getMegaDebridAccountList(settings);
if (configuredAccounts.length > 0 && getAvailableMegaDebridAccounts(settings).length === 0) {
return true;
}
}
return isProviderDailyLimitReached(settings, effectiveProvider); return isProviderDailyLimitReached(settings, effectiveProvider);
} }
@ -2737,9 +2478,6 @@ export class DebridService {
if (effectiveProvider === "debridlink" && parseDebridLinkApiKeys(settings.debridLinkApiKeys).length > 0 && getAvailableDebridLinkApiKeys(settings).length === 0) { if (effectiveProvider === "debridlink" && parseDebridLinkApiKeys(settings.debridLinkApiKeys).length > 0 && getAvailableDebridLinkApiKeys(settings).length === 0) {
return "Debrid-Link nicht verfuegbar (alle aktiven API-Keys deaktiviert oder ausgeschopft)"; return "Debrid-Link nicht verfuegbar (alle aktiven API-Keys deaktiviert oder ausgeschopft)";
} }
if ((effectiveProvider === "megadebrid-api" || effectiveProvider === "megadebrid-web") && getMegaDebridAccountList(settings).length > 0 && getAvailableMegaDebridAccounts(settings).length === 0) {
return "Mega-Debrid nicht verfuegbar (alle aktiven Accounts deaktiviert oder ausgeschopft)";
}
return `${PROVIDER_LABELS[effectiveProvider]} Tageslimit erreicht`; return `${PROVIDER_LABELS[effectiveProvider]} Tageslimit erreicht`;
} }
@ -2966,10 +2704,10 @@ export class DebridService {
return result; return result;
} }
if (effectiveProvider === "megadebrid-api") { if (effectiveProvider === "megadebrid-api") {
return MegaDebridClient.unrestrictWithAccounts(settings, "api", provider === "megadebrid" && settings.megaDebridPreferApi, link, this.options.megaWebUnrestrict, signal); return new MegaDebridClient(settings.megaLogin, settings.megaPassword, "api", provider === "megadebrid" && settings.megaDebridPreferApi, this.options.megaWebUnrestrict).unrestrictLink(link, signal);
} }
if (effectiveProvider === "megadebrid-web") { if (effectiveProvider === "megadebrid-web") {
return MegaDebridClient.unrestrictWithAccounts(settings, "web", false, link, this.options.megaWebUnrestrict, signal); return new MegaDebridClient(settings.megaLogin, settings.megaPassword, "web", false, this.options.megaWebUnrestrict).unrestrictLink(link, signal);
} }
if (effectiveProvider === "alldebrid") { if (effectiveProvider === "alldebrid") {
if (this.shouldUseAllDebridWeb(settings) && this.options.allDebridWebUnrestrict) { if (this.shouldUseAllDebridWeb(settings) && this.options.allDebridWebUnrestrict) {

View File

@ -2,7 +2,6 @@ import fs from "node:fs";
import fsp from "node:fs/promises"; import fsp from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys"; import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
import { getMegaDebridAccountIds } from "../shared/mega-debrid-accounts";
import { AppSettings, BandwidthScheduleEntry, DebridFallbackProvider, DebridProvider, DownloadItem, DownloadStatus, HistoryEntry, HistoryRetentionMode, PackageEntry, PackagePriority, SessionState } from "../shared/types"; import { AppSettings, BandwidthScheduleEntry, DebridFallbackProvider, DebridProvider, DownloadItem, DownloadStatus, HistoryEntry, HistoryRetentionMode, PackageEntry, PackagePriority, SessionState } from "../shared/types";
import { getProviderUsageDayKey } from "../shared/provider-daily-limits"; import { getProviderUsageDayKey } from "../shared/provider-daily-limits";
import { defaultSettings } from "./constants"; import { defaultSettings } from "./constants";
@ -282,12 +281,6 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
const currentUsageDay = getProviderUsageDayKey(); const currentUsageDay = getProviderUsageDayKey();
const megaLogin = asText(settings.megaLogin); const megaLogin = asText(settings.megaLogin);
const megaPassword = asText(settings.megaPassword); const megaPassword = asText(settings.megaPassword);
// Migrate legacy single-account to multi-account format
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 megaDebridPreferApi = settings.megaDebridPreferApi !== undefined ? Boolean(settings.megaDebridPreferApi) : true;
const hasMegaCreds = Boolean(megaLogin && megaPassword); const hasMegaCreds = Boolean(megaLogin && megaPassword);
const megaDebridApiEnabled = settings.megaDebridApiEnabled !== undefined const megaDebridApiEnabled = settings.megaDebridApiEnabled !== undefined
@ -329,7 +322,6 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
realDebridUseWebLogin: Boolean(settings.realDebridUseWebLogin), realDebridUseWebLogin: Boolean(settings.realDebridUseWebLogin),
megaLogin, megaLogin,
megaPassword, megaPassword,
megaCredentials,
megaDebridApiEnabled, megaDebridApiEnabled,
megaDebridWebEnabled, megaDebridWebEnabled,
megaDebridPreferApi, megaDebridPreferApi,
@ -414,12 +406,6 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
debridLinkApiKeyDailyLimitBytes, debridLinkApiKeyDailyLimitBytes,
debridLinkApiKeyDailyUsageBytes: providerDailyUsageDay === currentUsageDay ? debridLinkApiKeyDailyUsageBytes : {}, debridLinkApiKeyDailyUsageBytes: providerDailyUsageDay === currentUsageDay ? debridLinkApiKeyDailyUsageBytes : {},
debridLinkApiKeyTotalUsageBytes, debridLinkApiKeyTotalUsageBytes,
megaDebridDisabledAccountIds: normalizeStringList(settings.megaDebridDisabledAccountIds, megaDebridAccountIds),
megaDebridAccountDailyLimitBytes: normalizeNamedByteMap(settings.megaDebridAccountDailyLimitBytes, megaDebridAccountIds),
megaDebridAccountDailyUsageBytes: providerDailyUsageDay === currentUsageDay
? normalizeNamedByteMap(settings.megaDebridAccountDailyUsageBytes, megaDebridAccountIds)
: {},
megaDebridAccountTotalUsageBytes: normalizeNamedByteMap(settings.megaDebridAccountTotalUsageBytes, megaDebridAccountIds),
providerDailyUsageDay: providerDailyUsageDay === currentUsageDay ? providerDailyUsageDay : currentUsageDay, providerDailyUsageDay: providerDailyUsageDay === currentUsageDay ? providerDailyUsageDay : currentUsageDay,
scheduledStartEpochMs: clampNumber(settings.scheduledStartEpochMs, defaults.scheduledStartEpochMs, 0, Number.MAX_SAFE_INTEGER) scheduledStartEpochMs: clampNumber(settings.scheduledStartEpochMs, defaults.scheduledStartEpochMs, 0, Number.MAX_SAFE_INTEGER)
}; };
@ -468,7 +454,6 @@ function sanitizeCredentialPersistence(settings: AppSettings): AppSettings {
realDebridUseWebLogin: settings.realDebridUseWebLogin, realDebridUseWebLogin: settings.realDebridUseWebLogin,
megaLogin: "", megaLogin: "",
megaPassword: "", megaPassword: "",
megaCredentials: "",
bestToken: "", bestToken: "",
bestDebridUseWebLogin: settings.bestDebridUseWebLogin, bestDebridUseWebLogin: settings.bestDebridUseWebLogin,
allDebridToken: "", allDebridToken: "",

View File

@ -1,6 +1,5 @@
import { CSSProperties, DragEvent, KeyboardEvent as ReactKeyboardEvent, ReactElement, memo, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { CSSProperties, DragEvent, KeyboardEvent as ReactKeyboardEvent, ReactElement, memo, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys"; import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
import { parseMegaDebridAccounts } from "../shared/mega-debrid-accounts";
import type { import type {
AllDebridHostInfo, AllDebridHostInfo,
AppSettings, AppSettings,
@ -233,8 +232,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
serviceLabel: "Mega-Debrid", serviceLabel: "Mega-Debrid",
title: "Mega-Debrid API", title: "Mega-Debrid API",
modeLabel: "API", modeLabel: "API",
pickerDescription: "Login:Passwort-Paare für Mega-Debrid (API). Mehrere Accounts zeilenweise für Multi-Account.", pickerDescription: "Login nur über die API, ohne Web-Fallback.",
needsToken: true needsCredentials: true
}, },
{ {
kind: "megadebrid-web", kind: "megadebrid-web",
@ -242,8 +241,8 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
serviceLabel: "Mega-Debrid", serviceLabel: "Mega-Debrid",
title: "Mega-Debrid Web", title: "Mega-Debrid Web",
modeLabel: "Web", modeLabel: "Web",
pickerDescription: "Login:Passwort-Paare für Mega-Debrid (Web). Mehrere Accounts zeilenweise für Multi-Account.", pickerDescription: "Login nur über Web, ohne API-Fallback.",
needsToken: true needsCredentials: true
}, },
{ {
kind: "bestdebrid-api", kind: "bestdebrid-api",
@ -405,9 +404,9 @@ function getAccountPickerFunctionLabel(option: AccountOption): string {
case "alldebrid-web": case "alldebrid-web":
return "Browser-Login"; return "Browser-Login";
case "megadebrid-api": case "megadebrid-api":
return "Login:Passwort (API)"; return "Login + Passwort (API)";
case "megadebrid-web": case "megadebrid-web":
return "Login:Passwort (Web)"; return "Login + Passwort (Web)";
case "bestdebrid-web": case "bestdebrid-web":
return "Cookies.txt-Import"; return "Cookies.txt-Import";
case "alldebrid-api": case "alldebrid-api":
@ -421,7 +420,6 @@ function getAccountPickerFunctionLabel(option: AccountOption): string {
} }
function hasMegaDebridCredentials(settings: AppSettings): boolean { function hasMegaDebridCredentials(settings: AppSettings): boolean {
if (parseMegaDebridAccounts(settings.megaCredentials || "").length > 0) return true;
return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim()); return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim());
} }
@ -529,12 +527,8 @@ function summarizeAccount(kind: AccountKind, settings: AppSettings): string {
case "realdebrid-web": case "realdebrid-web":
return "Browser-Login"; return "Browser-Login";
case "megadebrid-api": case "megadebrid-api":
case "megadebrid-web": { case "megadebrid-web":
const megaAccounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword); return settings.megaLogin.trim() ? maskValue(settings.megaLogin.trim(), 2, 6) : "Login + Passwort";
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";
}
case "bestdebrid-api": case "bestdebrid-api":
return maskValue(settings.bestToken, 3, 3); return maskValue(settings.bestToken, 3, 3);
case "bestdebrid-web": case "bestdebrid-web":
@ -566,12 +560,6 @@ function summarizeAccountLines(kind: AccountKind, settings: AppSettings): string
return keys.map((entry) => `${entry.label}: ${entry.masked}`); return keys.map((entry) => `${entry.label}: ${entry.masked}`);
} }
} }
if (kind === "megadebrid-api" || kind === "megadebrid-web") {
const accounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword);
if (accounts.length > 1) {
return accounts.map((entry) => `${entry.label}: ${entry.maskedLogin}`);
}
}
return [summarizeAccount(kind, settings)]; return [summarizeAccount(kind, settings)];
} }
@ -595,14 +583,8 @@ function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | n
case "realdebrid-web": case "realdebrid-web":
return { mode, kind, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {} }; return { mode, kind, token: "", login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {} };
case "megadebrid-api": case "megadebrid-api":
case "megadebrid-web": { case "megadebrid-web":
// Populate token field with megaCredentials, or build from legacy megaLogin/megaPassword return { mode, kind, token: "", login: settings.megaLogin, password: settings.megaPassword, dailyLimitGb, keyDailyLimitGbById: {} };
let megaToken = (settings.megaCredentials || "").trim();
if (!megaToken && settings.megaLogin.trim() && settings.megaPassword.trim()) {
megaToken = `${settings.megaLogin.trim()}:${settings.megaPassword.trim()}`;
}
return { mode, kind, token: megaToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {} };
}
case "bestdebrid-api": case "bestdebrid-api":
return { mode, kind, token: settings.bestToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {} }; return { mode, kind, token: settings.bestToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {} };
case "bestdebrid-web": case "bestdebrid-web":
@ -660,18 +642,10 @@ function applyAccountDialogToSettings(settings: AppSettings, dialog: AccountDial
return { ...settings, token, realDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes }; return { ...settings, token, realDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes };
case "realdebrid-web": case "realdebrid-web":
return { ...settings, token: "", realDebridUseWebLogin: true, providerDailyLimitBytes: nextProviderDailyLimitBytes }; return { ...settings, token: "", realDebridUseWebLogin: true, providerDailyLimitBytes: nextProviderDailyLimitBytes };
case "megadebrid-api": { case "megadebrid-api":
const megaAccounts = parseMegaDebridAccounts(token); return { ...settings, megaLogin: login, megaPassword: password, megaDebridApiEnabled: true, megaDebridPreferApi: true, providerDailyLimitBytes: nextProviderDailyLimitBytes };
const firstLogin = megaAccounts.length > 0 ? megaAccounts[0].login : ""; case "megadebrid-web":
const firstPassword = megaAccounts.length > 0 ? megaAccounts[0].password : ""; return { ...settings, megaLogin: login, megaPassword: password, megaDebridWebEnabled: true, megaDebridPreferApi: false, providerDailyLimitBytes: nextProviderDailyLimitBytes };
return { ...settings, megaCredentials: token, megaLogin: firstLogin, megaPassword: firstPassword, megaDebridApiEnabled: true, megaDebridPreferApi: true, providerDailyLimitBytes: nextProviderDailyLimitBytes };
}
case "megadebrid-web": {
const megaAccounts = parseMegaDebridAccounts(token);
const firstLogin = megaAccounts.length > 0 ? megaAccounts[0].login : "";
const firstPassword = megaAccounts.length > 0 ? megaAccounts[0].password : "";
return { ...settings, megaCredentials: token, megaLogin: firstLogin, megaPassword: firstPassword, megaDebridWebEnabled: true, megaDebridPreferApi: false, providerDailyLimitBytes: nextProviderDailyLimitBytes };
}
case "bestdebrid-api": case "bestdebrid-api":
return { ...settings, bestToken: token, bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes }; return { ...settings, bestToken: token, bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes };
case "bestdebrid-web": case "bestdebrid-web":
@ -718,11 +692,11 @@ function clearAccountServiceFromSettings(settings: AppSettings, service: Account
case "megadebrid-api": case "megadebrid-api":
return settings.megaDebridWebEnabled return settings.megaDebridWebEnabled
? { ...settings, megaDebridApiEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes } ? { ...settings, megaDebridApiEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes }
: { ...settings, megaCredentials: "", megaLogin: "", megaPassword: "", megaDebridApiEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes }; : { ...settings, megaLogin: "", megaPassword: "", megaDebridApiEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes };
case "megadebrid-web": case "megadebrid-web":
return settings.megaDebridApiEnabled return settings.megaDebridApiEnabled
? { ...settings, megaDebridWebEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes } ? { ...settings, megaDebridWebEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes }
: { ...settings, megaCredentials: "", megaLogin: "", megaPassword: "", megaDebridWebEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes }; : { ...settings, megaLogin: "", megaPassword: "", megaDebridWebEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes };
case "bestdebrid": case "bestdebrid":
return { ...settings, bestToken: "", bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes }; return { ...settings, bestToken: "", bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes };
case "alldebrid": case "alldebrid":
@ -755,11 +729,6 @@ function validateAccountDialog(dialog: AccountDialogState): string | null {
if (option.needsToken && !dialog.token.trim()) { if (option.needsToken && !dialog.token.trim()) {
return `${option.title}: Bitte Zugangstoken eintragen.`; return `${option.title}: Bitte Zugangstoken eintragen.`;
} }
if ((dialog.kind === "megadebrid-api" || dialog.kind === "megadebrid-web") && dialog.token.trim()) {
if (parseMegaDebridAccounts(dialog.token).length === 0) {
return `${option.title}: Mindestens ein gültiges Login:Passwort-Paar eintragen (Format: login:passwort, pro Zeile).`;
}
}
if (option.needsCredentials) { if (option.needsCredentials) {
if (!dialog.login.trim()) { if (!dialog.login.trim()) {
return `${option.title}: Bitte Login oder E-Mail eintragen.`; return `${option.title}: Bitte Login oder E-Mail eintragen.`;
@ -823,7 +792,7 @@ type StatsSection = {
const emptySnapshot = (): UiSnapshot => ({ const emptySnapshot = (): UiSnapshot => ({
settings: { settings: {
token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaCredentials: "", megaDebridApiEnabled: false, megaDebridWebEnabled: false, megaDebridPreferApi: true, bestToken: "", bestDebridUseWebLogin: false, allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "", debridLinkApiKeys: "", linkSnappyLogin: "", linkSnappyPassword: "", token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaDebridApiEnabled: false, megaDebridWebEnabled: false, megaDebridPreferApi: true, bestToken: "", bestDebridUseWebLogin: false, allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "", debridLinkApiKeys: "", linkSnappyLogin: "", linkSnappyPassword: "",
debridLinkDisabledKeyIds: [], debridLinkDisabledKeyIds: [],
archivePasswordList: "", archivePasswordList: "",
rememberToken: true, providerOrder: [], providerPrimary: "realdebrid", providerSecondary: "none", rememberToken: true, providerOrder: [], providerPrimary: "realdebrid", providerSecondary: "none",
@ -848,10 +817,6 @@ const emptySnapshot = (): UiSnapshot => ({
debridLinkApiKeyDailyLimitBytes: {}, debridLinkApiKeyDailyLimitBytes: {},
debridLinkApiKeyDailyUsageBytes: {}, debridLinkApiKeyDailyUsageBytes: {},
debridLinkApiKeyTotalUsageBytes: {}, debridLinkApiKeyTotalUsageBytes: {},
megaDebridDisabledAccountIds: [],
megaDebridAccountDailyLimitBytes: {},
megaDebridAccountDailyUsageBytes: {},
megaDebridAccountTotalUsageBytes: {},
providerDailyUsageDay: getProviderUsageDayKey(), providerDailyUsageDay: getProviderUsageDayKey(),
scheduledStartEpochMs: 0 scheduledStartEpochMs: 0
}, },
@ -2117,12 +2082,8 @@ export function App(): ReactElement {
let statusLabel = "Aktiviert"; let statusLabel = "Aktiviert";
let note = ""; let note = "";
if (kind === "megadebrid-api") { if (kind === "megadebrid-api") {
const megaAccountCount = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword).length;
statusLabel = megaAccountCount > 1 ? `${megaAccountCount} Accounts` : "Aktiviert";
note = "Nur API aktiv. Kein Web-Fallback."; note = "Nur API aktiv. Kein Web-Fallback.";
} else if (kind === "megadebrid-web") { } else if (kind === "megadebrid-web") {
const megaAccountCount = parseMegaDebridAccounts(settingsDraft.megaCredentials || "", settingsDraft.megaPassword).length;
statusLabel = megaAccountCount > 1 ? `${megaAccountCount} Accounts` : "Aktiviert";
note = "Nur Web aktiv. Kein API-Fallback."; note = "Nur Web aktiv. Kein API-Fallback.";
} else if (kind === "realdebrid-web") { } else if (kind === "realdebrid-web") {
note = "Login kann bei Bedarf direkt aus der Liste geöffnet werden."; note = "Login kann bei Bedarf direkt aus der Liste geöffnet werden.";
@ -5045,8 +5006,10 @@ export function App(): ReactElement {
<button className="btn" disabled={actionBusy} onClick={() => { void onOpenRealDebridLogin(); }}>Real-Debrid Web-Login öffnen</button> <button className="btn" disabled={actionBusy} onClick={() => { void onOpenRealDebridLogin(); }}>Real-Debrid Web-Login öffnen</button>
</> </>
)} )}
<label>Mega-Debrid Accounts (Login:Passwort pro Zeile)</label> <label>Mega-Debrid Login</label>
<textarea rows={3} value={settingsDraft.megaCredentials || ""} onChange={(e) => setText("megaCredentials", e.target.value)} style={{ fontFamily: "monospace", resize: "vertical" }} placeholder={"user@example.com:passwort"} /> <input value={settingsDraft.megaLogin} onChange={(e) => setText("megaLogin", e.target.value)} />
<label>Mega-Debrid Passwort</label>
<input type="password" value={settingsDraft.megaPassword} onChange={(e) => setText("megaPassword", e.target.value)} />
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.megaDebridPreferApi} onChange={(e) => setBool("megaDebridPreferApi", e.target.checked)} /> Mega-Debrid bevorzugt über API (schneller, Fallback auf Web)</label> <label className="toggle-line"><input type="checkbox" checked={settingsDraft.megaDebridPreferApi} onChange={(e) => setBool("megaDebridPreferApi", e.target.checked)} /> Mega-Debrid bevorzugt über API (schneller, Fallback auf Web)</label>
<label>BestDebrid API Token</label> <label>BestDebrid API Token</label>
<input type="password" value={settingsDraft.bestToken} onChange={(e) => setText("bestToken", e.target.value)} /> <input type="password" value={settingsDraft.bestToken} onChange={(e) => setText("bestToken", e.target.value)} />
@ -5412,7 +5375,7 @@ export function App(): ReactElement {
<div className="account-modal-fields"> <div className="account-modal-fields">
{accountDialogOption.needsToken && ( {accountDialogOption.needsToken && (
<div> <div>
<label>{accountDialogOption.service === "alldebrid" || accountDialogOption.service === "onefichier" || accountDialogOption.service === "debridlink" ? "API-Key(s)" : accountDialogOption.service === "megadebrid-api" || accountDialogOption.service === "megadebrid-web" ? "Login:Passwort (pro Zeile)" : "Token"}</label> <label>{accountDialogOption.service === "alldebrid" || accountDialogOption.service === "onefichier" || accountDialogOption.service === "debridlink" ? "API-Key(s)" : "Token"}</label>
{accountDialogOption.service === "debridlink" ? ( {accountDialogOption.service === "debridlink" ? (
<textarea <textarea
rows={4} rows={4}
@ -5425,14 +5388,6 @@ export function App(): ReactElement {
} : prev)} } : prev)}
style={{ fontFamily: "monospace", resize: "vertical" }} style={{ fontFamily: "monospace", resize: "vertical" }}
/> />
) : accountDialogOption.service === "megadebrid-api" || accountDialogOption.service === "megadebrid-web" ? (
<textarea
rows={4}
placeholder={"user1@example.com:passwort1\nuser2@example.com:passwort2"}
value={accountDialog.token}
onChange={(event) => setAccountDialog((prev) => prev ? { ...prev, token: event.target.value } : prev)}
style={{ fontFamily: "monospace", resize: "vertical" }}
/>
) : ( ) : (
<input type="password" value={accountDialog.token} onChange={(event) => setAccountDialog((prev) => prev ? { ...prev, token: event.target.value } : prev)} /> <input type="password" value={accountDialog.token} onChange={(event) => setAccountDialog((prev) => prev ? { ...prev, token: event.target.value } : prev)} />
)} )}
@ -5502,10 +5457,10 @@ export function App(): ReactElement {
<div className="account-modal-note">Der Web-Login nutzt ein echtes Browserfenster, damit reCAPTCHA sauber läuft.</div> <div className="account-modal-note">Der Web-Login nutzt ein echtes Browserfenster, damit reCAPTCHA sauber läuft.</div>
)} )}
{accountDialog.kind === "megadebrid-api" && ( {accountDialog.kind === "megadebrid-api" && (
<div className="account-modal-note">Ein Login:Passwort-Paar pro Zeile. Mehrere Accounts werden rotierend genutzt. Dieser Account nutzt nur die Mega-Debrid API. Kein Web-Fallback.</div> <div className="account-modal-note">Dieser Account nutzt nur die Mega-Debrid API. Kein Web-Fallback.</div>
)} )}
{accountDialog.kind === "megadebrid-web" && ( {accountDialog.kind === "megadebrid-web" && (
<div className="account-modal-note">Ein Login:Passwort-Paar pro Zeile. Mehrere Accounts werden rotierend genutzt. Dieser Account nutzt nur Mega-Debrid Web. Kein API-Fallback.</div> <div className="account-modal-note">Dieser Account nutzt nur Mega-Debrid Web. Kein API-Fallback.</div>
)} )}
{accountDialogOption.service === "alldebrid" && allDebridHostInfo && ( {accountDialogOption.service === "alldebrid" && allDebridHostInfo && (

View File

@ -1,96 +0,0 @@
export interface MegaDebridAccountEntry {
id: string;
login: string;
password: string;
index: number;
label: string;
maskedLogin: string;
}
const FNV64_OFFSET_BASIS = 0xcbf29ce484222325n;
const FNV64_PRIME = 0x100000001b3n;
const FNV64_MASK = 0xffffffffffffffffn;
function fnv1a64(text: string): string {
let hash = FNV64_OFFSET_BASIS;
for (const char of text) {
hash ^= BigInt(char.codePointAt(0) || 0);
hash = (hash * FNV64_PRIME) & FNV64_MASK;
}
return hash.toString(36);
}
export function getMegaDebridAccountId(login: string): string {
return `mda_${fnv1a64(login.trim().toLowerCase())}`;
}
export function maskMegaDebridLogin(login: string): string {
const trimmed = login.trim();
if (!trimmed) {
return "Nicht hinterlegt";
}
if (trimmed.length <= 4) {
return `${trimmed[0]}${"*".repeat(trimmed.length - 1)}`;
}
return `${trimmed.slice(0, 2)}${"*".repeat(Math.max(3, trimmed.length - 4))}${trimmed.slice(-2)}`;
}
export function getMegaDebridAccountLabel(index: number): string {
return `Account ${index + 1}`;
}
/**
* Parse newline-separated "login:password" pairs.
* Falls back to treating the entire string as a single login if no colon
* is found (backward compat with old megaLogin field).
*/
export function parseMegaDebridAccounts(raw: string, legacyPassword = ""): MegaDebridAccountEntry[] {
const seen = new Set<string>();
const lines = String(raw || "")
.split(/\n+/)
.map((line) => line.trim())
.filter(Boolean);
const entries: MegaDebridAccountEntry[] = [];
for (const line of lines) {
const colonIdx = line.indexOf(":");
let login: string;
let password: string;
if (colonIdx >= 0) {
login = line.slice(0, colonIdx).trim();
password = line.slice(colonIdx + 1).trim();
} else {
// Legacy format: just a login, use the provided fallback password
login = line;
password = legacyPassword;
}
if (!login || !password) {
continue;
}
const key = login.toLowerCase();
if (seen.has(key)) {
continue;
}
seen.add(key);
entries.push({
id: getMegaDebridAccountId(login),
login,
password,
index: entries.length,
label: getMegaDebridAccountLabel(entries.length),
maskedLogin: maskMegaDebridLogin(login)
});
}
return entries;
}
export function serializeMegaDebridAccounts(accounts: { login: string; password: string }[]): string {
return accounts
.filter((a) => a.login.trim() && a.password.trim())
.map((a) => `${a.login.trim()}:${a.password.trim()}`)
.join("\n");
}
export function getMegaDebridAccountIds(raw: string, legacyPassword = ""): string[] {
return parseMegaDebridAccounts(raw, legacyPassword).map((entry) => entry.id);
}

View File

@ -5,13 +5,11 @@ export type DebridLinkKeyByteMap = Record<string, number>;
type ProviderDailySettings = type ProviderDailySettings =
Pick<AppSettings, "providerDailyLimitBytes" | "providerDailyUsageBytes" | "providerDailyUsageDay"> Pick<AppSettings, "providerDailyLimitBytes" | "providerDailyUsageBytes" | "providerDailyUsageDay">
& Partial<Pick<AppSettings, "debridLinkApiKeyDailyLimitBytes" | "debridLinkApiKeyDailyUsageBytes">> & Partial<Pick<AppSettings, "debridLinkApiKeyDailyLimitBytes" | "debridLinkApiKeyDailyUsageBytes">>;
& Partial<Pick<AppSettings, "megaDebridDisabledAccountIds" | "megaDebridAccountDailyLimitBytes" | "megaDebridAccountDailyUsageBytes">>;
type ProviderUsageSettings = type ProviderUsageSettings =
ProviderDailySettings ProviderDailySettings
& Partial<Pick<AppSettings, "providerTotalUsageBytes" | "debridLinkApiKeyTotalUsageBytes">> & Partial<Pick<AppSettings, "providerTotalUsageBytes" | "debridLinkApiKeyTotalUsageBytes">>;
& Partial<Pick<AppSettings, "megaDebridAccountTotalUsageBytes">>;
function normalizePositiveBytes(value: unknown): number { function normalizePositiveBytes(value: unknown): number {
const numeric = Number(value); const numeric = Number(value);
@ -249,83 +247,3 @@ export function addDebridLinkApiKeyTotalUsageBytes(
debridLinkApiKeyTotalUsageBytes: currentUsageBytes debridLinkApiKeyTotalUsageBytes: currentUsageBytes
}; };
} }
// ── Mega-Debrid per-account limits ──
export function isMegaDebridAccountDisabled(settings: ProviderDailySettings, accountId: string): boolean {
return Array.isArray(settings.megaDebridDisabledAccountIds) && settings.megaDebridDisabledAccountIds.includes(accountId);
}
export function getMegaDebridAccountDailyLimitBytes(settings: ProviderDailySettings, accountId: string): number {
return normalizePositiveBytes(settings.megaDebridAccountDailyLimitBytes?.[accountId]);
}
export function getMegaDebridAccountDailyUsageBytes(
settings: ProviderDailySettings,
accountId: string,
epochMs = Date.now()
): number {
if (settings.providerDailyUsageDay !== getProviderUsageDayKey(epochMs)) {
return 0;
}
return normalizePositiveBytes(settings.megaDebridAccountDailyUsageBytes?.[accountId]);
}
export function isMegaDebridAccountDailyLimitReached(
settings: ProviderDailySettings,
accountId: string,
epochMs = Date.now()
): boolean {
const limit = getMegaDebridAccountDailyLimitBytes(settings, accountId);
return limit > 0 && getMegaDebridAccountDailyUsageBytes(settings, accountId, epochMs) >= limit;
}
export function getMegaDebridAccountTotalUsageBytes(settings: ProviderUsageSettings, accountId: string): number {
return normalizePositiveBytes(settings.megaDebridAccountTotalUsageBytes?.[accountId]);
}
export function addMegaDebridAccountDailyUsageBytes(
settings: ProviderDailySettings,
accountId: string,
byteDelta: number,
epochMs = Date.now()
): Pick<AppSettings, "providerDailyUsageDay" | "megaDebridAccountDailyUsageBytes"> {
const increment = normalizePositiveBytes(byteDelta);
const dayKey = getProviderUsageDayKey(epochMs);
const currentUsageBytes = settings.providerDailyUsageDay === dayKey
? { ...(settings.megaDebridAccountDailyUsageBytes || {}) }
: {};
if (increment <= 0) {
return {
providerDailyUsageDay: dayKey,
megaDebridAccountDailyUsageBytes: currentUsageBytes
};
}
currentUsageBytes[accountId] = normalizePositiveBytes(currentUsageBytes[accountId]) + increment;
return {
providerDailyUsageDay: dayKey,
megaDebridAccountDailyUsageBytes: currentUsageBytes
};
}
export function addMegaDebridAccountTotalUsageBytes(
settings: ProviderUsageSettings,
accountId: string,
byteDelta: number
): Pick<AppSettings, "megaDebridAccountTotalUsageBytes"> {
const increment = normalizePositiveBytes(byteDelta);
const currentUsageBytes = { ...(settings.megaDebridAccountTotalUsageBytes || {}) };
if (increment <= 0) {
return {
megaDebridAccountTotalUsageBytes: currentUsageBytes
};
}
currentUsageBytes[accountId] = normalizePositiveBytes(currentUsageBytes[accountId]) + increment;
return {
megaDebridAccountTotalUsageBytes: currentUsageBytes
};
}

View File

@ -58,7 +58,6 @@ export interface AppSettings {
realDebridUseWebLogin: boolean; realDebridUseWebLogin: boolean;
megaLogin: string; megaLogin: string;
megaPassword: string; megaPassword: string;
megaCredentials: string;
megaDebridApiEnabled: boolean; megaDebridApiEnabled: boolean;
megaDebridWebEnabled: boolean; megaDebridWebEnabled: boolean;
megaDebridPreferApi: boolean; megaDebridPreferApi: boolean;
@ -131,10 +130,6 @@ export interface AppSettings {
debridLinkApiKeyDailyLimitBytes: Record<string, number>; debridLinkApiKeyDailyLimitBytes: Record<string, number>;
debridLinkApiKeyDailyUsageBytes: Record<string, number>; debridLinkApiKeyDailyUsageBytes: Record<string, number>;
debridLinkApiKeyTotalUsageBytes: Record<string, number>; debridLinkApiKeyTotalUsageBytes: Record<string, number>;
megaDebridDisabledAccountIds: string[];
megaDebridAccountDailyLimitBytes: Record<string, number>;
megaDebridAccountDailyUsageBytes: Record<string, number>;
megaDebridAccountTotalUsageBytes: Record<string, number>;
providerDailyUsageDay: string; providerDailyUsageDay: string;
scheduledStartEpochMs: number; scheduledStartEpochMs: number;
} }

View File

@ -2,14 +2,13 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { defaultSettings, REQUEST_RETRIES } from "../src/main/constants"; import { defaultSettings, REQUEST_RETRIES } from "../src/main/constants";
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys"; import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits"; import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
import { DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, normalizeResolvedFilename, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid"; import { DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, normalizeResolvedFilename, resetDebridLinkRuntimeStateForTests } from "../src/main/debrid";
const originalFetch = globalThis.fetch; const originalFetch = globalThis.fetch;
afterEach(() => { afterEach(() => {
globalThis.fetch = originalFetch; globalThis.fetch = originalFetch;
resetDebridLinkRuntimeStateForTests(); resetDebridLinkRuntimeStateForTests();
resetMegaDebridRuntimeStateForTests();
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
@ -20,7 +19,6 @@ describe("debrid service", () => {
token: "rd-token", token: "rd-token",
megaLogin: "user", megaLogin: "user",
megaPassword: "pass", megaPassword: "pass",
megaCredentials: "user:pass",
bestToken: "", bestToken: "",
providerOrder: [] as const, providerOrder: [] as const,
providerPrimary: "realdebrid" as const, providerPrimary: "realdebrid" as const,
@ -60,7 +58,6 @@ describe("debrid service", () => {
token: "rd-token", token: "rd-token",
megaLogin: "user", megaLogin: "user",
megaPassword: "pass", megaPassword: "pass",
megaCredentials: "user:pass",
providerPrimary: "realdebrid" as const, providerPrimary: "realdebrid" as const,
providerSecondary: "megadebrid" as const, providerSecondary: "megadebrid" as const,
providerTertiary: "bestdebrid" as const, providerTertiary: "bestdebrid" as const,
@ -969,7 +966,6 @@ describe("debrid service", () => {
allDebridToken: "", allDebridToken: "",
megaLogin: "user", megaLogin: "user",
megaPassword: "pass", megaPassword: "pass",
megaCredentials: "user:pass",
providerOrder: [] as const, providerOrder: [] as const,
providerPrimary: "megadebrid" as const, providerPrimary: "megadebrid" as const,
providerSecondary: "megadebrid" as const, providerSecondary: "megadebrid" as const,
@ -1003,7 +999,6 @@ describe("debrid service", () => {
allDebridToken: "", allDebridToken: "",
megaLogin: "user", megaLogin: "user",
megaPassword: "pass", megaPassword: "pass",
megaCredentials: "user:pass",
megaDebridApiEnabled: true, megaDebridApiEnabled: true,
megaDebridWebEnabled: true, megaDebridWebEnabled: true,
providerPrimary: "megadebrid-api" as const, providerPrimary: "megadebrid-api" as const,
@ -1034,7 +1029,6 @@ describe("debrid service", () => {
allDebridToken: "", allDebridToken: "",
megaLogin: "user", megaLogin: "user",
megaPassword: "pass", megaPassword: "pass",
megaCredentials: "user:pass",
megaDebridApiEnabled: true, megaDebridApiEnabled: true,
megaDebridWebEnabled: true, megaDebridWebEnabled: true,
providerOrder: [] as const, providerOrder: [] as const,
@ -1068,7 +1062,6 @@ describe("debrid service", () => {
allDebridToken: "", allDebridToken: "",
megaLogin: "user", megaLogin: "user",
megaPassword: "pass", megaPassword: "pass",
megaCredentials: "user:pass",
providerOrder: [] as const, providerOrder: [] as const,
providerPrimary: "megadebrid" as const, providerPrimary: "megadebrid" as const,
providerSecondary: "none" as const, providerSecondary: "none" as const,
@ -1111,7 +1104,6 @@ describe("debrid service", () => {
allDebridToken: "ad-token", allDebridToken: "ad-token",
megaLogin: "user", megaLogin: "user",
megaPassword: "pass", megaPassword: "pass",
megaCredentials: "user:pass",
providerPrimary: "megadebrid" as const, providerPrimary: "megadebrid" as const,
providerSecondary: "megadebrid" as const, providerSecondary: "megadebrid" as const,
providerTertiary: "megadebrid" as const, providerTertiary: "megadebrid" as const,
@ -1143,7 +1135,6 @@ describe("debrid service", () => {
token: "", token: "",
megaLogin: "user", megaLogin: "user",
megaPassword: "pass", megaPassword: "pass",
megaCredentials: "user:pass",
providerPrimary: "realdebrid" as const, providerPrimary: "realdebrid" as const,
providerSecondary: "megadebrid" as const, providerSecondary: "megadebrid" as const,
providerTertiary: "none" as const, providerTertiary: "none" as const,
@ -1168,7 +1159,6 @@ describe("debrid service", () => {
token: "rd-token", token: "rd-token",
megaLogin: "user", megaLogin: "user",
megaPassword: "pass", megaPassword: "pass",
megaCredentials: "user:pass",
providerPrimary: "realdebrid" as const, providerPrimary: "realdebrid" as const,
providerSecondary: "none" as const, providerSecondary: "none" as const,
providerTertiary: "none" as const, providerTertiary: "none" as const,

View File

@ -6167,7 +6167,6 @@ describe("download manager", () => {
...defaultSettings(), ...defaultSettings(),
megaLogin: "mega-user", megaLogin: "mega-user",
megaPassword: "mega-pass", megaPassword: "mega-pass",
megaCredentials: "mega-user:mega-pass",
megaDebridWebEnabled: true, megaDebridWebEnabled: true,
megaDebridApiEnabled: false, megaDebridApiEnabled: false,
megaDebridPreferApi: false, megaDebridPreferApi: false,
@ -9392,7 +9391,6 @@ describe("download manager", () => {
...defaultSettings(), ...defaultSettings(),
megaLogin: "mega-user", megaLogin: "mega-user",
megaPassword: "mega-pass", megaPassword: "mega-pass",
megaCredentials: "mega-user:mega-pass",
megaDebridApiEnabled: true, megaDebridApiEnabled: true,
providerDailyUsageDay: getProviderUsageDayKey(), providerDailyUsageDay: getProviderUsageDayKey(),
providerDailyUsageBytes: { realdebrid: 512 }, providerDailyUsageBytes: { realdebrid: 512 },