Add daily traffic limits, auto-sort packages, Debrid-Link multi-key improvements

Daily traffic limits:
- Per-provider daily download limit (configurable in GB per provider)
- Per Debrid-Link API key daily limit (individual limits per key)
- Usage tracking with automatic daily reset at midnight
- Provider is skipped when daily limit reached, falls back to next provider
- Reset button per provider and per Debrid-Link key in account settings
- Hoster routing skips daily-limited providers gracefully

Debrid-Link multi-key improvements:
- Keys now display with labels (#1, #2...) and masked tokens in account list
- Option to show detailed per-key view with individual usage stats
- Keys that hit their daily limit are automatically skipped
- providerAccountId/providerAccountLabel stored per download item

Auto-sort packages by progress:
- Active packages automatically sorted to top during downloads
- Sorted by completion ratio, then downloaded bytes
- Toggle in settings (autoSortPackagesByProgress)

UI polish:
- Package column headers: flatter, more transparent design
- LinkSnappy mode label: "Login" renamed to "Web"
- Account list: new toggle for detailed Debrid-Link key display
- Account usage stats section with warning styling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe
2026-03-07 02:29:48 +01:00
co-authored by Claude Opus 4.6
parent 71b3612e82
commit e212ccc86f
20 changed files with 1149 additions and 54 deletions
+32
View File
@@ -1,9 +1,11 @@
import path from "node:path";
import { app } from "electron";
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
import {
AddLinksPayload,
AllDebridHostInfo,
AppSettings,
DebridProvider,
DuplicatePolicy,
HistoryEntry,
PackagePriority,
@@ -16,6 +18,7 @@ import {
UpdateInstallProgress,
UpdateInstallResult
} from "../shared/types";
import { resetDebridLinkApiKeyDailyUsage, resetProviderDailyUsage } from "../shared/provider-daily-limits";
import { importDlcContainers } from "./container";
import { APP_VERSION } from "./constants";
import { DownloadManager } from "./download-manager";
@@ -176,6 +179,11 @@ export class AppController {
// Preserve the live totalDownloadedAllTime from the download manager
const liveSettings = this.manager.getSettings();
nextSettings.totalDownloadedAllTime = Math.max(nextSettings.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0);
nextSettings.providerDailyUsageDay = liveSettings.providerDailyUsageDay;
nextSettings.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) };
nextSettings.debridLinkApiKeyDailyUsageBytes = Object.fromEntries(
Object.entries(liveSettings.debridLinkApiKeyDailyUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId))
);
this.settings = nextSettings;
saveSettings(this.storagePaths, this.settings);
this.manager.setSettings(this.settings);
@@ -193,6 +201,30 @@ export class AppController {
return this.settings;
}
public resetProviderDailyUsage(provider: DebridProvider): AppSettings {
const liveSettings = this.manager.getSettings();
const nextSettings = normalizeSettings({
...liveSettings,
...resetProviderDailyUsage(liveSettings, provider)
});
this.settings = nextSettings;
saveSettings(this.storagePaths, this.settings);
this.manager.setSettings(this.settings);
return this.settings;
}
public resetDebridLinkApiKeyDailyUsage(keyId: string): AppSettings {
const liveSettings = this.manager.getSettings();
const nextSettings = normalizeSettings({
...liveSettings,
...resetDebridLinkApiKeyDailyUsage(liveSettings, keyId)
});
this.settings = nextSettings;
saveSettings(this.storagePaths, this.settings);
this.manager.setSettings(this.settings);
return this.settings;
}
public async openRealDebridLoginWindow(): Promise<void> {
await this.realDebridWebFallback.openLoginWindow();
}
+8
View File
@@ -1,6 +1,7 @@
import path from "node:path";
import os from "node:os";
import { AppSettings } from "../shared/types";
import { getProviderUsageDayKey } from "../shared/provider-daily-limits";
import packageJson from "../../package.json";
export const APP_NAME = "Multi Debrid Downloader";
@@ -94,6 +95,8 @@ export function defaultSettings(): AppSettings {
minimizeToTray: false,
theme: "dark" as const,
collapseNewPackages: true,
accountListShowDetailedDebridLinkKeys: false,
autoSortPackagesByProgress: true,
autoSkipExtracted: false,
confirmDeleteSelection: true,
totalDownloadedAllTime: 0,
@@ -103,6 +106,11 @@ export function defaultSettings(): AppSettings {
autoExtractWhenStopped: true,
disabledProviders: [],
hosterRouting: {},
providerDailyLimitBytes: {},
providerDailyUsageBytes: {},
debridLinkApiKeyDailyLimitBytes: {},
debridLinkApiKeyDailyUsageBytes: {},
providerDailyUsageDay: getProviderUsageDayKey(),
scheduledStartEpochMs: 0
};
}
+79 -24
View File
@@ -1,4 +1,6 @@
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
import { AllDebridHostInfo, AppSettings, DebridFallbackProvider, DebridProvider } from "../shared/types";
import { isDebridLinkApiKeyDailyLimitReached, isProviderDailyLimitReached } from "../shared/provider-daily-limits";
import { APP_VERSION, REQUEST_RETRIES } from "./constants";
import { logger } from "./logger";
import { RealDebridClient, UnrestrictedLink } from "./realdebrid";
@@ -65,10 +67,20 @@ interface DebridServiceOptions {
function cloneSettings(settings: AppSettings): AppSettings {
return {
...settings,
bandwidthSchedules: (settings.bandwidthSchedules || []).map((entry) => ({ ...entry }))
bandwidthSchedules: (settings.bandwidthSchedules || []).map((entry) => ({ ...entry })),
providerDailyLimitBytes: { ...(settings.providerDailyLimitBytes || {}) },
providerDailyUsageBytes: { ...(settings.providerDailyUsageBytes || {}) },
debridLinkApiKeyDailyLimitBytes: { ...(settings.debridLinkApiKeyDailyLimitBytes || {}) },
debridLinkApiKeyDailyUsageBytes: { ...(settings.debridLinkApiKeyDailyUsageBytes || {}) }
};
}
function getAvailableDebridLinkApiKeys(settings: AppSettings, epochMs = Date.now()) {
return parseDebridLinkApiKeys(settings.debridLinkApiKeys).filter(
(entry) => !isDebridLinkApiKeyDailyLimitReached(settings, entry.id, epochMs)
);
}
function hasMegaDebridCredentials(settings: AppSettings): boolean {
return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim());
}
@@ -1305,27 +1317,31 @@ export async function fetchAllDebridHostInfo(token: string, host = "rapidgator",
// ── Debrid-Link Client ──
class DebridLinkClient {
private apiKeys: string[];
private apiKeys: ReturnType<typeof parseDebridLinkApiKeys>;
private currentKeyIndex: number = 0;
public constructor(apiKeysRaw: string) {
this.apiKeys = apiKeysRaw
.split(/[\n,]+/)
.map((k) => k.trim())
.filter(Boolean);
this.apiKeys = parseDebridLinkApiKeys(apiKeysRaw);
}
public async unrestrictLink(link: string, signal?: AbortSignal): Promise<UnrestrictedLink> {
public async unrestrictLink(link: string, settings: AppSettings, signal?: AbortSignal): Promise<UnrestrictedLink> {
if (this.apiKeys.length === 0) {
throw new Error("Debrid-Link: Kein API-Key konfiguriert");
}
const startIndex = this.currentKeyIndex;
let triedAll = false;
if (getAvailableDebridLinkApiKeys(settings).length === 0) {
throw new Error(`Debrid-Link: Alle ${this.apiKeys.length} API-Keys haben ihr Tageslimit erreicht`);
}
while (!triedAll) {
let checkedKeys = 0;
while (checkedKeys < this.apiKeys.length) {
const apiKey = this.apiKeys[this.currentKeyIndex];
const keyLabel = this.apiKeys.length > 1 ? ` #${this.currentKeyIndex + 1}` : "";
checkedKeys += 1;
const keyLabel = this.apiKeys.length > 1 ? ` (${apiKey.label})` : "";
if (isDebridLinkApiKeyDailyLimitReached(settings, apiKey.id)) {
this.currentKeyIndex = (this.currentKeyIndex + 1) % this.apiKeys.length;
continue;
}
let lastError = "";
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
@@ -1335,7 +1351,7 @@ class DebridLinkClient {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Bearer ${apiKey}`
Authorization: `Bearer ${apiKey.token}`
},
body: `url=${encodeURIComponent(link)}`,
signal: withTimeoutSignal(signal, API_TIMEOUT_MS)
@@ -1383,7 +1399,9 @@ class DebridLinkClient {
directUrl,
fileSize,
retriesUsed: attempt - 1,
sourceLabel: keyLabel ? `#${this.currentKeyIndex + 1}` : "API"
sourceLabel: apiKey.label,
sourceAccountId: apiKey.id,
sourceAccountLabel: apiKey.label
};
} catch (error) {
lastError = compactErrorText(error);
@@ -1400,9 +1418,6 @@ class DebridLinkClient {
}
this.currentKeyIndex = (this.currentKeyIndex + 1) % this.apiKeys.length;
if (this.currentKeyIndex === startIndex) {
triedAll = true;
}
}
throw new Error(`Debrid-Link: Alle ${this.apiKeys.length} API-Keys haben ihr Limit erreicht`);
@@ -1915,6 +1930,29 @@ export class DebridService {
return Boolean(settings.bestDebridUseWebLogin && this.options.bestDebridWebUnrestrict);
}
private isProviderDailyLimited(settings: AppSettings, provider: DebridProvider): boolean {
const effectiveProvider = resolveMegaDebridProvider(settings, provider);
if (effectiveProvider === "debridlink") {
const configuredKeys = parseDebridLinkApiKeys(settings.debridLinkApiKeys);
if (configuredKeys.length > 0 && getAvailableDebridLinkApiKeys(settings).length === 0) {
return true;
}
}
return isProviderDailyLimitReached(settings, effectiveProvider);
}
private isProviderSelectableFor(settings: AppSettings, provider: DebridProvider): boolean {
return this.isProviderConfiguredFor(settings, provider) && !this.isProviderDailyLimited(settings, provider);
}
private formatProviderLimitMessage(settings: AppSettings, provider: DebridProvider): string {
const effectiveProvider = resolveMegaDebridProvider(settings, provider);
if (effectiveProvider === "debridlink" && parseDebridLinkApiKeys(settings.debridLinkApiKeys).length > 0 && getAvailableDebridLinkApiKeys(settings).length === 0) {
return "Debrid-Link Tageslimit erreicht (alle API-Keys ausgeschopft)";
}
return `${PROVIDER_LABELS[effectiveProvider]} Tageslimit erreicht`;
}
public async unrestrictLink(link: string, signal?: AbortSignal, settingsSnapshot?: AppSettings): Promise<ProviderUnrestrictedLink> {
const settings = settingsSnapshot ? cloneSettings(settingsSnapshot) : cloneSettings(this.settings);
@@ -1923,7 +1961,7 @@ export class DebridService {
const hosterKey = extractHosterFromUrl(link);
if (hosterKey && routing[hosterKey]) {
const routedProvider = routing[hosterKey];
if (this.isProviderConfiguredFor(settings, routedProvider)) {
if (this.isProviderSelectableFor(settings, routedProvider)) {
logger.info(`Hoster-Zuordnung: ${hosterKey}${PROVIDER_LABELS[routedProvider]}`);
try {
const result = await this.unrestrictViaProvider(settings, routedProvider, link, signal);
@@ -1949,6 +1987,8 @@ export class DebridService {
logger.warn(`Hoster-Zuordnung ${hosterKey}${PROVIDER_LABELS[routedProvider]} fehlgeschlagen, Fallback auf Provider-Kette: ${errorText}`);
// Fall through to normal provider chain
}
} else if (this.isProviderConfiguredFor(settings, routedProvider) && this.isProviderDailyLimited(settings, routedProvider)) {
logger.info(`Hoster-Zuordnung ${hosterKey} ? ${PROVIDER_LABELS[routedProvider]} ?bersprungen (${this.formatProviderLimitMessage(settings, routedProvider)})`);
} else {
logger.warn(`Hoster-Zuordnung ${hosterKey}${PROVIDER_LABELS[routedProvider]} übersprungen (Provider nicht konfiguriert/deaktiviert)`);
}
@@ -1956,7 +1996,7 @@ export class DebridService {
// 1Fichier is a direct file hoster. If the link is a 1fichier.com URL
// and the API key is configured, use 1Fichier directly before debrid providers.
if (ONEFICHIER_URL_RE.test(link) && this.isProviderConfiguredFor(settings, "onefichier")) {
if (ONEFICHIER_URL_RE.test(link) && this.isProviderSelectableFor(settings, "onefichier")) {
try {
const result = await this.unrestrictViaProvider(settings, "onefichier", link, signal);
return {
@@ -1976,7 +2016,7 @@ export class DebridService {
// DDownload is a direct file hoster, not a debrid service.
// If the link is a ddownload.com/ddl.to URL and the account is configured,
// use DDownload directly before trying any debrid providers.
if (DDOWNLOAD_URL_RE.test(link) && this.isProviderConfiguredFor(settings, "ddownload")) {
if (DDOWNLOAD_URL_RE.test(link) && this.isProviderSelectableFor(settings, "ddownload")) {
try {
const result = await this.unrestrictViaProvider(settings, "ddownload", link, signal);
return {
@@ -2003,8 +2043,14 @@ export class DebridService {
if (!this.isProviderConfiguredFor(settings, primary)) {
throw new Error(`${PROVIDER_LABELS[primary]} nicht konfiguriert`);
}
const selectedProvider = this.isProviderDailyLimited(settings, primary)
? order.find((provider) => provider !== primary && this.isProviderSelectableFor(settings, provider))
: primary;
if (!selectedProvider) {
throw new Error(this.formatProviderLimitMessage(settings, primary));
}
try {
const result = await this.unrestrictViaProvider(settings, primary, link, signal);
const result = await this.unrestrictViaProvider(settings, selectedProvider, link, signal);
let fileName = result.fileName;
if (isRapidgatorLink(link) && looksLikeOpaqueFilename(fileName || filenameFromUrl(link))) {
const fromPage = await resolveRapidgatorFilename(link, signal);
@@ -2015,19 +2061,20 @@ export class DebridService {
return {
...result,
fileName,
provider: primary,
providerLabel: PROVIDER_LABELS[primary] + (result.sourceLabel ? ` (${result.sourceLabel})` : "")
provider: selectedProvider,
providerLabel: PROVIDER_LABELS[selectedProvider] + (result.sourceLabel ? ` (${result.sourceLabel})` : "")
};
} catch (error) {
const errorText = compactErrorText(error);
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
throw error;
}
throw new Error(`Unrestrict fehlgeschlagen: ${PROVIDER_LABELS[primary]}: ${errorText}`);
throw new Error(`Unrestrict fehlgeschlagen: ${PROVIDER_LABELS[selectedProvider]}: ${errorText}`);
}
}
let configuredFound = false;
let limitReachedFound = false;
const attempts: string[] = [];
for (const provider of order) {
@@ -2035,6 +2082,11 @@ export class DebridService {
continue;
}
configuredFound = true;
if (this.isProviderDailyLimited(settings, provider)) {
limitReachedFound = true;
attempts.push(this.formatProviderLimitMessage(settings, provider));
continue;
}
try {
const result = await this.unrestrictViaProvider(settings, provider, link, signal);
@@ -2063,6 +2115,9 @@ export class DebridService {
if (!configuredFound) {
throw new Error("Kein Debrid-Provider konfiguriert");
}
if (limitReachedFound && attempts.every((entry) => /Tageslimit erreicht$/i.test(entry))) {
throw new Error("Alle konfigurierten Provider haben ihr Tageslimit erreicht");
}
throw new Error(`Unrestrict fehlgeschlagen: ${attempts.join(" | ")}`);
}
@@ -2138,7 +2193,7 @@ export class DebridService {
return new OneFichierClient(settings.oneFichierApiKey).unrestrictLink(link, signal);
}
if (effectiveProvider === "debridlink") {
const dlResult = await this.getDebridLinkClient(settings.debridLinkApiKeys).unrestrictLink(link, signal);
const dlResult = await this.getDebridLinkClient(settings.debridLinkApiKeys).unrestrictLink(link, settings, signal);
dlResult.sourceLabel = dlResult.sourceLabel || "API";
return dlResult;
}
+57 -7
View File
@@ -20,6 +20,8 @@ import {
StartConflictResolutionResult,
UiSnapshot
} from "../shared/types";
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
import { addDebridLinkApiKeyDailyUsageBytes, addProviderDailyUsageBytes, getProviderUsageDayKey, isDebridLinkApiKeyDailyLimitReached, isProviderDailyLimitReached } from "../shared/provider-daily-limits";
import { REQUEST_RETRIES, SAMPLE_VIDEO_EXTENSIONS, SPEED_WINDOW_SECONDS, WRITE_BUFFER_SIZE, WRITE_FLUSH_TIMEOUT_MS, ALLOCATION_UNIT_SIZE, STREAM_HIGH_WATER_MARK, DISK_BUSY_THRESHOLD_MS } from "./constants";
// Reference counter for NODE_TLS_REJECT_UNAUTHORIZED to avoid race conditions
@@ -77,7 +79,7 @@ const DEFAULT_LOW_THROUGHPUT_TIMEOUT_MS = 120000;
const DEFAULT_LOW_THROUGHPUT_MIN_BYTES = 64 * 1024;
const MINI_DOWNLOAD_RETRY_THRESHOLD_BYTES = 1024 * 1024;
const MINI_DOWNLOAD_RETRY_THRESHOLD_BYTES = 100 * 1024;
const ALLDEBRID_HOST_INFO_TTL_MS = 60000;
@@ -198,7 +200,11 @@ function cloneSession(session: SessionState): SessionState {
function cloneSettings(settings: AppSettings): AppSettings {
return {
...settings,
bandwidthSchedules: (settings.bandwidthSchedules || []).map((entry) => ({ ...entry }))
bandwidthSchedules: (settings.bandwidthSchedules || []).map((entry) => ({ ...entry })),
providerDailyLimitBytes: { ...(settings.providerDailyLimitBytes || {}) },
providerDailyUsageBytes: { ...(settings.providerDailyUsageBytes || {}) },
debridLinkApiKeyDailyLimitBytes: { ...(settings.debridLinkApiKeyDailyLimitBytes || {}) },
debridLinkApiKeyDailyUsageBytes: { ...(settings.debridLinkApiKeyDailyUsageBytes || {}) }
};
}
@@ -1069,6 +1075,7 @@ export class DownloadManager extends EventEmitter {
const previous = this.settings;
next.totalDownloadedAllTime = Math.max(next.totalDownloadedAllTime || 0, this.settings.totalDownloadedAllTime || 0);
this.settings = next;
this.ensureProviderDailyUsageFresh(nowMs());
this.debridService.setSettings(next);
this.allDebridHostInfoCache.clear();
@@ -1136,6 +1143,7 @@ export class DownloadManager extends EventEmitter {
public getSnapshot(): UiSnapshot {
const now = nowMs();
this.ensureProviderDailyUsageFresh(now, true);
this.pruneSpeedEvents(now);
const paused = this.session.running && this.session.paused;
const speedBps = !this.session.running || paused ? 0 : this.speedBytesLastWindow / SPEED_WINDOW_SECONDS;
@@ -4410,11 +4418,46 @@ export class DownloadManager extends EventEmitter {
return remaining;
}
private ensureProviderDailyUsageFresh(now = nowMs(), persist = false): void {
const currentDay = getProviderUsageDayKey(now);
if (this.settings.providerDailyUsageDay === currentDay) {
return;
}
this.settings.providerDailyUsageDay = currentDay;
this.settings.providerDailyUsageBytes = {};
this.settings.debridLinkApiKeyDailyUsageBytes = {};
this.statsCache = null;
this.statsCacheAt = 0;
if (persist) {
this.lastSettingsPersistAt = now;
void saveSettingsAsync(this.storagePaths, this.settings).catch((err) => logger.warn(`saveSettingsAsync Fehler: ${compactErrorText(err as Error)}`));
}
}
private recordProviderDownloadedBytes(provider: DownloadItem["provider"], byteDelta: number, providerAccountId?: string): void {
if (!provider) {
return;
}
const effectiveProvider = resolveMegaDebridProvider(this.settings, provider) || provider;
const nextUsage = addProviderDailyUsageBytes(this.settings, effectiveProvider, byteDelta);
this.settings.providerDailyUsageDay = nextUsage.providerDailyUsageDay;
this.settings.providerDailyUsageBytes = nextUsage.providerDailyUsageBytes;
if (effectiveProvider === "debridlink" && providerAccountId) {
const nextKeyUsage = addDebridLinkApiKeyDailyUsageBytes(this.settings, providerAccountId, byteDelta);
this.settings.providerDailyUsageDay = nextKeyUsage.providerDailyUsageDay;
this.settings.debridLinkApiKeyDailyUsageBytes = nextKeyUsage.debridLinkApiKeyDailyUsageBytes;
}
}
private isProviderConfigured(provider: DebridProvider): boolean {
this.ensureProviderDailyUsageFresh(nowMs());
const effectiveProvider = resolveMegaDebridProvider(this.settings, provider) || provider;
if ((this.settings.disabledProviders || []).includes(provider) || (this.settings.disabledProviders || []).includes(effectiveProvider)) {
return false;
}
if (isProviderDailyLimitReached(this.settings, effectiveProvider)) {
return false;
}
if (effectiveProvider === "realdebrid") {
return Boolean(this.settings.realDebridUseWebLogin || this.settings.token.trim());
}
@@ -4439,7 +4482,8 @@ export class DownloadManager extends EventEmitter {
return Boolean(this.settings.oneFichierApiKey.trim());
}
if (effectiveProvider === "debridlink") {
return Boolean(this.settings.debridLinkApiKeys.trim());
const configuredKeys = parseDebridLinkApiKeys(this.settings.debridLinkApiKeys);
return configuredKeys.some((entry) => !isDebridLinkApiKeyDailyLimitReached(this.settings, entry.id));
}
if (provider === "linksnappy") {
return Boolean(this.settings.linkSnappyLogin.trim() && this.settings.linkSnappyPassword.trim());
@@ -4471,7 +4515,10 @@ export class DownloadManager extends EventEmitter {
private getExpectedProviderForItem(item: DownloadItem): DebridProvider | null {
if (item.provider) {
return resolveMegaDebridProvider(this.settings, item.provider);
const resolvedProvider = resolveMegaDebridProvider(this.settings, item.provider);
if (resolvedProvider && this.isProviderConfigured(resolvedProvider)) {
return resolvedProvider;
}
}
const hosterKey = extractHosterKey(item.url);
@@ -5232,6 +5279,8 @@ export class DownloadManager extends EventEmitter {
this.recordProviderSuccess(this.getProviderFailureKeyForItem(item, unrestricted.provider));
item.provider = unrestricted.provider;
item.providerLabel = unrestricted.providerLabel;
item.providerAccountId = unrestricted.sourceAccountId;
item.providerAccountLabel = unrestricted.sourceAccountLabel;
item.retries += unrestricted.retriesUsed;
item.fileName = sanitizeFilename(unrestricted.fileName || filenameFromUrl(item.url));
try {
@@ -5341,7 +5390,7 @@ export class DownloadManager extends EventEmitter {
item.totalBytes = (item.totalBytes || 0) > 0 ? item.totalBytes : null;
item.speedBps = 0;
item.updatedAt = nowMs();
throw new Error(`Datei zu klein (${humanSize(fileSizeOnDisk)}, erwartet ${item.totalBytes ? humanSize(item.totalBytes) : ">= 1 MB"})`);
throw new Error(`Datei zu klein (${humanSize(fileSizeOnDisk)}, erwartet ${item.totalBytes ? humanSize(item.totalBytes) : ">= 100 KB"})`);
}
done = true;
@@ -6154,6 +6203,7 @@ export class DownloadManager extends EventEmitter {
this.session.totalDownloadedBytes += buffer.length;
this.sessionDownloadedBytes += buffer.length;
this.settings.totalDownloadedAllTime += buffer.length;
this.recordProviderDownloadedBytes(item.provider, buffer.length, item.providerAccountId);
this.itemContributedBytes.set(active.itemId, (this.itemContributedBytes.get(active.itemId) || 0) + buffer.length);
this.recordSpeed(buffer.length, item.packageId);
throughputWindowBytes += buffer.length;
@@ -6998,7 +7048,7 @@ export class DownloadManager extends EventEmitter {
// Show transitional label while next archive initializes
const done = currentCount;
if (done < progress.total) {
pkg.postProcessLabel = `Entpacken (${done}/${progress.total}) - Naechstes Archiv...`;
pkg.postProcessLabel = `Entpacken (${done}/${progress.total}) - Nächstes Archiv...`;
this.emitState();
}
} else {
@@ -7375,7 +7425,7 @@ export class DownloadManager extends EventEmitter {
// Show transitional label while next archive initializes
const done = currentCount;
if (done < progress.total) {
emitExtractStatus(`Entpacken (${done}/${progress.total}) - Naechstes Archiv...`, true);
emitExtractStatus(`Entpacken (${done}/${progress.total}) - Nächstes Archiv...`, true);
}
} else {
// Update this archive's items with per-archive progress
+26 -1
View File
@@ -1,7 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import { app, BrowserWindow, clipboard, dialog, ipcMain, IpcMainInvokeEvent, Menu, shell, Tray } from "electron";
import { AddLinksPayload, AppSettings, UpdateInstallProgress } from "../shared/types";
import { AddLinksPayload, AppSettings, DebridProvider, UpdateInstallProgress } from "../shared/types";
import { AppController } from "./app-controller";
import { IPC_CHANNELS } from "../shared/ipc";
import { getLogFilePath, logger } from "./logger";
@@ -26,6 +26,17 @@ function validatePlainObject(value: unknown, name: string): Record<string, unkno
const IMPORT_QUEUE_MAX_BYTES = 10 * 1024 * 1024;
const RENAME_PACKAGE_MAX_CHARS = 240;
const RESETTABLE_PROVIDER_KEYS = new Set<DebridProvider>([
"realdebrid",
"megadebrid-api",
"megadebrid-web",
"bestdebrid",
"alldebrid",
"ddownload",
"onefichier",
"debridlink",
"linksnappy"
]);
function validateStringArray(value: unknown, name: string): string[] {
if (!Array.isArray(value) || !value.every(v => typeof v === "string")) {
throw new Error(`${name} muss ein String-Array sein`);
@@ -289,6 +300,20 @@ function registerIpcHandlers(): void {
}
return result;
});
ipcMain.handle(IPC_CHANNELS.RESET_PROVIDER_DAILY_USAGE, (_event: IpcMainInvokeEvent, provider: string) => {
const validatedProvider = validateString(provider, "provider") as DebridProvider;
if (!RESETTABLE_PROVIDER_KEYS.has(validatedProvider)) {
throw new Error("provider ist ungültig");
}
return controller.resetProviderDailyUsage(validatedProvider);
});
ipcMain.handle(IPC_CHANNELS.RESET_DEBRID_LINK_API_KEY_DAILY_USAGE, (_event: IpcMainInvokeEvent, keyId: string) => {
const validatedKeyId = validateString(keyId, "keyId").trim();
if (!validatedKeyId) {
throw new Error("keyId ist ung?ltig");
}
return controller.resetDebridLinkApiKeyDailyUsage(validatedKeyId);
});
ipcMain.handle(IPC_CHANNELS.ADD_LINKS, (_event: IpcMainInvokeEvent, payload: AddLinksPayload) => {
validatePlainObject(payload ?? {}, "payload");
validateString(payload?.rawText, "rawText");
+2
View File
@@ -10,6 +10,8 @@ export interface UnrestrictedLink {
retriesUsed: number;
skipTlsVerify?: boolean;
sourceLabel?: string;
sourceAccountId?: string;
sourceAccountLabel?: string;
}
function shouldRetryStatus(status: number): boolean {
+90 -1
View File
@@ -1,7 +1,9 @@
import fs from "node:fs";
import fsp from "node:fs/promises";
import path from "node:path";
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
import { AppSettings, BandwidthScheduleEntry, DebridFallbackProvider, DebridProvider, DownloadItem, DownloadStatus, HistoryEntry, PackageEntry, PackagePriority, SessionState } from "../shared/types";
import { getProviderUsageDayKey } from "../shared/provider-daily-limits";
import { defaultSettings } from "./constants";
import { logger } from "./logger";
@@ -143,6 +145,57 @@ function normalizeDisabledProviders(raw: unknown): DebridProvider[] {
return result;
}
function normalizeProviderByteMap(
raw: unknown,
megaDebridPreferApi: boolean,
megaDebridApiEnabled: boolean,
megaDebridWebEnabled: boolean,
mergeMode: "max" | "sum"
): Partial<Record<DebridProvider, number>> {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
return {};
}
const result: Partial<Record<DebridProvider, number>> = {};
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
const provider = normalizeConfiguredProvider(key, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled);
if (!provider) {
continue;
}
const bytes = clampNumber(value, 0, 0, Number.MAX_SAFE_INTEGER);
if (bytes <= 0) {
continue;
}
if (mergeMode === "sum") {
result[provider] = (result[provider] || 0) + bytes;
} else {
result[provider] = Math.max(result[provider] || 0, bytes);
}
}
return result;
}
function normalizeNamedByteMap(raw: unknown, allowedKeys: readonly string[]): Record<string, number> {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
return {};
}
const allowed = new Set(allowedKeys);
const result: Record<string, number> = {};
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
const normalizedKey = String(key || "").trim();
if (!normalizedKey || !allowed.has(normalizedKey)) {
continue;
}
const bytes = clampNumber(value, 0, 0, Number.MAX_SAFE_INTEGER);
if (bytes <= 0) {
continue;
}
result[normalizedKey] = bytes;
}
return result;
}
function normalizeHosterRouting(raw: unknown, megaDebridPreferApi: boolean, megaDebridApiEnabled: boolean, megaDebridWebEnabled: boolean): Record<string, DebridProvider> {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
const result: Record<string, DebridProvider> = {};
@@ -205,6 +258,7 @@ function migrateUpdateRepo(raw: string, fallback: string): string {
export function normalizeSettings(settings: AppSettings): AppSettings {
const defaults = defaultSettings();
const currentUsageDay = getProviderUsageDayKey();
const megaLogin = asText(settings.megaLogin);
const megaPassword = asText(settings.megaPassword);
const megaDebridPreferApi = settings.megaDebridPreferApi !== undefined ? Boolean(settings.megaDebridPreferApi) : true;
@@ -215,6 +269,24 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
const megaDebridWebEnabled = settings.megaDebridWebEnabled !== undefined
? Boolean(settings.megaDebridWebEnabled)
: (hasMegaCreds ? !megaDebridPreferApi : defaults.megaDebridWebEnabled);
const providerDailyUsageDayRaw = asText(settings.providerDailyUsageDay);
const providerDailyUsageDay = /^\d{4}-\d{2}-\d{2}$/.test(providerDailyUsageDayRaw)
? providerDailyUsageDayRaw
: currentUsageDay;
const debridLinkApiKeyIds = getDebridLinkApiKeyIds(String(settings.debridLinkApiKeys ?? ""));
const providerDailyUsageBytes = normalizeProviderByteMap(
settings.providerDailyUsageBytes,
megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled,
"sum"
);
const debridLinkApiKeyDailyLimitBytes = normalizeNamedByteMap(
settings.debridLinkApiKeyDailyLimitBytes,
debridLinkApiKeyIds
);
const debridLinkApiKeyDailyUsageBytes = normalizeNamedByteMap(
settings.debridLinkApiKeyDailyUsageBytes,
debridLinkApiKeyIds
);
const normalized: AppSettings = {
token: asText(settings.token),
realDebridUseWebLogin: Boolean(settings.realDebridUseWebLogin),
@@ -273,6 +345,10 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
clipboardWatch: Boolean(settings.clipboardWatch),
minimizeToTray: Boolean(settings.minimizeToTray),
collapseNewPackages: settings.collapseNewPackages !== undefined ? Boolean(settings.collapseNewPackages) : defaults.collapseNewPackages,
accountListShowDetailedDebridLinkKeys: settings.accountListShowDetailedDebridLinkKeys !== undefined
? Boolean(settings.accountListShowDetailedDebridLinkKeys)
: defaults.accountListShowDetailedDebridLinkKeys,
autoSortPackagesByProgress: settings.autoSortPackagesByProgress !== undefined ? Boolean(settings.autoSortPackagesByProgress) : defaults.autoSortPackagesByProgress,
autoSkipExtracted: settings.autoSkipExtracted !== undefined ? Boolean(settings.autoSkipExtracted) : defaults.autoSkipExtracted,
confirmDeleteSelection: settings.confirmDeleteSelection !== undefined ? Boolean(settings.confirmDeleteSelection) : defaults.confirmDeleteSelection,
totalDownloadedAllTime: typeof settings.totalDownloadedAllTime === "number" && settings.totalDownloadedAllTime >= 0 ? settings.totalDownloadedAllTime : defaults.totalDownloadedAllTime,
@@ -282,7 +358,17 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
extractCpuPriority: settings.extractCpuPriority,
autoExtractWhenStopped: settings.autoExtractWhenStopped !== undefined ? Boolean(settings.autoExtractWhenStopped) : defaults.autoExtractWhenStopped,
disabledProviders: normalizeDisabledProviders(settings.disabledProviders),
hosterRouting: normalizeHosterRouting(settings.hosterRouting, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled)
hosterRouting: normalizeHosterRouting(settings.hosterRouting, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled),
providerDailyLimitBytes: normalizeProviderByteMap(
settings.providerDailyLimitBytes,
megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled,
"max"
),
providerDailyUsageBytes: providerDailyUsageDay === currentUsageDay ? providerDailyUsageBytes : {},
debridLinkApiKeyDailyLimitBytes,
debridLinkApiKeyDailyUsageBytes: providerDailyUsageDay === currentUsageDay ? debridLinkApiKeyDailyUsageBytes : {},
providerDailyUsageDay: providerDailyUsageDay === currentUsageDay ? providerDailyUsageDay : currentUsageDay,
scheduledStartEpochMs: clampNumber(settings.scheduledStartEpochMs, defaults.scheduledStartEpochMs, 0, Number.MAX_SAFE_INTEGER)
};
if (!VALID_PRIMARY_PROVIDERS.has(normalized.providerPrimary)) {
@@ -414,6 +500,9 @@ export function normalizeLoadedSession(raw: unknown): SessionState {
packageId,
url,
provider: VALID_ITEM_PROVIDERS.has(providerRaw) ? providerRaw : null,
providerLabel: asText(item.providerLabel) || undefined,
providerAccountId: asText(item.providerAccountId) || undefined,
providerAccountLabel: asText(item.providerAccountLabel) || undefined,
status,
retries: clampNumber(item.retries, 0, 0, 1_000_000),
speedBps: clampNumber(item.speedBps, 0, 0, 10_000_000_000),