Initial public release v1.7.233

This commit is contained in:
Sucukdeluxe
2026-08-01 22:29:53 +02:00
commit dc367633f5
152 changed files with 85159 additions and 0 deletions
+66
View File
@@ -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);
}
+77
View File
@@ -0,0 +1,77 @@
export const IPC_CHANNELS = {
GET_SNAPSHOT: "app:get-snapshot",
GET_VERSION: "app:get-version",
CHECK_UPDATES: "app:check-updates",
INSTALL_UPDATE: "app:install-update",
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",
RESOLVE_START_CONFLICT: "queue:resolve-start-conflict",
CLEAR_ALL: "queue:clear-all",
START: "queue:start",
START_PACKAGES: "queue:start-packages",
STOP: "queue:stop",
TOGGLE_PAUSE: "queue:toggle-pause",
CANCEL_PACKAGE: "queue:cancel-package",
RENAME_PACKAGE: "queue:rename-package",
REORDER_PACKAGES: "queue:reorder-packages",
REMOVE_ITEM: "queue:remove-item",
TOGGLE_PACKAGE: "queue:toggle-package",
EXPORT_PACKAGE_SELECTION: "queue:export-package-selection",
EXPORT_ITEM_SELECTION: "queue:export-item-selection",
EXPORT_QUEUE: "queue:export",
IMPORT_QUEUE: "queue:import",
PICK_FOLDER: "dialog:pick-folder",
PICK_CONTAINERS: "dialog:pick-containers",
STATE_UPDATE: "state:update",
CLIPBOARD_DETECTED: "clipboard:detected",
TOGGLE_CLIPBOARD: "clipboard:toggle",
GET_SESSION_STATS: "stats:get-session-stats",
RESET_SESSION_STATS: "stats:reset-session",
RESET_DOWNLOAD_STATS: "stats:reset-download",
RESTART: "app:restart",
QUIT: "app:quit",
EXPORT_BACKUP: "app:export-backup",
IMPORT_BACKUP: "app:import-backup",
EXPORT_SUPPORT_BUNDLE: "app:export-support-bundle",
OPEN_LOG: "app:open-log",
OPEN_AUDIT_LOG: "app:open-audit-log",
OPEN_RENAME_LOG: "app:open-rename-log",
OPEN_SESSION_LOG: "app:open-session-log",
OPEN_TRACE_LOG: "app:open-trace-log",
OPEN_PACKAGE_LOG: "app:open-package-log",
OPEN_ITEM_LOG: "app:open-item-log",
GET_DEBUG_SETUP_CHECK: "app:get-debug-setup-check",
GET_RECENT_ERRORS: "app:get-recent-errors",
TEST_NOTIFY: "app:test-notify",
GET_TRACE_CONFIG: "app:get-trace-config",
SET_TRACE_ENABLED: "app:set-trace-enabled",
ROTATE_DEBUG_TOKEN: "app:rotate-debug-token",
GET_REMOTE_DIAGNOSTICS: "app:get-remote-diagnostics",
ENABLE_REMOTE_DIAGNOSTICS: "app:enable-remote-diagnostics",
DISABLE_REMOTE_DIAGNOSTICS: "app:disable-remote-diagnostics",
ROTATE_REMOTE_DIAGNOSTICS_TOKEN: "app:rotate-remote-diagnostics-token",
OPEN_REALDEBRID_LOGIN: "app:open-realdebrid-login",
OPEN_ALLDEBRID_LOGIN: "app:open-alldebrid-login",
IMPORT_BESTDEBRID_COOKIES: "app:import-bestdebrid-cookies",
GET_ALLDEBRID_HOST_INFO: "app:get-alldebrid-host-info",
GET_DEBRIDLINK_HOST_LIMITS: "app:get-debridlink-host-limits",
CHECK_DEBRID_ACCOUNTS: "app:check-debrid-accounts",
CHECK_MEGA_DEBRID_ACCOUNT: "app:check-mega-debrid-account",
RETRY_EXTRACTION: "queue:retry-extraction",
EXTRACT_NOW: "queue:extract-now",
RESET_PACKAGE: "queue:reset-package",
GET_HISTORY: "history:get",
CLEAR_HISTORY: "history:clear",
REMOVE_HISTORY_ENTRY: "history:remove-entry",
SET_PACKAGE_PRIORITY: "queue:set-package-priority",
SKIP_ITEMS: "queue:skip-items",
RESET_ITEMS: "queue:reset-items",
START_ITEMS: "queue:start-items",
LOG_RENDERER_ERROR: "log:renderer-error"
} as const;
+90
View File
@@ -0,0 +1,90 @@
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}`;
}
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 {
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);
}
+24
View File
@@ -0,0 +1,24 @@
export function isMegaDebridResolveFailure(errorText: string): boolean {
const text = String(errorText || "").toLowerCase();
return /supprim/.test(text)
|| text.includes("introuvable")
|| text.includes("n'existe plus")
|| text.includes("n existe plus")
|| text.includes("fichier inexistant");
}
export function isMegaDebridTransientResolveFailure(errorText: string): boolean {
const text = String(errorText || "").toLowerCase();
return isMegaDebridResolveFailure(text)
|| text.includes("datei beim hoster gerade nicht abrufbar")
|| text.includes("datei beim hoster nicht gefunden");
}
export function germanMegaDebridResolveReason(errorText: string): string {
const text = String(errorText || "").toLowerCase();
if (text.includes("datei beim hoster nicht gefunden")
|| text.includes("introuvable") || text.includes("fichier inexistant") || text.includes("n'existe plus") || text.includes("n existe plus")) {
return "Datei beim Hoster nicht gefunden";
}
return "Datei beim Hoster gerade nicht abrufbar";
}
+101
View File
@@ -0,0 +1,101 @@
import type {
AddLinksPayload,
AllDebridHostInfo,
AppSettings,
DebridAccountStatus,
DebugSetupCheckResult,
DebridLinkHostLimitInfo,
DebridProvider,
DuplicatePolicy,
EnableRemoteDiagnosticsInput,
HistoryEntry,
PackagePriority,
RemoteDiagnosticsInfo,
RendererErrorReport,
SessionStats,
StartConflictEntry,
StartConflictResolutionResult,
SupportTraceConfig,
UiSnapshot,
UpdateCheckResult,
UpdateInstallProgress,
UpdateInstallResult
} from "./types";
export interface ElectronApi {
getSnapshot: () => Promise<UiSnapshot>;
getVersion: () => Promise<string>;
checkUpdates: () => Promise<UpdateCheckResult>;
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[]>;
resolveStartConflict: (packageId: string, policy: DuplicatePolicy) => Promise<StartConflictResolutionResult>;
clearAll: () => Promise<void>;
start: () => Promise<void>;
startPackages: (packageIds: string[]) => Promise<void>;
stop: () => Promise<void>;
togglePause: () => Promise<boolean>;
cancelPackage: (packageId: string) => Promise<void>;
renamePackage: (packageId: string, newName: string) => Promise<void>;
reorderPackages: (packageIds: string[]) => Promise<void>;
removeItem: (itemId: string) => Promise<void>;
togglePackage: (packageId: string) => Promise<void>;
exportPackageSelection: (packageIds: string[]) => Promise<{ saved: boolean; packageCount: number; linkCount: number; filePath?: string }>;
exportItemSelection: (itemIds: string[]) => Promise<{ saved: boolean; packageCount: number; linkCount: number; filePath?: string }>;
exportQueue: () => Promise<{ saved: boolean }>;
importQueue: (json: string) => Promise<{ addedPackages: number; addedLinks: number }>;
toggleClipboard: () => Promise<boolean>;
pickFolder: () => Promise<string | null>;
pickContainers: () => Promise<string[]>;
getSessionStats: () => Promise<SessionStats>;
resetSessionStats: () => Promise<void>;
resetDownloadStats: () => Promise<void>;
restart: () => Promise<void>;
quit: () => Promise<void>;
exportBackup: () => Promise<{ saved: boolean }>;
importBackup: () => Promise<{ restored: boolean; relaunch: boolean; message: string }>;
exportSupportBundle: () => Promise<{ saved: boolean; filePath?: string }>;
openLog: () => Promise<void>;
openAuditLog: () => Promise<void>;
openRenameLog: () => Promise<void>;
openSessionLog: () => Promise<void>;
openTraceLog: () => Promise<void>;
openPackageLog: (packageId: string) => Promise<void>;
openItemLog: (itemId: string) => Promise<void>;
getDebugSetupCheck: () => Promise<DebugSetupCheckResult>;
getRecentErrors: () => Promise<Array<{ ts: string; level: string; message: string }>>;
testNotification: (url: string, mention: string) => Promise<boolean>;
getTraceConfig: () => Promise<SupportTraceConfig>;
setTraceEnabled: (enabled: boolean, note?: string, durationMinutes?: number) => Promise<SupportTraceConfig>;
rotateDebugToken: () => Promise<{ path: string }>;
getRemoteDiagnostics: () => Promise<RemoteDiagnosticsInfo>;
enableRemoteDiagnostics: (input: EnableRemoteDiagnosticsInput) => Promise<RemoteDiagnosticsInfo>;
disableRemoteDiagnostics: () => Promise<RemoteDiagnosticsInfo>;
rotateRemoteDiagnosticsToken: () => Promise<RemoteDiagnosticsInfo>;
openRealDebridLogin: () => Promise<void>;
openAllDebridLogin: () => Promise<void>;
importBestDebridCookies: () => Promise<number>;
getAllDebridHostInfo: () => Promise<AllDebridHostInfo>;
getDebridLinkHostLimits: () => Promise<DebridLinkHostLimitInfo[]>;
checkDebridAccounts: () => Promise<DebridAccountStatus[]>;
checkMegaDebridAccount: (login: string, password: string) => Promise<DebridAccountStatus | null>;
retryExtraction: (packageId: string) => Promise<void>;
extractNow: (packageId: string) => Promise<void>;
resetPackage: (packageId: string) => Promise<void>;
getHistory: () => Promise<HistoryEntry[]>;
clearHistory: () => Promise<void>;
removeHistoryEntry: (entryId: string) => Promise<void>;
setPackagePriority: (packageId: string, priority: PackagePriority) => Promise<void>;
skipItems: (itemIds: string[]) => Promise<void>;
resetItems: (itemIds: string[]) => Promise<void>;
startItems: (itemIds: string[]) => Promise<void>;
reportRendererError: (report: RendererErrorReport) => void;
onStateUpdate: (callback: (snapshot: UiSnapshot) => void) => () => void;
onClipboardDetected: (callback: (links: string[]) => void) => () => void;
onUpdateInstallProgress: (callback: (progress: UpdateInstallProgress) => void) => () => void;
}
+329
View File
@@ -0,0 +1,329 @@
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">>
& Partial<Pick<AppSettings, "megaDebridDisabledAccountIds" | "megaDebridAccountDailyLimitBytes" | "megaDebridAccountDailyUsageBytes">>;
type ProviderUsageSettings =
ProviderDailySettings
& Partial<Pick<AppSettings, "providerTotalUsageBytes" | "debridLinkApiKeyTotalUsageBytes">>
& Partial<Pick<AppSettings, "megaDebridAccountTotalUsageBytes">>;
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 getProviderTotalUsageBytes(settings: ProviderUsageSettings, provider: DebridProvider): number {
return normalizePositiveBytes(settings.providerTotalUsageBytes?.[provider]);
}
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 addProviderTotalUsageBytes(
settings: ProviderUsageSettings,
provider: DebridProvider,
byteDelta: number
): Pick<AppSettings, "providerTotalUsageBytes"> {
const increment = normalizePositiveBytes(byteDelta);
const currentUsageBytes = { ...(settings.providerTotalUsageBytes || {}) };
if (increment <= 0) {
return {
providerTotalUsageBytes: currentUsageBytes
};
}
currentUsageBytes[provider] = normalizePositiveBytes(currentUsageBytes[provider]) + increment;
return {
providerTotalUsageBytes: currentUsageBytes
};
}
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 getDebridLinkApiKeyTotalUsageBytes(settings: ProviderUsageSettings, keyId: string): number {
return normalizePositiveBytes(settings.debridLinkApiKeyTotalUsageBytes?.[keyId]);
}
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
};
}
export function addDebridLinkApiKeyTotalUsageBytes(
settings: ProviderUsageSettings,
keyId: string,
byteDelta: number
): Pick<AppSettings, "debridLinkApiKeyTotalUsageBytes"> {
const increment = normalizePositiveBytes(byteDelta);
const currentUsageBytes = { ...(settings.debridLinkApiKeyTotalUsageBytes || {}) };
if (increment <= 0) {
return {
debridLinkApiKeyTotalUsageBytes: currentUsageBytes
};
}
currentUsageBytes[keyId] = normalizePositiveBytes(currentUsageBytes[keyId]) + increment;
return {
debridLinkApiKeyTotalUsageBytes: currentUsageBytes
};
}
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
};
}
+567
View File
@@ -0,0 +1,567 @@
export type DownloadStatus =
| "queued"
| "validating"
| "downloading"
| "paused"
| "reconnect_wait"
| "extracting"
| "integrity_check"
| "completed"
| "failed"
| "cancelled";
export type CleanupMode = "none" | "trash" | "delete";
export type ConflictMode = "overwrite" | "skip" | "rename" | "ask";
export type SpeedMode = "global" | "per_download";
export type FinishedCleanupPolicy = "never" | "immediate" | "on_start" | "package_done";
export type DebridProvider =
| "realdebrid"
| "megadebrid"
| "megadebrid-api"
| "megadebrid-web"
| "bestdebrid"
| "alldebrid"
| "ddownload"
| "onefichier"
| "debridlink"
| "linksnappy";
export type DebridFallbackProvider = DebridProvider | "none";
export type AppTheme = "dark" | "light";
export type PackagePriority = "high" | "normal" | "low";
export type ExtractCpuPriority = "high" | "middle" | "low";
export type HistoryRetentionMode = "never" | "session" | "permanent";
export interface BandwidthScheduleEntry {
id: string;
startHour: number;
endHour: number;
speedLimitKbps: number;
enabled: boolean;
}
export interface DownloadStats {
totalDownloaded: number;
totalDownloadedAllTime: number;
totalFiles?: number;
totalFilesSession: number;
totalFilesAllTime: number;
totalPackages: number;
sessionStartedAt: number;
appSessionStartedAt: number;
sessionRuntimeMs: number;
totalRuntimeMs: number;
runtimeMeasuredAt: number;
}
export interface DebridAccountStatus {
accountId: string;
provider: "megadebrid" | "debridlink";
label: string;
maskedLogin: string;
valid: boolean;
isPremium: boolean;
premiumUntilMs: number | null;
email?: string;
message: string;
checkedAt: number;
}
export interface AppSettings {
token: string;
realDebridUseWebLogin: boolean;
megaLogin: string;
megaPassword: string;
megaCredentials: string;
megaDebridApiEnabled: boolean;
megaDebridWebEnabled: boolean;
megaDebridPreferApi: boolean;
bestToken: string;
bestDebridUseWebLogin: boolean;
allDebridToken: string;
allDebridUseWebLogin: boolean;
ddownloadLogin: string;
ddownloadPassword: string;
oneFichierApiKey: string;
debridLinkApiKeys: string;
debridLinkDisabledKeyIds: string[];
linkSnappyLogin: string;
linkSnappyPassword: string;
archivePasswordList: string;
rememberToken: boolean;
providerOrder: readonly DebridProvider[];
providerPrimary: DebridProvider;
providerSecondary: DebridFallbackProvider;
providerTertiary: DebridFallbackProvider;
autoProviderFallback: boolean;
outputDir: string;
packageName: string;
autoExtract: boolean;
autoRename4sf4sj: boolean;
keepGermanAudioOnly: boolean;
germanAudioMode: "tag" | "first";
extractDir: string;
collectMkvToLibrary: boolean;
mkvLibraryDir: string;
createExtractSubfolder: boolean;
hybridExtract: boolean;
cleanupMode: CleanupMode;
extractConflictMode: ConflictMode;
removeLinkFilesAfterExtract: boolean;
removeSamplesAfterExtract: boolean;
enableIntegrityCheck: boolean;
autoResumeOnStart: boolean;
autoReconnect: boolean;
reconnectWaitSeconds: number;
completedCleanupPolicy: FinishedCleanupPolicy;
maxParallel: number;
maxParallelExtract: number;
retryLimit: number;
speedLimitEnabled: boolean;
speedLimitKbps: number;
speedLimitMode: SpeedMode;
updateRepo: string;
autoUpdateCheck: boolean;
clipboardWatch: boolean;
minimizeToTray: boolean;
theme: AppTheme;
collapseNewPackages: boolean;
historyRetentionMode: HistoryRetentionMode;
historyMaxEntries: number;
historyMaxAgeDays: number;
accountListShowDetailedDebridLinkKeys: boolean;
autoSortPackagesByProgress: boolean;
autoSkipExtracted: boolean;
hideExtractedItems: boolean;
confirmDeleteSelection: boolean;
backupIncludeDownloads: boolean;
backupIncludeRemoteDiagnostics: boolean;
notifyUrl: string;
notifyMention: string;
notifyOnPackageCompleted: boolean;
notifyOnPackageFailed: boolean;
notifyOnRunFinished: boolean;
totalDownloadedAllTime: number;
totalCompletedFilesAllTime: number;
totalRuntimeAllTimeMs: number;
bandwidthSchedules: BandwidthScheduleEntry[];
columnOrder: string[];
extractCpuPriority: ExtractCpuPriority;
autoExtractWhenStopped: boolean;
disabledProviders: DebridProvider[];
hosterRouting: Record<string, DebridProvider>;
providerDailyLimitBytes: Partial<Record<DebridProvider, number>>;
providerDailyUsageBytes: Partial<Record<DebridProvider, number>>;
providerTotalUsageBytes: Partial<Record<DebridProvider, number>>;
debridLinkApiKeyDailyLimitBytes: Record<string, number>;
debridLinkApiKeyDailyUsageBytes: Record<string, number>;
debridLinkApiKeyTotalUsageBytes: Record<string, number>;
megaDebridDisabledAccountIds: string[];
megaDebridAccountDailyLimitBytes: Record<string, number>;
megaDebridAccountDailyUsageBytes: Record<string, number>;
megaDebridAccountTotalUsageBytes: Record<string, number>;
debridAccountStatuses: Record<string, DebridAccountStatus>;
providerDailyUsageDay: string;
scheduledStartEpochMs: number;
}
export interface DownloadItem {
id: string;
packageId: string;
url: string;
provider: DebridProvider | null;
providerLabel?: string;
providerAccountId?: string;
providerAccountLabel?: string;
status: DownloadStatus;
retries: number;
speedBps: number;
downloadedBytes: number;
totalBytes: number | null;
progressPercent: number;
fileName: string;
targetPath: string;
resumable: boolean;
attempts: number;
lastError: string;
fullStatus: string;
createdAt: number;
updatedAt: number;
onlineStatus?: "online" | "offline" | "checking";
}
export interface AudioStripFileResult {
name: string;
action: string;
reason: string;
languages?: string;
}
export interface AudioStripSummary {
at: number;
candidates: number;
remuxed: number;
keptSingle: number;
skippedNoGerman: number;
skippedNoTool: number;
failed: number;
files: AudioStripFileResult[];
}
export interface PackageEntry {
id: string;
name: string;
outputDir: string;
extractDir: string;
status: DownloadStatus;
itemIds: string[];
cancelled: boolean;
enabled: boolean;
priority?: PackagePriority;
postProcessLabel?: string;
audioStripSummary?: AudioStripSummary;
downloadStartedAt?: number;
downloadCompletedAt?: number;
createdAt: number;
updatedAt: number;
}
export interface SessionState {
version: number;
packageOrder: string[];
packages: Record<string, PackageEntry>;
items: Record<string, DownloadItem>;
runStartedAt: number;
totalDownloadedBytes: number;
summaryText: string;
reconnectUntil: number;
reconnectReason: string;
paused: boolean;
running: boolean;
updatedAt: number;
}
export interface DownloadSummary {
total: number;
success: number;
failed: number;
cancelled: number;
extracted: number;
durationSeconds: number;
averageSpeedBps: number;
}
export interface ParsedPackageInput {
name: string;
links: string[];
fileNames?: string[];
}
export interface ContainerImportResult {
packages: ParsedPackageInput[];
source: "dlc";
}
export interface RotationEvent {
id: string;
at: number;
level: "INFO" | "WARN" | "ERROR";
provider: string;
accountLabel: string;
event: string;
reason?: string;
category?: string;
cooldownSec?: number;
next?: string;
}
export interface UiSnapshot {
settings: AppSettings;
session: SessionState;
summary: DownloadSummary | null;
stats: DownloadStats;
speedText: string;
etaText: string;
canStart: boolean;
canStop: boolean;
canPause: boolean;
clipboardActive: boolean;
reconnectSeconds: number;
packageSpeedBps: Record<string, number>;
payloadKind?: "full" | "delta";
removedItemIds?: string[];
removedPackageIds?: string[];
rotationEvents?: RotationEvent[];
}
export interface AddLinksPayload {
rawText: string;
packageName?: string;
duplicatePolicy?: DuplicatePolicy;
}
export interface AddContainerPayload {
filePaths: string[];
}
export type DuplicatePolicy = "keep" | "skip" | "overwrite";
export interface QueueAddResult {
addedPackages: number;
addedLinks: number;
skippedExistingPackages: string[];
overwrittenPackages: string[];
}
export interface ContainerConflictResult {
conflicts: string[];
packageCount: number;
linkCount: number;
}
export interface StartConflictEntry {
packageId: string;
packageName: string;
extractDir: string;
}
export interface StartConflictResolutionResult {
skipped: boolean;
overwritten: boolean;
}
export interface UpdateCheckResult {
updateAvailable: boolean;
currentVersion: string;
latestVersion: string;
latestTag: string;
releaseUrl: string;
setupAssetUrl?: string;
setupAssetName?: string;
setupAssetDigest?: string;
releaseNotes?: string;
error?: string;
}
export interface UpdateInstallResult {
started: boolean;
message: string;
}
export interface UpdateInstallProgress {
stage: "starting" | "downloading" | "verifying" | "launching" | "done" | "error";
percent: number | null;
downloadedBytes: number;
totalBytes: number | null;
message: string;
}
export type AllDebridHostState = "up" | "down" | "not_tracked" | "unknown";
export type AllDebridHostInfoSource = "api" | "web";
export type DebridLinkHostState = "up" | "down" | "unknown";
export type DebridLinkKeyState = "ready" | "cooldown" | "invalid" | "quota" | "rate_limit" | "error" | "unknown";
export interface AllDebridHostInfo {
host: string;
source: AllDebridHostInfoSource;
state: AllDebridHostState;
statusLabel: string;
fetchedAt: number;
lastCheckedAt: number | null;
quota: number | null;
quotaMax: number | null;
quotaType: string;
limitSimuDl: number | null;
note: string;
}
export interface DebridLinkHostLimitInfo {
keyId: string;
keyLabel: string;
host: string;
fetchedAt: number;
trafficCurrentBytes: number | null;
trafficMaxBytes: number | null;
linksCurrent: number | null;
linksMax: number | null;
note: string;
state: DebridLinkKeyState;
stateLabel: string;
stateDetail: string;
cooldownUntil: number | null;
cooldownRemainingMs: number;
lastCheckedAt: number | null;
hostState: DebridLinkHostState;
hostStateLabel: string;
hostNote: string;
}
export interface ParsedHashEntry {
fileName: string;
algorithm: "crc32" | "md5" | "sha1";
digest: string;
}
export interface BandwidthSample {
timestamp: number;
speedBps: number;
}
export interface BandwidthStats {
samples: BandwidthSample[];
currentSpeedBps: number;
averageSpeedBps: number;
maxSpeedBps: number;
totalBytesSession: number;
sessionDurationSeconds: number;
}
export interface SessionStats {
bandwidth: BandwidthStats;
totalDownloads: number;
completedDownloads: number;
failedDownloads: number;
activeDownloads: number;
queuedDownloads: number;
}
export interface SupportTraceConfig {
enabled: boolean;
includeMainLog: boolean;
includeAudit: boolean;
logDebugRequests: boolean;
autoDisableAt: string | null;
updatedAt: string;
}
export interface SupportFileSizeInfo {
path: string | null;
exists: boolean;
bytes: number;
}
export interface SupportDirectorySizeInfo {
path: string;
exists: boolean;
fileCount: number;
bytes: number;
}
export interface SupportDiskSpaceInfo {
path: string;
totalBytes: number | null;
freeBytes: number | null;
freePercent: number | null;
}
export interface SupportBundleEstimate {
estimatedBytes: number;
estimatedEntries: number;
duplicatedLiveLogBytes: number;
note: string;
}
export interface DebugSetupCheckResult {
status: "ok" | "warn";
enabled: boolean;
runtimeBaseDir: string;
host: string;
port: number;
localOnly: boolean;
tokenConfigured: boolean;
tokenPath: string;
supportManifestPath: string;
supportManifestPresent: boolean;
traceConfigPath: string | null;
traceLogPath: string | null;
traceEnabled: boolean;
traceAutoDisableAt: string | null;
diskSpace: {
runtime: SupportDiskSpaceInfo;
output: SupportDiskSpaceInfo;
extract: SupportDiskSpaceInfo;
};
logSummary: {
totalBytes: number;
main: SupportFileSizeInfo;
mainBackup: SupportFileSizeInfo;
audit: SupportFileSizeInfo;
auditBackup: SupportFileSizeInfo;
rename: SupportFileSizeInfo;
renameBackup: SupportFileSizeInfo;
session: SupportFileSizeInfo;
trace: SupportFileSizeInfo;
traceBackup: SupportFileSizeInfo;
sessionLogs: SupportDirectorySizeInfo;
packageLogs: SupportDirectorySizeInfo;
itemLogs: SupportDirectorySizeInfo;
};
supportBundle: SupportBundleEstimate;
warnings: string[];
notes: string[];
localUrls: {
health: string;
meta: string;
diagnostics: string;
};
remoteUrlTemplates: {
health: string;
meta: string;
diagnostics: string;
};
}
export interface HistoryEntry {
id: string;
name: string;
totalBytes: number;
downloadedBytes: number;
fileCount: number;
provider: DebridProvider | null;
completedAt: number;
durationSeconds: number;
status: "completed" | "deleted";
outputDir: string;
urls?: string[];
}
export interface HistoryState {
entries: HistoryEntry[];
maxEntries: number;
}
export interface RendererErrorReport {
kind: "error" | "unhandledrejection" | "react";
message: string;
stack?: string;
source?: string;
line?: number;
column?: number;
componentStack?: string;
}
export interface RemoteDiagnosticsStatus {
running: boolean;
host: string;
port: number;
hasToken: boolean;
localOnly: boolean;
allowlistCount: number;
}
export interface RemoteDiagnosticsInfo {
status: RemoteDiagnosticsStatus;
code: string | null;
publicHost: string;
name: string;
allowlist: string[];
suggestedHosts: string[];
}
export interface EnableRemoteDiagnosticsInput {
hostMode: "local" | "network";
publicHost: string;
port?: number;
allowlist: string[];
name?: string;
rotateToken?: boolean;
}