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:
co-authored by
Claude Opus 4.6
parent
71b3612e82
commit
e212ccc86f
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface UnrestrictedLink {
|
||||
retriesUsed: number;
|
||||
skipTlsVerify?: boolean;
|
||||
sourceLabel?: string;
|
||||
sourceAccountId?: string;
|
||||
sourceAccountLabel?: string;
|
||||
}
|
||||
|
||||
function shouldRetryStatus(status: number): boolean {
|
||||
|
||||
+90
-1
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user