Add Mega-Debrid multi-account support with automatic fallback

Multiple Mega-Debrid accounts can now be configured as login:password
pairs (one per line). When an account hits Fair-Use limits or errors,
the next account is tried automatically.

- New parser module mega-debrid-accounts.ts (parse, ID generation,
  masking, serialization)
- Per-account daily limits, usage tracking, enable/disable
- Account rotation with per-mode cooldowns (API failures don't
  block Web attempts)
- Backward compatible: existing single megaLogin/megaPassword
  is auto-migrated to the new format
- UI: textarea for credentials, account list with masked logins

Follows the existing Debrid-Link multi-key pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-23 20:12:51 +01:00
parent 672c74f98f
commit 6df0834b67
10 changed files with 3614 additions and 3001 deletions

View File

@ -0,0 +1,91 @@
# 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

@ -21,8 +21,8 @@ export const WRITE_BUFFER_SIZE = 512 * 1024; // 512 KB write buffer (JDown
export const WRITE_FLUSH_TIMEOUT_MS = 2000; // 2s flush timeout export const WRITE_FLUSH_TIMEOUT_MS = 2000; // 2s flush timeout
export const ALLOCATION_UNIT_SIZE = 4096; // 4 KB NTFS alignment export const ALLOCATION_UNIT_SIZE = 4096; // 4 KB NTFS alignment
export const STREAM_HIGH_WATER_MARK = 512 * 1024; // 512 KB stream buffer — lower than before (2 MB) so backpressure triggers sooner when disk is slow export const STREAM_HIGH_WATER_MARK = 512 * 1024; // 512 KB stream buffer — lower than before (2 MB) so backpressure triggers sooner when disk is slow
export const DISK_BUSY_THRESHOLD_MS = 300; // Internal detection threshold for disk backpressure export const DISK_BUSY_THRESHOLD_MS = 300; // Internal detection threshold for disk backpressure
export const DISK_BUSY_STATUS_THRESHOLD_MS = 500; // Delay UI/log display for brief disk-write spikes export const DISK_BUSY_STATUS_THRESHOLD_MS = 500; // Delay UI/log display for brief disk-write spikes
export const SAMPLE_DIR_NAMES = new Set(["sample", "samples"]); export const SAMPLE_DIR_NAMES = new Set(["sample", "samples"]);
export const SAMPLE_VIDEO_EXTENSIONS = new Set([".mkv", ".mp4", ".avi", ".mov", ".wmv", ".m4v", ".ts", ".m2ts", ".webm"]); export const SAMPLE_VIDEO_EXTENSIONS = new Set([".mkv", ".mp4", ".avi", ".mov", ".wmv", ".m4v", ".ts", ".m2ts", ".webm"]);
@ -46,6 +46,7 @@ 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,
@ -93,32 +94,36 @@ export function defaultSettings(): AppSettings {
speedLimitMode: "global", speedLimitMode: "global",
updateRepo: DEFAULT_UPDATE_REPO, updateRepo: DEFAULT_UPDATE_REPO,
autoUpdateCheck: true, autoUpdateCheck: true,
clipboardWatch: false, clipboardWatch: false,
minimizeToTray: false, minimizeToTray: false,
theme: "dark" as const, theme: "dark" as const,
collapseNewPackages: true, collapseNewPackages: true,
historyRetentionMode: "permanent", historyRetentionMode: "permanent",
accountListShowDetailedDebridLinkKeys: false, accountListShowDetailedDebridLinkKeys: false,
autoSortPackagesByProgress: true, autoSortPackagesByProgress: true,
autoSkipExtracted: false, autoSkipExtracted: false,
hideExtractedItems: true, hideExtractedItems: true,
confirmDeleteSelection: true, confirmDeleteSelection: true,
totalDownloadedAllTime: 0, totalDownloadedAllTime: 0,
totalCompletedFilesAllTime: 0, totalCompletedFilesAllTime: 0,
totalRuntimeAllTimeMs: 0, totalRuntimeAllTimeMs: 0,
bandwidthSchedules: [], bandwidthSchedules: [],
columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"], columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"],
extractCpuPriority: "high", extractCpuPriority: "high",
autoExtractWhenStopped: true, autoExtractWhenStopped: true,
disabledProviders: [], disabledProviders: [],
hosterRouting: {}, hosterRouting: {},
providerDailyLimitBytes: {}, providerDailyLimitBytes: {},
providerDailyUsageBytes: {}, providerDailyUsageBytes: {},
providerTotalUsageBytes: {}, providerTotalUsageBytes: {},
debridLinkApiKeyDailyLimitBytes: {}, debridLinkApiKeyDailyLimitBytes: {},
debridLinkApiKeyDailyUsageBytes: {}, debridLinkApiKeyDailyUsageBytes: {},
debridLinkApiKeyTotalUsageBytes: {}, debridLinkApiKeyTotalUsageBytes: {},
providerDailyUsageDay: getProviderUsageDayKey(), megaDebridDisabledAccountIds: [],
scheduledStartEpochMs: 0 megaDebridAccountDailyLimitBytes: {},
}; megaDebridAccountDailyUsageBytes: {},
} megaDebridAccountTotalUsageBytes: {},
providerDailyUsageDay: getProviderUsageDayKey(),
scheduledStartEpochMs: 0
};
}

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@ 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";
@ -281,6 +282,12 @@ 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
@ -322,6 +329,7 @@ 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,
@ -406,6 +414,12 @@ 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)
}; };
@ -454,6 +468,7 @@ 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,5 +1,6 @@
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,
@ -232,8 +233,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 nur über die API, ohne Web-Fallback.", pickerDescription: "Login:Passwort-Paare für Mega-Debrid (API). Mehrere Accounts zeilenweise für Multi-Account.",
needsCredentials: true needsToken: true
}, },
{ {
kind: "megadebrid-web", kind: "megadebrid-web",
@ -241,8 +242,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 nur über Web, ohne API-Fallback.", pickerDescription: "Login:Passwort-Paare für Mega-Debrid (Web). Mehrere Accounts zeilenweise für Multi-Account.",
needsCredentials: true needsToken: true
}, },
{ {
kind: "bestdebrid-api", kind: "bestdebrid-api",
@ -404,9 +405,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":
@ -420,6 +421,7 @@ 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());
} }
@ -527,8 +529,12 @@ 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": {
return settings.megaLogin.trim() ? maskValue(settings.megaLogin.trim(), 2, 6) : "Login + Passwort"; const megaAccounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword);
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":
@ -560,6 +566,12 @@ 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)];
} }
@ -583,8 +595,14 @@ 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": {
return { mode, kind, token: "", login: settings.megaLogin, password: settings.megaPassword, dailyLimitGb, keyDailyLimitGbById: {} }; // Populate token field with megaCredentials, or build from legacy megaLogin/megaPassword
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":
@ -642,10 +660,18 @@ 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": {
return { ...settings, megaLogin: login, megaPassword: password, megaDebridApiEnabled: true, megaDebridPreferApi: true, providerDailyLimitBytes: nextProviderDailyLimitBytes }; const megaAccounts = parseMegaDebridAccounts(token);
case "megadebrid-web": const firstLogin = megaAccounts.length > 0 ? megaAccounts[0].login : "";
return { ...settings, megaLogin: login, megaPassword: password, megaDebridWebEnabled: true, megaDebridPreferApi: false, providerDailyLimitBytes: nextProviderDailyLimitBytes }; const firstPassword = megaAccounts.length > 0 ? megaAccounts[0].password : "";
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":
@ -692,11 +718,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, megaLogin: "", megaPassword: "", megaDebridApiEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes }; : { ...settings, megaCredentials: "", 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, megaLogin: "", megaPassword: "", megaDebridWebEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes }; : { ...settings, megaCredentials: "", megaLogin: "", megaPassword: "", megaDebridWebEnabled: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes };
case "bestdebrid": case "bestdebrid":
return { ...settings, bestToken: "", bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes }; return { ...settings, bestToken: "", bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes, providerDailyUsageBytes: nextProviderDailyUsageBytes };
case "alldebrid": case "alldebrid":
@ -729,6 +755,11 @@ 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.`;
@ -792,7 +823,7 @@ type StatsSection = {
const emptySnapshot = (): UiSnapshot => ({ const emptySnapshot = (): UiSnapshot => ({
settings: { settings: {
token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaDebridApiEnabled: false, megaDebridWebEnabled: false, megaDebridPreferApi: true, bestToken: "", bestDebridUseWebLogin: false, allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "", debridLinkApiKeys: "", linkSnappyLogin: "", linkSnappyPassword: "", token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaCredentials: "", 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",
@ -817,6 +848,10 @@ const emptySnapshot = (): UiSnapshot => ({
debridLinkApiKeyDailyLimitBytes: {}, debridLinkApiKeyDailyLimitBytes: {},
debridLinkApiKeyDailyUsageBytes: {}, debridLinkApiKeyDailyUsageBytes: {},
debridLinkApiKeyTotalUsageBytes: {}, debridLinkApiKeyTotalUsageBytes: {},
megaDebridDisabledAccountIds: [],
megaDebridAccountDailyLimitBytes: {},
megaDebridAccountDailyUsageBytes: {},
megaDebridAccountTotalUsageBytes: {},
providerDailyUsageDay: getProviderUsageDayKey(), providerDailyUsageDay: getProviderUsageDayKey(),
scheduledStartEpochMs: 0 scheduledStartEpochMs: 0
}, },
@ -2082,8 +2117,12 @@ 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.";
@ -5006,10 +5045,8 @@ 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 Login</label> <label>Mega-Debrid Accounts (Login:Passwort pro Zeile)</label>
<input value={settingsDraft.megaLogin} onChange={(e) => setText("megaLogin", e.target.value)} /> <textarea rows={3} value={settingsDraft.megaCredentials || ""} onChange={(e) => setText("megaCredentials", e.target.value)} style={{ fontFamily: "monospace", resize: "vertical" }} placeholder={"user@example.com:passwort"} />
<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)} />
@ -5375,7 +5412,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)" : "Token"}</label> <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>
{accountDialogOption.service === "debridlink" ? ( {accountDialogOption.service === "debridlink" ? (
<textarea <textarea
rows={4} rows={4}
@ -5388,6 +5425,14 @@ 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)} />
)} )}
@ -5457,10 +5502,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">Dieser Account nutzt nur die Mega-Debrid API. Kein Web-Fallback.</div> <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>
)} )}
{accountDialog.kind === "megadebrid-web" && ( {accountDialog.kind === "megadebrid-web" && (
<div className="account-modal-note">Dieser Account nutzt nur Mega-Debrid Web. Kein API-Fallback.</div> <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>
)} )}
{accountDialogOption.service === "alldebrid" && allDebridHostInfo && ( {accountDialogOption.service === "alldebrid" && allDebridHostInfo && (

View File

@ -0,0 +1,96 @@
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,11 +5,13 @@ 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);
@ -247,3 +249,83 @@ 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

@ -27,9 +27,9 @@ export type DebridProvider =
| "linksnappy"; | "linksnappy";
export type DebridFallbackProvider = DebridProvider | "none"; export type DebridFallbackProvider = DebridProvider | "none";
export type AppTheme = "dark" | "light"; export type AppTheme = "dark" | "light";
export type PackagePriority = "high" | "normal" | "low"; export type PackagePriority = "high" | "normal" | "low";
export type ExtractCpuPriority = "high" | "middle" | "low"; export type ExtractCpuPriority = "high" | "middle" | "low";
export type HistoryRetentionMode = "never" | "session" | "permanent"; export type HistoryRetentionMode = "never" | "session" | "permanent";
export interface BandwidthScheduleEntry { export interface BandwidthScheduleEntry {
id: string; id: string;
@ -39,25 +39,26 @@ export interface BandwidthScheduleEntry {
enabled: boolean; enabled: boolean;
} }
export interface DownloadStats { export interface DownloadStats {
totalDownloaded: number; totalDownloaded: number;
totalDownloadedAllTime: number; totalDownloadedAllTime: number;
totalFiles?: number; totalFiles?: number;
totalFilesSession: number; totalFilesSession: number;
totalFilesAllTime: number; totalFilesAllTime: number;
totalPackages: number; totalPackages: number;
sessionStartedAt: number; sessionStartedAt: number;
appSessionStartedAt: number; appSessionStartedAt: number;
sessionRuntimeMs: number; sessionRuntimeMs: number;
totalRuntimeMs: number; totalRuntimeMs: number;
runtimeMeasuredAt: number; runtimeMeasuredAt: number;
} }
export interface AppSettings { export interface AppSettings {
token: string; token: string;
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;
@ -74,7 +75,7 @@ export interface AppSettings {
linkSnappyPassword: string; linkSnappyPassword: string;
archivePasswordList: string; archivePasswordList: string;
rememberToken: boolean; rememberToken: boolean;
providerOrder: readonly DebridProvider[]; providerOrder: readonly DebridProvider[];
providerPrimary: DebridProvider; providerPrimary: DebridProvider;
providerSecondary: DebridFallbackProvider; providerSecondary: DebridFallbackProvider;
providerTertiary: DebridFallbackProvider; providerTertiary: DebridFallbackProvider;
@ -107,32 +108,36 @@ export interface AppSettings {
autoUpdateCheck: boolean; autoUpdateCheck: boolean;
clipboardWatch: boolean; clipboardWatch: boolean;
minimizeToTray: boolean; minimizeToTray: boolean;
theme: AppTheme; theme: AppTheme;
collapseNewPackages: boolean; collapseNewPackages: boolean;
historyRetentionMode: HistoryRetentionMode; historyRetentionMode: HistoryRetentionMode;
accountListShowDetailedDebridLinkKeys: boolean; accountListShowDetailedDebridLinkKeys: boolean;
autoSortPackagesByProgress: boolean; autoSortPackagesByProgress: boolean;
autoSkipExtracted: boolean; autoSkipExtracted: boolean;
hideExtractedItems: boolean; hideExtractedItems: boolean;
confirmDeleteSelection: boolean; confirmDeleteSelection: boolean;
totalDownloadedAllTime: number; totalDownloadedAllTime: number;
totalCompletedFilesAllTime: number; totalCompletedFilesAllTime: number;
totalRuntimeAllTimeMs: number; totalRuntimeAllTimeMs: number;
bandwidthSchedules: BandwidthScheduleEntry[]; bandwidthSchedules: BandwidthScheduleEntry[];
columnOrder: string[]; columnOrder: string[];
extractCpuPriority: ExtractCpuPriority; extractCpuPriority: ExtractCpuPriority;
autoExtractWhenStopped: boolean; autoExtractWhenStopped: boolean;
disabledProviders: DebridProvider[]; disabledProviders: DebridProvider[];
hosterRouting: Record<string, DebridProvider>; hosterRouting: Record<string, DebridProvider>;
providerDailyLimitBytes: Partial<Record<DebridProvider, number>>; providerDailyLimitBytes: Partial<Record<DebridProvider, number>>;
providerDailyUsageBytes: Partial<Record<DebridProvider, number>>; providerDailyUsageBytes: Partial<Record<DebridProvider, number>>;
providerTotalUsageBytes: Partial<Record<DebridProvider, number>>; providerTotalUsageBytes: Partial<Record<DebridProvider, number>>;
debridLinkApiKeyDailyLimitBytes: Record<string, number>; debridLinkApiKeyDailyLimitBytes: Record<string, number>;
debridLinkApiKeyDailyUsageBytes: Record<string, number>; debridLinkApiKeyDailyUsageBytes: Record<string, number>;
debridLinkApiKeyTotalUsageBytes: Record<string, number>; debridLinkApiKeyTotalUsageBytes: Record<string, number>;
providerDailyUsageDay: string; megaDebridDisabledAccountIds: string[];
scheduledStartEpochMs: number; megaDebridAccountDailyLimitBytes: Record<string, number>;
} megaDebridAccountDailyUsageBytes: Record<string, number>;
megaDebridAccountTotalUsageBytes: Record<string, number>;
providerDailyUsageDay: string;
scheduledStartEpochMs: number;
}
export interface DownloadItem { export interface DownloadItem {
id: string; id: string;
@ -167,14 +172,14 @@ export interface PackageEntry {
status: DownloadStatus; status: DownloadStatus;
itemIds: string[]; itemIds: string[];
cancelled: boolean; cancelled: boolean;
enabled: boolean; enabled: boolean;
priority?: PackagePriority; priority?: PackagePriority;
postProcessLabel?: string; postProcessLabel?: string;
downloadStartedAt?: number; downloadStartedAt?: number;
downloadCompletedAt?: number; downloadCompletedAt?: number;
createdAt: number; createdAt: number;
updatedAt: number; updatedAt: number;
} }
export interface SessionState { export interface SessionState {
version: number; version: number;
@ -338,104 +343,104 @@ export interface BandwidthStats {
sessionDurationSeconds: number; sessionDurationSeconds: number;
} }
export interface SessionStats { export interface SessionStats {
bandwidth: BandwidthStats; bandwidth: BandwidthStats;
totalDownloads: number; totalDownloads: number;
completedDownloads: number; completedDownloads: number;
failedDownloads: number; failedDownloads: number;
activeDownloads: number; activeDownloads: number;
queuedDownloads: number; queuedDownloads: number;
} }
export interface SupportTraceConfig { export interface SupportTraceConfig {
enabled: boolean; enabled: boolean;
includeMainLog: boolean; includeMainLog: boolean;
includeAudit: boolean; includeAudit: boolean;
logDebugRequests: boolean; logDebugRequests: boolean;
autoDisableAt: string | null; autoDisableAt: string | null;
updatedAt: string; updatedAt: string;
} }
export interface SupportFileSizeInfo { export interface SupportFileSizeInfo {
path: string | null; path: string | null;
exists: boolean; exists: boolean;
bytes: number; bytes: number;
} }
export interface SupportDirectorySizeInfo { export interface SupportDirectorySizeInfo {
path: string; path: string;
exists: boolean; exists: boolean;
fileCount: number; fileCount: number;
bytes: number; bytes: number;
} }
export interface SupportDiskSpaceInfo { export interface SupportDiskSpaceInfo {
path: string; path: string;
totalBytes: number | null; totalBytes: number | null;
freeBytes: number | null; freeBytes: number | null;
freePercent: number | null; freePercent: number | null;
} }
export interface SupportBundleEstimate { export interface SupportBundleEstimate {
estimatedBytes: number; estimatedBytes: number;
estimatedEntries: number; estimatedEntries: number;
duplicatedLiveLogBytes: number; duplicatedLiveLogBytes: number;
note: string; note: string;
} }
export interface DebugSetupCheckResult { export interface DebugSetupCheckResult {
status: "ok" | "warn"; status: "ok" | "warn";
enabled: boolean; enabled: boolean;
runtimeBaseDir: string; runtimeBaseDir: string;
host: string; host: string;
port: number; port: number;
localOnly: boolean; localOnly: boolean;
tokenConfigured: boolean; tokenConfigured: boolean;
tokenPath: string; tokenPath: string;
aiManifestPath: string; aiManifestPath: string;
aiManifestPresent: boolean; aiManifestPresent: boolean;
traceConfigPath: string | null; traceConfigPath: string | null;
traceLogPath: string | null; traceLogPath: string | null;
traceEnabled: boolean; traceEnabled: boolean;
traceAutoDisableAt: string | null; traceAutoDisableAt: string | null;
diskSpace: { diskSpace: {
runtime: SupportDiskSpaceInfo; runtime: SupportDiskSpaceInfo;
output: SupportDiskSpaceInfo; output: SupportDiskSpaceInfo;
extract: SupportDiskSpaceInfo; extract: SupportDiskSpaceInfo;
}; };
logSummary: { logSummary: {
totalBytes: number; totalBytes: number;
main: SupportFileSizeInfo; main: SupportFileSizeInfo;
mainBackup: SupportFileSizeInfo; mainBackup: SupportFileSizeInfo;
audit: SupportFileSizeInfo; audit: SupportFileSizeInfo;
auditBackup: SupportFileSizeInfo; auditBackup: SupportFileSizeInfo;
rename: SupportFileSizeInfo; rename: SupportFileSizeInfo;
renameBackup: SupportFileSizeInfo; renameBackup: SupportFileSizeInfo;
session: SupportFileSizeInfo; session: SupportFileSizeInfo;
trace: SupportFileSizeInfo; trace: SupportFileSizeInfo;
traceBackup: SupportFileSizeInfo; traceBackup: SupportFileSizeInfo;
sessionLogs: SupportDirectorySizeInfo; sessionLogs: SupportDirectorySizeInfo;
packageLogs: SupportDirectorySizeInfo; packageLogs: SupportDirectorySizeInfo;
itemLogs: SupportDirectorySizeInfo; itemLogs: SupportDirectorySizeInfo;
}; };
supportBundle: SupportBundleEstimate; supportBundle: SupportBundleEstimate;
warnings: string[]; warnings: string[];
notes: string[]; notes: string[];
localUrls: { localUrls: {
health: string; health: string;
meta: string; meta: string;
diagnostics: string; diagnostics: string;
}; };
remoteUrlTemplates: { remoteUrlTemplates: {
health: string; health: string;
meta: string; meta: string;
diagnostics: string; diagnostics: string;
}; };
} }
export interface HistoryEntry { export interface HistoryEntry {
id: string; id: string;
name: string; name: string;
totalBytes: number; totalBytes: number;
downloadedBytes: number; downloadedBytes: number;
fileCount: number; fileCount: number;

File diff suppressed because it is too large Load Diff

View File

@ -6167,6 +6167,7 @@ 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,
@ -9391,6 +9392,7 @@ 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 },