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
@@ -0,0 +1,66 @@
|
||||
export interface DebridLinkApiKeyEntry {
|
||||
id: string;
|
||||
token: string;
|
||||
index: number;
|
||||
label: string;
|
||||
masked: 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 maskDebridLinkApiKey(token: string): string {
|
||||
const trimmed = token.trim();
|
||||
if (!trimmed) {
|
||||
return "Nicht hinterlegt";
|
||||
}
|
||||
if (trimmed.length <= 6) {
|
||||
return "*".repeat(trimmed.length);
|
||||
}
|
||||
return `${trimmed.slice(0, 3)}${"*".repeat(Math.max(4, trimmed.length - 6))}${trimmed.slice(-3)}`;
|
||||
}
|
||||
|
||||
export function getDebridLinkApiKeyId(token: string): string {
|
||||
return `dlk_${fnv1a64(token.trim())}`;
|
||||
}
|
||||
|
||||
export function getDebridLinkApiKeyLabel(index: number): string {
|
||||
return `Key ${index + 1}`;
|
||||
}
|
||||
|
||||
export function parseDebridLinkApiKeys(raw: string): DebridLinkApiKeyEntry[] {
|
||||
const seen = new Set<string>();
|
||||
const tokens = String(raw || "")
|
||||
.split(/[\n,]+/)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
.filter((token) => {
|
||||
if (seen.has(token)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(token);
|
||||
return true;
|
||||
});
|
||||
|
||||
return tokens.map((token, index) => ({
|
||||
id: getDebridLinkApiKeyId(token),
|
||||
token,
|
||||
index,
|
||||
label: getDebridLinkApiKeyLabel(index),
|
||||
masked: maskDebridLinkApiKey(token)
|
||||
}));
|
||||
}
|
||||
|
||||
export function getDebridLinkApiKeyIds(raw: string): string[] {
|
||||
return parseDebridLinkApiKeys(raw).map((entry) => entry.id);
|
||||
}
|
||||
@@ -6,6 +6,8 @@ export const IPC_CHANNELS = {
|
||||
UPDATE_INSTALL_PROGRESS: "app:update-install-progress",
|
||||
OPEN_EXTERNAL: "app:open-external",
|
||||
UPDATE_SETTINGS: "app:update-settings",
|
||||
RESET_PROVIDER_DAILY_USAGE: "app:reset-provider-daily-usage",
|
||||
RESET_DEBRID_LINK_API_KEY_DAILY_USAGE: "app:reset-debrid-link-api-key-daily-usage",
|
||||
ADD_LINKS: "queue:add-links",
|
||||
ADD_CONTAINERS: "queue:add-containers",
|
||||
GET_START_CONFLICTS: "queue:get-start-conflicts",
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
AddLinksPayload,
|
||||
AllDebridHostInfo,
|
||||
AppSettings,
|
||||
DebridProvider,
|
||||
DuplicatePolicy,
|
||||
HistoryEntry,
|
||||
PackagePriority,
|
||||
@@ -21,6 +22,8 @@ export interface ElectronApi {
|
||||
installUpdate: () => Promise<UpdateInstallResult>;
|
||||
openExternal: (url: string) => Promise<boolean>;
|
||||
updateSettings: (settings: Partial<AppSettings>) => Promise<AppSettings>;
|
||||
resetProviderDailyUsage: (provider: DebridProvider) => Promise<AppSettings>;
|
||||
resetDebridLinkApiKeyDailyUsage: (keyId: string) => Promise<AppSettings>;
|
||||
addLinks: (payload: AddLinksPayload) => Promise<{ addedPackages: number; addedLinks: number; invalidCount: number }>;
|
||||
addContainers: (filePaths: string[]) => Promise<{ addedPackages: number; addedLinks: number }>;
|
||||
getStartConflicts: () => Promise<StartConflictEntry[]>;
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import type { AppSettings, DebridProvider } from "./types";
|
||||
|
||||
export type ProviderByteMap = Partial<Record<DebridProvider, number>>;
|
||||
export type DebridLinkKeyByteMap = Record<string, number>;
|
||||
|
||||
type ProviderDailySettings =
|
||||
Pick<AppSettings, "providerDailyLimitBytes" | "providerDailyUsageBytes" | "providerDailyUsageDay">
|
||||
& Partial<Pick<AppSettings, "debridLinkApiKeyDailyLimitBytes" | "debridLinkApiKeyDailyUsageBytes">>;
|
||||
|
||||
function normalizePositiveBytes(value: unknown): number {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.floor(numeric);
|
||||
}
|
||||
|
||||
export function getProviderUsageDayKey(epochMs = Date.now()): string {
|
||||
const current = new Date(epochMs);
|
||||
const year = current.getFullYear();
|
||||
const month = String(current.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(current.getDate()).padStart(2, "0");
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
export function getProviderDailyLimitBytes(settings: ProviderDailySettings, provider: DebridProvider): number {
|
||||
return normalizePositiveBytes(settings.providerDailyLimitBytes?.[provider]);
|
||||
}
|
||||
|
||||
export function getProviderDailyUsageBytes(
|
||||
settings: ProviderDailySettings,
|
||||
provider: DebridProvider,
|
||||
epochMs = Date.now()
|
||||
): number {
|
||||
if (settings.providerDailyUsageDay !== getProviderUsageDayKey(epochMs)) {
|
||||
return 0;
|
||||
}
|
||||
return normalizePositiveBytes(settings.providerDailyUsageBytes?.[provider]);
|
||||
}
|
||||
|
||||
export function getProviderDailyRemainingBytes(
|
||||
settings: ProviderDailySettings,
|
||||
provider: DebridProvider,
|
||||
epochMs = Date.now()
|
||||
): number | null {
|
||||
const limit = getProviderDailyLimitBytes(settings, provider);
|
||||
if (limit <= 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.max(0, limit - getProviderDailyUsageBytes(settings, provider, epochMs));
|
||||
}
|
||||
|
||||
export function isProviderDailyLimitReached(
|
||||
settings: ProviderDailySettings,
|
||||
provider: DebridProvider,
|
||||
epochMs = Date.now()
|
||||
): boolean {
|
||||
const limit = getProviderDailyLimitBytes(settings, provider);
|
||||
return limit > 0 && getProviderDailyUsageBytes(settings, provider, epochMs) >= limit;
|
||||
}
|
||||
|
||||
export function resetProviderDailyUsage(
|
||||
settings: ProviderDailySettings,
|
||||
provider?: DebridProvider,
|
||||
epochMs = Date.now()
|
||||
): Pick<AppSettings, "providerDailyUsageDay" | "providerDailyUsageBytes"> {
|
||||
const dayKey = getProviderUsageDayKey(epochMs);
|
||||
if (!provider) {
|
||||
return {
|
||||
providerDailyUsageDay: dayKey,
|
||||
providerDailyUsageBytes: {}
|
||||
};
|
||||
}
|
||||
|
||||
const nextUsageBytes = settings.providerDailyUsageDay === dayKey
|
||||
? { ...(settings.providerDailyUsageBytes || {}) }
|
||||
: {};
|
||||
delete nextUsageBytes[provider];
|
||||
|
||||
return {
|
||||
providerDailyUsageDay: dayKey,
|
||||
providerDailyUsageBytes: nextUsageBytes
|
||||
};
|
||||
}
|
||||
|
||||
export function addProviderDailyUsageBytes(
|
||||
settings: ProviderDailySettings,
|
||||
provider: DebridProvider,
|
||||
byteDelta: number,
|
||||
epochMs = Date.now()
|
||||
): Pick<AppSettings, "providerDailyUsageDay" | "providerDailyUsageBytes"> {
|
||||
const increment = normalizePositiveBytes(byteDelta);
|
||||
const dayKey = getProviderUsageDayKey(epochMs);
|
||||
const currentUsageBytes = settings.providerDailyUsageDay === dayKey
|
||||
? { ...(settings.providerDailyUsageBytes || {}) }
|
||||
: {};
|
||||
if (increment <= 0) {
|
||||
return {
|
||||
providerDailyUsageDay: dayKey,
|
||||
providerDailyUsageBytes: currentUsageBytes
|
||||
};
|
||||
}
|
||||
|
||||
const nextUsageBytes = currentUsageBytes;
|
||||
nextUsageBytes[provider] = normalizePositiveBytes(nextUsageBytes[provider]) + increment;
|
||||
|
||||
return {
|
||||
providerDailyUsageDay: dayKey,
|
||||
providerDailyUsageBytes: nextUsageBytes
|
||||
};
|
||||
}
|
||||
|
||||
export function getDebridLinkApiKeyDailyLimitBytes(settings: ProviderDailySettings, keyId: string): number {
|
||||
return normalizePositiveBytes(settings.debridLinkApiKeyDailyLimitBytes?.[keyId]);
|
||||
}
|
||||
|
||||
export function getDebridLinkApiKeyDailyUsageBytes(
|
||||
settings: ProviderDailySettings,
|
||||
keyId: string,
|
||||
epochMs = Date.now()
|
||||
): number {
|
||||
if (settings.providerDailyUsageDay !== getProviderUsageDayKey(epochMs)) {
|
||||
return 0;
|
||||
}
|
||||
return normalizePositiveBytes(settings.debridLinkApiKeyDailyUsageBytes?.[keyId]);
|
||||
}
|
||||
|
||||
export function getDebridLinkApiKeyDailyRemainingBytes(
|
||||
settings: ProviderDailySettings,
|
||||
keyId: string,
|
||||
epochMs = Date.now()
|
||||
): number | null {
|
||||
const limit = getDebridLinkApiKeyDailyLimitBytes(settings, keyId);
|
||||
if (limit <= 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.max(0, limit - getDebridLinkApiKeyDailyUsageBytes(settings, keyId, epochMs));
|
||||
}
|
||||
|
||||
export function isDebridLinkApiKeyDailyLimitReached(
|
||||
settings: ProviderDailySettings,
|
||||
keyId: string,
|
||||
epochMs = Date.now()
|
||||
): boolean {
|
||||
const limit = getDebridLinkApiKeyDailyLimitBytes(settings, keyId);
|
||||
return limit > 0 && getDebridLinkApiKeyDailyUsageBytes(settings, keyId, epochMs) >= limit;
|
||||
}
|
||||
|
||||
export function resetDebridLinkApiKeyDailyUsage(
|
||||
settings: ProviderDailySettings,
|
||||
keyId?: string,
|
||||
epochMs = Date.now()
|
||||
): Pick<AppSettings, "providerDailyUsageDay" | "debridLinkApiKeyDailyUsageBytes"> {
|
||||
const dayKey = getProviderUsageDayKey(epochMs);
|
||||
if (!keyId) {
|
||||
return {
|
||||
providerDailyUsageDay: dayKey,
|
||||
debridLinkApiKeyDailyUsageBytes: {}
|
||||
};
|
||||
}
|
||||
|
||||
const nextUsageBytes = settings.providerDailyUsageDay === dayKey
|
||||
? { ...(settings.debridLinkApiKeyDailyUsageBytes || {}) }
|
||||
: {};
|
||||
delete nextUsageBytes[keyId];
|
||||
|
||||
return {
|
||||
providerDailyUsageDay: dayKey,
|
||||
debridLinkApiKeyDailyUsageBytes: nextUsageBytes
|
||||
};
|
||||
}
|
||||
|
||||
export function addDebridLinkApiKeyDailyUsageBytes(
|
||||
settings: ProviderDailySettings,
|
||||
keyId: string,
|
||||
byteDelta: number,
|
||||
epochMs = Date.now()
|
||||
): Pick<AppSettings, "providerDailyUsageDay" | "debridLinkApiKeyDailyUsageBytes"> {
|
||||
const increment = normalizePositiveBytes(byteDelta);
|
||||
const dayKey = getProviderUsageDayKey(epochMs);
|
||||
const currentUsageBytes = settings.providerDailyUsageDay === dayKey
|
||||
? { ...(settings.debridLinkApiKeyDailyUsageBytes || {}) }
|
||||
: {};
|
||||
if (increment <= 0) {
|
||||
return {
|
||||
providerDailyUsageDay: dayKey,
|
||||
debridLinkApiKeyDailyUsageBytes: currentUsageBytes
|
||||
};
|
||||
}
|
||||
|
||||
currentUsageBytes[keyId] = normalizePositiveBytes(currentUsageBytes[keyId]) + increment;
|
||||
|
||||
return {
|
||||
providerDailyUsageDay: dayKey,
|
||||
debridLinkApiKeyDailyUsageBytes: currentUsageBytes
|
||||
};
|
||||
}
|
||||
@@ -101,6 +101,8 @@ export interface AppSettings {
|
||||
minimizeToTray: boolean;
|
||||
theme: AppTheme;
|
||||
collapseNewPackages: boolean;
|
||||
accountListShowDetailedDebridLinkKeys: boolean;
|
||||
autoSortPackagesByProgress: boolean;
|
||||
autoSkipExtracted: boolean;
|
||||
confirmDeleteSelection: boolean;
|
||||
totalDownloadedAllTime: number;
|
||||
@@ -110,6 +112,11 @@ export interface AppSettings {
|
||||
autoExtractWhenStopped: boolean;
|
||||
disabledProviders: DebridProvider[];
|
||||
hosterRouting: Record<string, DebridProvider>;
|
||||
providerDailyLimitBytes: Partial<Record<DebridProvider, number>>;
|
||||
providerDailyUsageBytes: Partial<Record<DebridProvider, number>>;
|
||||
debridLinkApiKeyDailyLimitBytes: Record<string, number>;
|
||||
debridLinkApiKeyDailyUsageBytes: Record<string, number>;
|
||||
providerDailyUsageDay: string;
|
||||
scheduledStartEpochMs: number;
|
||||
}
|
||||
|
||||
@@ -119,6 +126,8 @@ export interface DownloadItem {
|
||||
url: string;
|
||||
provider: DebridProvider | null;
|
||||
providerLabel?: string;
|
||||
providerAccountId?: string;
|
||||
providerAccountLabel?: string;
|
||||
status: DownloadStatus;
|
||||
retries: number;
|
||||
speedBps: number;
|
||||
|
||||
Reference in New Issue
Block a user