Release v1.7.1
This commit is contained in:
parent
576be53b83
commit
7737a4b0da
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.6.90",
|
||||
"version": "1.7.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.6.90",
|
||||
"version": "1.7.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "real-debrid-downloader",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"description": "Desktop downloader",
|
||||
"main": "build/main/main/main.js",
|
||||
"author": "Sucukdeluxe",
|
||||
|
||||
@ -22,7 +22,7 @@ import { resetDebridLinkApiKeyDailyUsage, resetProviderDailyUsage } from "../sha
|
||||
import { importDlcContainers } from "./container";
|
||||
import { APP_VERSION } from "./constants";
|
||||
import { DownloadManager } from "./download-manager";
|
||||
import { fetchAllDebridHostInfo } from "./debrid";
|
||||
import { fetchAllDebridHostInfo, fetchDebridLinkHostLimits } from "./debrid";
|
||||
import { parseCollectorInput } from "./link-parser";
|
||||
import { configureLogger, getLogFilePath, logger } from "./logger";
|
||||
import { AllDebridWebFallback } from "./all-debrid-web";
|
||||
@ -248,6 +248,10 @@ export class AppController {
|
||||
return fetchAllDebridHostInfo(token, host);
|
||||
}
|
||||
|
||||
public async getDebridLinkHostLimits(host = "rapidgator") {
|
||||
return fetchDebridLinkHostLimits(this.settings.debridLinkApiKeys, host);
|
||||
}
|
||||
|
||||
public async checkUpdates(): Promise<UpdateCheckResult> {
|
||||
const result = await checkGitHubUpdate(this.settings.updateRepo);
|
||||
if (!result.error) {
|
||||
|
||||
@ -56,6 +56,7 @@ export function defaultSettings(): AppSettings {
|
||||
ddownloadPassword: "",
|
||||
oneFichierApiKey: "",
|
||||
debridLinkApiKeys: "",
|
||||
debridLinkDisabledKeyIds: [],
|
||||
linkSnappyLogin: "",
|
||||
linkSnappyPassword: "",
|
||||
archivePasswordList: "",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||
import { AllDebridHostInfo, AppSettings, DebridFallbackProvider, DebridProvider } from "../shared/types";
|
||||
import { AllDebridHostInfo, AppSettings, DebridFallbackProvider, DebridLinkHostLimitInfo, DebridProvider } from "../shared/types";
|
||||
import { isDebridLinkApiKeyDailyLimitReached, isProviderDailyLimitReached } from "../shared/provider-daily-limits";
|
||||
import { APP_VERSION, REQUEST_RETRIES } from "./constants";
|
||||
import { logger } from "./logger";
|
||||
@ -68,6 +68,7 @@ function cloneSettings(settings: AppSettings): AppSettings {
|
||||
return {
|
||||
...settings,
|
||||
bandwidthSchedules: (settings.bandwidthSchedules || []).map((entry) => ({ ...entry })),
|
||||
debridLinkDisabledKeyIds: [...(settings.debridLinkDisabledKeyIds || [])],
|
||||
providerDailyLimitBytes: { ...(settings.providerDailyLimitBytes || {}) },
|
||||
providerDailyUsageBytes: { ...(settings.providerDailyUsageBytes || {}) },
|
||||
debridLinkApiKeyDailyLimitBytes: { ...(settings.debridLinkApiKeyDailyLimitBytes || {}) },
|
||||
@ -75,9 +76,13 @@ function cloneSettings(settings: AppSettings): AppSettings {
|
||||
};
|
||||
}
|
||||
|
||||
function getAvailableDebridLinkApiKeys(settings: AppSettings, epochMs = Date.now()) {
|
||||
export function isDebridLinkApiKeyDisabled(settings: AppSettings, keyId: string): boolean {
|
||||
return (settings.debridLinkDisabledKeyIds || []).includes(keyId);
|
||||
}
|
||||
|
||||
export function getAvailableDebridLinkApiKeys(settings: AppSettings, epochMs = Date.now()) {
|
||||
return parseDebridLinkApiKeys(settings.debridLinkApiKeys).filter(
|
||||
(entry) => !isDebridLinkApiKeyDailyLimitReached(settings, entry.id, epochMs)
|
||||
(entry) => !isDebridLinkApiKeyDisabled(settings, entry.id) && !isDebridLinkApiKeyDailyLimitReached(settings, entry.id, epochMs)
|
||||
);
|
||||
}
|
||||
|
||||
@ -312,6 +317,126 @@ function toAllDebridHostStatusLabel(state: AllDebridHostInfo["state"]): string {
|
||||
return "Unbekannt";
|
||||
}
|
||||
|
||||
function normalizeDebridLinkHostKey(value: string): string {
|
||||
return String(value || "").replace(/[^a-z0-9]+/gi, "").toLowerCase();
|
||||
}
|
||||
|
||||
function parseDebridLinkSuccess(payload: Record<string, unknown> | null): boolean {
|
||||
if (!payload) {
|
||||
return false;
|
||||
}
|
||||
if (typeof payload.success === "boolean") {
|
||||
return payload.success;
|
||||
}
|
||||
return pickString(payload, ["result"]).toUpperCase() === "OK";
|
||||
}
|
||||
|
||||
function parseDebridLinkHosters(payload: Record<string, unknown> | null): Record<string, unknown>[] {
|
||||
const value = asRecord(payload?.value);
|
||||
const hosters = value?.hosters ?? payload?.hosters;
|
||||
if (Array.isArray(hosters)) {
|
||||
return hosters.filter((entry): entry is Record<string, unknown> => Boolean(asRecord(entry))).map((entry) => entry as Record<string, unknown>);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function findDebridLinkHostEntry(payload: Record<string, unknown> | null, host: string): Record<string, unknown> | null {
|
||||
const wanted = normalizeDebridLinkHostKey(host);
|
||||
for (const entry of parseDebridLinkHosters(payload)) {
|
||||
const name = normalizeDebridLinkHostKey(pickString(entry, ["name", "host"]));
|
||||
if (name === wanted) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function fetchDebridLinkHostLimitForKey(apiKey: { id: string; label: string; token: string }, host: string, signal?: AbortSignal): Promise<DebridLinkHostLimitInfo> {
|
||||
let lastError = "";
|
||||
const hostLabel = host.trim() || "rapidgator";
|
||||
const endpoints = [`${DEBRID_LINK_API_BASE}/downloader/limits/all`, `${DEBRID_LINK_API_BASE}/downloader/limits`];
|
||||
|
||||
for (const endpoint of endpoints) {
|
||||
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey.token}`,
|
||||
"User-Agent": DEBRID_USER_AGENT
|
||||
},
|
||||
signal: withTimeoutSignal(signal, API_TIMEOUT_MS)
|
||||
});
|
||||
const text = await response.text();
|
||||
const payload = parseJsonSafe(text);
|
||||
|
||||
if (response.status === 404 && endpoint.endsWith("/all")) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const reason = parseError(response.status, text, payload);
|
||||
if (shouldRetryStatus(response.status) && attempt < REQUEST_RETRIES) {
|
||||
await sleepWithSignal(retryDelayForResponse(response, attempt), signal);
|
||||
continue;
|
||||
}
|
||||
throw new Error(reason);
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
throw new Error("Debrid-Link Limits Antwort ist kein JSON-Objekt");
|
||||
}
|
||||
if (!parseDebridLinkSuccess(payload)) {
|
||||
throw new Error(pickString(payload, ["error_description", "error", "message"]) || "Debrid-Link Limits fehlgeschlagen");
|
||||
}
|
||||
|
||||
const hostEntry = findDebridLinkHostEntry(payload, hostLabel);
|
||||
if (!hostEntry) {
|
||||
if (endpoint.endsWith("/all")) {
|
||||
return {
|
||||
keyId: apiKey.id,
|
||||
keyLabel: apiKey.label,
|
||||
host: hostLabel,
|
||||
fetchedAt: Date.now(),
|
||||
trafficCurrentBytes: null,
|
||||
trafficMaxBytes: null,
|
||||
linksCurrent: null,
|
||||
linksMax: null,
|
||||
note: `${hostLabel} nicht in der Debrid-Link-Limits-Antwort vorhanden.`
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const daySize = asRecord(hostEntry.daySize);
|
||||
const dayCount = asRecord(hostEntry.dayCount);
|
||||
return {
|
||||
keyId: apiKey.id,
|
||||
keyLabel: apiKey.label,
|
||||
host: pickString(hostEntry, ["name", "host"]) || hostLabel,
|
||||
fetchedAt: Date.now(),
|
||||
trafficCurrentBytes: pickNumber(daySize, ["current"]),
|
||||
trafficMaxBytes: pickNumber(daySize, ["value", "max"]),
|
||||
linksCurrent: pickNumber(dayCount, ["current"]),
|
||||
linksMax: pickNumber(dayCount, ["value", "max"]),
|
||||
note: ""
|
||||
};
|
||||
} catch (error) {
|
||||
lastError = compactErrorText(error);
|
||||
if (signal?.aborted || (/aborted/i.test(lastError) && !/timeout/i.test(lastError))) {
|
||||
break;
|
||||
}
|
||||
if (attempt >= REQUEST_RETRIES || !isRetryableErrorText(lastError)) {
|
||||
break;
|
||||
}
|
||||
await sleepWithSignal(retryDelay(attempt), signal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(String(lastError || `Debrid-Link Limits für ${apiKey.label} fehlgeschlagen`).replace(/^Error:\s*/i, ""));
|
||||
}
|
||||
|
||||
function uniqueProviderOrder(order: DebridProvider[]): DebridProvider[] {
|
||||
const seen = new Set<DebridProvider>();
|
||||
const result: DebridProvider[] = [];
|
||||
@ -1314,6 +1439,19 @@ export async function fetchAllDebridHostInfo(token: string, host = "rapidgator",
|
||||
return new AllDebridClient(token).getHostInfo(host, signal);
|
||||
}
|
||||
|
||||
export async function fetchDebridLinkHostLimits(apiKeysRaw: string, host = "rapidgator", signal?: AbortSignal): Promise<DebridLinkHostLimitInfo[]> {
|
||||
const apiKeys = parseDebridLinkApiKeys(apiKeysRaw);
|
||||
if (apiKeys.length === 0) {
|
||||
throw new Error("Debrid-Link ist nicht konfiguriert");
|
||||
}
|
||||
|
||||
const results: DebridLinkHostLimitInfo[] = [];
|
||||
for (const apiKey of apiKeys) {
|
||||
results.push(await fetchDebridLinkHostLimitForKey(apiKey, host, signal));
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// ── Debrid-Link Client ──
|
||||
|
||||
class DebridLinkClient {
|
||||
@ -1330,7 +1468,7 @@ class DebridLinkClient {
|
||||
}
|
||||
|
||||
if (getAvailableDebridLinkApiKeys(settings).length === 0) {
|
||||
throw new Error(`Debrid-Link: Alle ${this.apiKeys.length} API-Keys haben ihr Tageslimit erreicht`);
|
||||
throw new Error("Debrid-Link: Kein aktiver API-Key verfuegbar (deaktiviert oder am Tageslimit)");
|
||||
}
|
||||
|
||||
let checkedKeys = 0;
|
||||
@ -1338,7 +1476,13 @@ class DebridLinkClient {
|
||||
const apiKey = this.apiKeys[this.currentKeyIndex];
|
||||
checkedKeys += 1;
|
||||
const keyLabel = this.apiKeys.length > 1 ? ` (${apiKey.label})` : "";
|
||||
if (isDebridLinkApiKeyDisabled(settings, apiKey.id)) {
|
||||
logger.info(`Debrid-Link${keyLabel}: uebersprungen (manuell deaktiviert), pruefe naechsten Key`);
|
||||
this.currentKeyIndex = (this.currentKeyIndex + 1) % this.apiKeys.length;
|
||||
continue;
|
||||
}
|
||||
if (isDebridLinkApiKeyDailyLimitReached(settings, apiKey.id)) {
|
||||
logger.info(`Debrid-Link${keyLabel}: uebersprungen (lokales Tageslimit erreicht), pruefe naechsten Key`);
|
||||
this.currentKeyIndex = (this.currentKeyIndex + 1) % this.apiKeys.length;
|
||||
continue;
|
||||
}
|
||||
@ -1364,6 +1508,7 @@ class DebridLinkClient {
|
||||
const errorDesc = String(json.error_description || json.error || "Unbekannter Debrid-Link-Fehler");
|
||||
|
||||
if (DEBRID_LINK_QUOTA_ERRORS.has(errorCode)) {
|
||||
logger.warn(`Debrid-Link${keyLabel}: API-Quota erreicht (${errorCode}: ${errorDesc}), wechsle zum naechsten Key`);
|
||||
logger.warn(`Debrid-Link Quota erreicht${keyLabel}: ${errorCode} – ${errorDesc}`);
|
||||
break;
|
||||
}
|
||||
@ -1411,6 +1556,7 @@ class DebridLinkClient {
|
||||
if (/Ungültig|abgelaufen/i.test(lastError)) {
|
||||
throw error;
|
||||
}
|
||||
logger.warn(`Debrid-Link${keyLabel}: Fehler bei Unrestrict-Versuch ${attempt}/${REQUEST_RETRIES}: ${lastError}`);
|
||||
if (attempt < REQUEST_RETRIES) {
|
||||
await sleep(retryDelay(attempt), signal);
|
||||
}
|
||||
@ -1418,9 +1564,14 @@ class DebridLinkClient {
|
||||
}
|
||||
|
||||
this.currentKeyIndex = (this.currentKeyIndex + 1) % this.apiKeys.length;
|
||||
if (checkedKeys < this.apiKeys.length) {
|
||||
const nextKey = this.apiKeys[this.currentKeyIndex];
|
||||
const nextKeyLabel = this.apiKeys.length > 1 ? ` (${nextKey.label})` : "";
|
||||
logger.info(`Debrid-Link${keyLabel}: kein Erfolg, wechsle zu naechstem Key${nextKeyLabel}`);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Debrid-Link: Alle ${this.apiKeys.length} API-Keys haben ihr Limit erreicht`);
|
||||
throw new Error("Debrid-Link: Kein aktiver API-Key verfuegbar");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1948,7 +2099,7 @@ export class DebridService {
|
||||
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 "Debrid-Link nicht verfuegbar (alle aktiven API-Keys deaktiviert oder ausgeschopft)";
|
||||
}
|
||||
return `${PROVIDER_LABELS[effectiveProvider]} Tageslimit erreicht`;
|
||||
}
|
||||
@ -2084,11 +2235,13 @@ export class DebridService {
|
||||
configuredFound = true;
|
||||
if (this.isProviderDailyLimited(settings, provider)) {
|
||||
limitReachedFound = true;
|
||||
logger.info(`Provider-Kette: ${PROVIDER_LABELS[provider]} uebersprungen (${this.formatProviderLimitMessage(settings, provider)})`);
|
||||
attempts.push(this.formatProviderLimitMessage(settings, provider));
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Provider-Kette: versuche ${PROVIDER_LABELS[provider]}`);
|
||||
const result = await this.unrestrictViaProvider(settings, provider, link, signal);
|
||||
let fileName = result.fileName;
|
||||
if (isRapidgatorLink(link) && looksLikeOpaqueFilename(fileName || filenameFromUrl(link))) {
|
||||
@ -2108,6 +2261,12 @@ export class DebridService {
|
||||
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
|
||||
throw error;
|
||||
}
|
||||
const nextProvider = order.slice(order.indexOf(provider) + 1).find((candidate) => this.isProviderSelectableFor(settings, candidate));
|
||||
if (nextProvider) {
|
||||
logger.warn(`Provider-Kette: ${PROVIDER_LABELS[provider]} fehlgeschlagen (${errorText}), Fallback auf ${PROVIDER_LABELS[nextProvider]}`);
|
||||
} else {
|
||||
logger.warn(`Provider-Kette: ${PROVIDER_LABELS[provider]} fehlgeschlagen (${errorText}), kein weiterer Provider verfuegbar`);
|
||||
}
|
||||
attempts.push(`${PROVIDER_LABELS[provider]}: ${compactErrorText(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ import {
|
||||
UiSnapshot
|
||||
} from "../shared/types";
|
||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||
import { addDebridLinkApiKeyDailyUsageBytes, addProviderDailyUsageBytes, getProviderUsageDayKey, isDebridLinkApiKeyDailyLimitReached, isProviderDailyLimitReached } from "../shared/provider-daily-limits";
|
||||
import { addDebridLinkApiKeyDailyUsageBytes, addProviderDailyUsageBytes, getProviderUsageDayKey, 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
|
||||
@ -41,7 +41,7 @@ function releaseTlsSkip(): void {
|
||||
}
|
||||
}
|
||||
import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
|
||||
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo } from "./debrid";
|
||||
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys } from "./debrid";
|
||||
import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree } from "./extractor";
|
||||
import { validateFileAgainstManifest } from "./integrity";
|
||||
import { logger } from "./logger";
|
||||
@ -459,6 +459,7 @@ function toWindowsLongPathIfNeeded(filePath: string): string {
|
||||
const SCENE_RELEASE_FOLDER_RE = /-(?:4sf|4sj)$/i;
|
||||
const SCENE_GROUP_SUFFIX_RE = /-(?=[A-Za-z0-9]{2,}$)(?=[A-Za-z0-9]*[A-Z])[A-Za-z0-9]+$/;
|
||||
const SCENE_EPISODE_RE = /(?:^|[._\-\s])s(\d{1,2})e(\d{1,3})(?:e(\d{1,3}))?(?:[._\-\s]|$)/i;
|
||||
const SCENE_EPISODE_JOINED_RE = /s(\d{1,2})e(\d{1,3})(?:e(\d{1,3}))?(?:[._\-\s]|$)/i;
|
||||
const SCENE_SEASON_ONLY_RE = /(^|[._\-\s])s\d{1,2}(?=[._\-\s]|$)/i;
|
||||
const SCENE_SEASON_CAPTURE_RE = /(?:^|[._\-\s])s(\d{1,2})(?=[._\-\s]|$)/i;
|
||||
const SCENE_EPISODE_ONLY_RE = /(?:^|[._\-\s])e(?:p(?:isode)?)?\s*0*(\d{1,3})(?:[._\-\s]|$)/i;
|
||||
@ -518,7 +519,8 @@ function hasSceneGroupSuffix(fileName: string): boolean {
|
||||
}
|
||||
|
||||
export function extractEpisodeToken(fileName: string): string | null {
|
||||
const match = String(fileName || "").match(SCENE_EPISODE_RE);
|
||||
const text = String(fileName || "");
|
||||
const match = text.match(SCENE_EPISODE_RE) || text.match(SCENE_EPISODE_JOINED_RE);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
@ -4483,7 +4485,7 @@ export class DownloadManager extends EventEmitter {
|
||||
}
|
||||
if (effectiveProvider === "debridlink") {
|
||||
const configuredKeys = parseDebridLinkApiKeys(this.settings.debridLinkApiKeys);
|
||||
return configuredKeys.some((entry) => !isDebridLinkApiKeyDailyLimitReached(this.settings, entry.id));
|
||||
return configuredKeys.length > 0 && getAvailableDebridLinkApiKeys(this.settings).length > 0;
|
||||
}
|
||||
if (provider === "linksnappy") {
|
||||
return Boolean(this.settings.linkSnappyLogin.trim() && this.settings.linkSnappyPassword.trim());
|
||||
|
||||
@ -74,8 +74,8 @@ function isDevMode(): boolean {
|
||||
|
||||
function createWindow(): BrowserWindow {
|
||||
const window = new BrowserWindow({
|
||||
width: 1440,
|
||||
height: 940,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
minWidth: 1120,
|
||||
minHeight: 760,
|
||||
backgroundColor: "#070b14",
|
||||
@ -94,7 +94,7 @@ function createWindow(): BrowserWindow {
|
||||
responseHeaders: {
|
||||
...details.responseHeaders,
|
||||
"Content-Security-Policy": [
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://api.real-debrid.com https://codeberg.org https://bestdebrid.com https://api.alldebrid.com https://www.mega-debrid.eu https://git.24-music.de https://ddownload.com https://ddl.to"
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https://api.real-debrid.com https://codeberg.org https://bestdebrid.com https://api.alldebrid.com https://www.mega-debrid.eu https://git.24-music.de https://ddownload.com https://ddl.to https://debrid-link.com"
|
||||
]
|
||||
}
|
||||
});
|
||||
@ -527,6 +527,10 @@ function registerIpcHandlers(): void {
|
||||
return controller.getAllDebridHostInfo();
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.GET_DEBRIDLINK_HOST_LIMITS, async () => {
|
||||
return controller.getDebridLinkHostLimits();
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.IMPORT_BACKUP, async () => {
|
||||
const options = {
|
||||
properties: ["openFile"] as Array<"openFile">,
|
||||
|
||||
@ -196,6 +196,25 @@ function normalizeNamedByteMap(raw: unknown, allowedKeys: readonly string[]): Re
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeStringList(raw: unknown, allowedKeys: readonly string[]): string[] {
|
||||
if (!Array.isArray(raw)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allowed = new Set(allowedKeys);
|
||||
const seen = new Set<string>();
|
||||
const result: string[] = [];
|
||||
for (const entry of raw) {
|
||||
const normalized = String(entry || "").trim();
|
||||
if (!normalized || !allowed.has(normalized) || seen.has(normalized)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(normalized);
|
||||
result.push(normalized);
|
||||
}
|
||||
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> = {};
|
||||
@ -287,6 +306,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
||||
settings.debridLinkApiKeyDailyUsageBytes,
|
||||
debridLinkApiKeyIds
|
||||
);
|
||||
const debridLinkDisabledKeyIds = normalizeStringList(settings.debridLinkDisabledKeyIds, debridLinkApiKeyIds);
|
||||
const normalized: AppSettings = {
|
||||
token: asText(settings.token),
|
||||
realDebridUseWebLogin: Boolean(settings.realDebridUseWebLogin),
|
||||
@ -303,6 +323,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
||||
ddownloadPassword: asText(settings.ddownloadPassword),
|
||||
oneFichierApiKey: asText(settings.oneFichierApiKey),
|
||||
debridLinkApiKeys: String(settings.debridLinkApiKeys ?? "").replace(/\r\n|\r/g, "\n").trim(),
|
||||
debridLinkDisabledKeyIds,
|
||||
linkSnappyLogin: asText(settings.linkSnappyLogin),
|
||||
linkSnappyPassword: asText(settings.linkSnappyPassword),
|
||||
archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n|\r/g, "\n"),
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
AddLinksPayload,
|
||||
AllDebridHostInfo,
|
||||
AppSettings,
|
||||
DebridLinkHostLimitInfo,
|
||||
DebridProvider,
|
||||
DuplicatePolicy,
|
||||
HistoryEntry,
|
||||
@ -59,6 +60,7 @@ const api: ElectronApi = {
|
||||
openAllDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ALLDEBRID_LOGIN),
|
||||
importBestDebridCookies: (): Promise<number> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES),
|
||||
getAllDebridHostInfo: (): Promise<AllDebridHostInfo> => ipcRenderer.invoke(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO),
|
||||
getDebridLinkHostLimits: (): Promise<DebridLinkHostLimitInfo[]> => ipcRenderer.invoke(IPC_CHANNELS.GET_DEBRIDLINK_HOST_LIMITS),
|
||||
retryExtraction: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId),
|
||||
extractNow: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId),
|
||||
resetPackage: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RESET_PACKAGE, packageId),
|
||||
|
||||
@ -6,6 +6,7 @@ import type {
|
||||
AppTheme,
|
||||
BandwidthScheduleEntry,
|
||||
DebridFallbackProvider,
|
||||
DebridLinkHostLimitInfo,
|
||||
DebridProvider,
|
||||
DownloadItem,
|
||||
DownloadStats,
|
||||
@ -105,7 +106,9 @@ interface AccountDialogState {
|
||||
interface DebridLinkAccountKeyEntry {
|
||||
id: string;
|
||||
label: string;
|
||||
token: string;
|
||||
masked: string;
|
||||
disabled: boolean;
|
||||
dailyUsedBytes: number;
|
||||
dailyLimitBytes: number;
|
||||
dailyRemainingBytes: number | null;
|
||||
@ -691,6 +694,7 @@ const emptyStats = (): DownloadStats => ({
|
||||
const emptySnapshot = (): UiSnapshot => ({
|
||||
settings: {
|
||||
token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaDebridApiEnabled: false, megaDebridWebEnabled: false, megaDebridPreferApi: true, bestToken: "", bestDebridUseWebLogin: false, allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "", debridLinkApiKeys: "", linkSnappyLogin: "", linkSnappyPassword: "",
|
||||
debridLinkDisabledKeyIds: [],
|
||||
archivePasswordList: "",
|
||||
rememberToken: true, providerOrder: [], providerPrimary: "realdebrid", providerSecondary: "none",
|
||||
providerTertiary: "none", autoProviderFallback: true, outputDir: "", packageName: "",
|
||||
@ -875,6 +879,39 @@ function formatAllDebridTimestamp(info: AllDebridHostInfo): string {
|
||||
return formatDateTime(info.lastCheckedAt || info.fetchedAt);
|
||||
}
|
||||
|
||||
function formatDebridLinkTraffic(info: DebridLinkHostLimitInfo | null | undefined): string {
|
||||
if (!info) {
|
||||
return "Lade...";
|
||||
}
|
||||
const toGb = (bytes: number): string => `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
if (info.trafficCurrentBytes !== null && info.trafficMaxBytes !== null) {
|
||||
return `${toGb(info.trafficCurrentBytes)} / ${toGb(info.trafficMaxBytes)}`;
|
||||
}
|
||||
if (info.trafficMaxBytes !== null) {
|
||||
return `max. ${toGb(info.trafficMaxBytes)}`;
|
||||
}
|
||||
if (info.trafficCurrentBytes !== null) {
|
||||
return toGb(info.trafficCurrentBytes);
|
||||
}
|
||||
return info.note || "Nicht verfügbar";
|
||||
}
|
||||
|
||||
function formatDebridLinkCountQuota(info: DebridLinkHostLimitInfo | null | undefined): string {
|
||||
if (!info) {
|
||||
return "Lade...";
|
||||
}
|
||||
if (info.linksCurrent !== null && info.linksMax !== null) {
|
||||
return `${info.linksCurrent} / ${info.linksMax}`;
|
||||
}
|
||||
if (info.linksMax !== null) {
|
||||
return `max. ${info.linksMax}`;
|
||||
}
|
||||
if (info.linksCurrent !== null) {
|
||||
return String(info.linksCurrent);
|
||||
}
|
||||
return info.note || "Nicht verfügbar";
|
||||
}
|
||||
|
||||
interface BandwidthChartProps {
|
||||
items: Record<string, DownloadItem>;
|
||||
running: boolean;
|
||||
@ -1254,6 +1291,10 @@ export function App(): ReactElement {
|
||||
const [linkPopup, setLinkPopup] = useState<LinkPopupState | null>(null);
|
||||
const [accountDialog, setAccountDialog] = useState<AccountDialogState | null>(null);
|
||||
const [accountDialogSearch, setAccountDialogSearch] = useState("");
|
||||
const [keyStatsPopup, setKeyStatsPopup] = useState<string | null>(null);
|
||||
const [debridLinkHostLimits, setDebridLinkHostLimits] = useState<Record<string, DebridLinkHostLimitInfo>>({});
|
||||
const [debridLinkHostLimitsLoading, setDebridLinkHostLimitsLoading] = useState(false);
|
||||
const [debridLinkHostLimitsError, setDebridLinkHostLimitsError] = useState("");
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [deleteConfirm, setDeleteConfirm] = useState<{ ids: Set<string>; dontAsk: boolean } | null>(null);
|
||||
const [columnOrder, setColumnOrder] = useState<string[]>(() => DEFAULT_COLUMN_ORDER);
|
||||
@ -1271,6 +1312,7 @@ export function App(): ReactElement {
|
||||
const [allDebridHostInfo, setAllDebridHostInfo] = useState<AllDebridHostInfo | null>(null);
|
||||
const [allDebridHostLoading, setAllDebridHostLoading] = useState(false);
|
||||
const allDebridHostRequestRef = useRef(0);
|
||||
const debridLinkHostLimitsRequestRef = useRef(0);
|
||||
const accountColumnResizeRef = useRef<{ key: AccountColumnKey; startX: number; startWidth: number } | null>(null);
|
||||
const onAccountColumnResizeMove = useCallback((event: MouseEvent): void => {
|
||||
const active = accountColumnResizeRef.current;
|
||||
@ -1435,6 +1477,149 @@ export function App(): ReactElement {
|
||||
}
|
||||
}, [showToast]);
|
||||
|
||||
const loadDebridLinkHostLimits = useCallback(async (silent = false): Promise<void> => {
|
||||
const requestId = debridLinkHostLimitsRequestRef.current + 1;
|
||||
debridLinkHostLimitsRequestRef.current = requestId;
|
||||
setDebridLinkHostLimitsLoading(true);
|
||||
setDebridLinkHostLimitsError("");
|
||||
setDebridLinkHostLimits({});
|
||||
try {
|
||||
const apiKeys = parseDebridLinkApiKeys(settingsDraft.debridLinkApiKeys || "");
|
||||
if (apiKeys.length === 0) {
|
||||
throw new Error("Debrid-Link ist nicht konfiguriert");
|
||||
}
|
||||
|
||||
let loadedAny = false;
|
||||
let firstError = "";
|
||||
for (let index = 0; index < apiKeys.length; index += 1) {
|
||||
if (!mountedRef.current || debridLinkHostLimitsRequestRef.current !== requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const apiKey = apiKeys[index];
|
||||
const controller = new AbortController();
|
||||
const timer = window.setTimeout(() => controller.abort(), 8000);
|
||||
let info: DebridLinkHostLimitInfo;
|
||||
try {
|
||||
const readLimitsPayload = async (path: "limits" | "limits/all") => {
|
||||
const response = await fetch(`https://debrid-link.com/api/v2/downloader/${path}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey.token}`
|
||||
},
|
||||
signal: controller.signal
|
||||
});
|
||||
const payload = await response.json() as {
|
||||
success?: boolean;
|
||||
value?: {
|
||||
hosters?: Array<{
|
||||
name?: string;
|
||||
displayName?: string;
|
||||
daySize?: { current?: number; value?: number };
|
||||
dayCount?: { current?: number; value?: number };
|
||||
}>;
|
||||
};
|
||||
error?: string;
|
||||
error_description?: string;
|
||||
};
|
||||
if (!response.ok || !payload?.success) {
|
||||
throw new Error(String(payload?.error_description || payload?.error || `HTTP ${response.status}`));
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
|
||||
let payload = await readLimitsPayload("limits/all");
|
||||
let hostEntry = (payload.value?.hosters || []).find((entry) => String(entry.name || "").toLowerCase() === "rapidgator");
|
||||
if (!hostEntry) {
|
||||
payload = await readLimitsPayload("limits");
|
||||
hostEntry = (payload.value?.hosters || []).find((entry) => String(entry.name || "").toLowerCase() === "rapidgator");
|
||||
}
|
||||
if (!hostEntry) {
|
||||
info = {
|
||||
keyId: apiKey.id,
|
||||
keyLabel: apiKey.label,
|
||||
host: "rapidgator",
|
||||
fetchedAt: Date.now(),
|
||||
trafficCurrentBytes: null,
|
||||
trafficMaxBytes: null,
|
||||
linksCurrent: null,
|
||||
linksMax: null,
|
||||
note: "Rapidgator nicht in der API-Antwort gefunden."
|
||||
};
|
||||
} else {
|
||||
info = {
|
||||
keyId: apiKey.id,
|
||||
keyLabel: apiKey.label,
|
||||
host: String(hostEntry.displayName || hostEntry.name || "rapidgator"),
|
||||
fetchedAt: Date.now(),
|
||||
trafficCurrentBytes: typeof hostEntry.daySize?.current === "number" ? hostEntry.daySize.current : null,
|
||||
trafficMaxBytes: typeof hostEntry.daySize?.value === "number" ? hostEntry.daySize.value : null,
|
||||
linksCurrent: typeof hostEntry.dayCount?.current === "number" ? hostEntry.dayCount.current : null,
|
||||
linksMax: typeof hostEntry.dayCount?.value === "number" ? hostEntry.dayCount.value : null,
|
||||
note: ""
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
const message = String(error || "Quota konnte nicht geladen werden");
|
||||
if (!firstError) {
|
||||
firstError = message;
|
||||
}
|
||||
info = {
|
||||
keyId: apiKey.id,
|
||||
keyLabel: apiKey.label,
|
||||
host: "rapidgator",
|
||||
fetchedAt: Date.now(),
|
||||
trafficCurrentBytes: null,
|
||||
trafficMaxBytes: null,
|
||||
linksCurrent: null,
|
||||
linksMax: null,
|
||||
note: message
|
||||
};
|
||||
} finally {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
|
||||
if (!mountedRef.current || debridLinkHostLimitsRequestRef.current !== requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
loadedAny = true;
|
||||
setDebridLinkHostLimits((prev) => ({
|
||||
...prev,
|
||||
[info.keyId]: info
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
if (!loadedAny && firstError) {
|
||||
throw new Error(firstError);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!mountedRef.current || debridLinkHostLimitsRequestRef.current !== requestId) {
|
||||
return;
|
||||
}
|
||||
setDebridLinkHostLimits({});
|
||||
setDebridLinkHostLimitsError(String(error));
|
||||
if (!silent) {
|
||||
showToast(`Debrid-Link Quota fehlgeschlagen: ${String(error)}`, 3200);
|
||||
}
|
||||
} finally {
|
||||
if (mountedRef.current && debridLinkHostLimitsRequestRef.current === requestId) {
|
||||
setDebridLinkHostLimitsLoading(false);
|
||||
}
|
||||
}
|
||||
}, [settingsDraft.debridLinkApiKeys, showToast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (keyStatsPopup !== "debridlink") {
|
||||
setDebridLinkHostLimits({});
|
||||
setDebridLinkHostLimitsError("");
|
||||
setDebridLinkHostLimitsLoading(false);
|
||||
return;
|
||||
}
|
||||
void loadDebridLinkHostLimits(true);
|
||||
}, [keyStatsPopup, loadDebridLinkHostLimits]);
|
||||
|
||||
const clearImportQueueFocusListener = useCallback((): void => {
|
||||
const handler = importQueueFocusHandlerRef.current;
|
||||
if (!handler) {
|
||||
@ -1764,7 +1949,7 @@ export function App(): ReactElement {
|
||||
continue;
|
||||
}
|
||||
const option = findAccountOption(kind);
|
||||
let statusLabel = "Konfiguriert";
|
||||
let statusLabel = "Aktiviert";
|
||||
let note = "";
|
||||
if (kind === "megadebrid-api") {
|
||||
note = "Nur API aktiv. Kein Web-Fallback.";
|
||||
@ -1790,7 +1975,7 @@ export function App(): ReactElement {
|
||||
}
|
||||
if (kind === "debridlink-api") {
|
||||
const keyCount = parseDebridLinkApiKeys(settingsDraft.debridLinkApiKeys || "").length;
|
||||
statusLabel = keyCount > 1 ? `${keyCount} API-Keys` : "Konfiguriert";
|
||||
statusLabel = keyCount > 1 ? `${keyCount} API-Keys` : "Aktiviert";
|
||||
}
|
||||
const provider = getAccountServiceProvider(service);
|
||||
const dailyUsedBytes = getProviderDailyUsageBytes(snapshot.settings, provider);
|
||||
@ -1816,7 +2001,9 @@ export function App(): ReactElement {
|
||||
return {
|
||||
id: key.id,
|
||||
label: key.label,
|
||||
token: key.token,
|
||||
masked: key.masked,
|
||||
disabled: (settingsDraft.debridLinkDisabledKeyIds || []).includes(key.id),
|
||||
dailyUsedBytes: keyDailyUsedBytes,
|
||||
dailyLimitBytes: keyDailyLimitBytes,
|
||||
dailyRemainingBytes: keyDailyRemainingBytes,
|
||||
@ -1826,11 +2013,19 @@ export function App(): ReactElement {
|
||||
: [];
|
||||
if (kind === "debridlink-api" && debridLinkKeys.length > 0) {
|
||||
const limitedCount = debridLinkKeys.filter((entry) => entry.dailyLimitReached).length;
|
||||
const disabledKeyCount = debridLinkKeys.filter((entry) => entry.disabled).length;
|
||||
const keyNotes: string[] = [];
|
||||
if (limitedCount > 0) {
|
||||
const limitNote = `${limitedCount}/${debridLinkKeys.length} API-Keys am Limit.`;
|
||||
note = note ? `${limitNote} ${note}` : limitNote;
|
||||
keyNotes.push(`${limitedCount}/${debridLinkKeys.length} API-Keys am Limit.`);
|
||||
}
|
||||
if (limitedCount === debridLinkKeys.length) {
|
||||
if (disabledKeyCount > 0) {
|
||||
keyNotes.push(`${disabledKeyCount}/${debridLinkKeys.length} API-Keys deaktiviert.`);
|
||||
}
|
||||
if (keyNotes.length > 0) {
|
||||
const combinedKeyNote = keyNotes.join(" ");
|
||||
note = note ? `${combinedKeyNote} ${note}` : combinedKeyNote;
|
||||
}
|
||||
if (debridLinkKeys.every((entry) => entry.disabled || entry.dailyLimitReached)) {
|
||||
dailyLimitReached = true;
|
||||
}
|
||||
}
|
||||
@ -2185,6 +2380,28 @@ export function App(): ReactElement {
|
||||
});
|
||||
};
|
||||
|
||||
const onToggleDebridLinkApiKeyEnabled = async (entry: ConfiguredAccountEntry, key: DebridLinkAccountKeyEntry): Promise<void> => {
|
||||
await performQuickAction(async () => {
|
||||
const currentDisabledIds = settingsDraft.debridLinkDisabledKeyIds || [];
|
||||
const nextDisabledIds = key.disabled
|
||||
? currentDisabledIds.filter((existingId) => existingId !== key.id)
|
||||
: [...currentDisabledIds, key.id];
|
||||
const nextDraft: AppSettings = {
|
||||
...settingsDraft,
|
||||
debridLinkDisabledKeyIds: nextDisabledIds
|
||||
};
|
||||
await persistSpecificSettings(nextDraft);
|
||||
showToast(
|
||||
key.disabled
|
||||
? `${entry.serviceLabel} ${key.label} aktiviert`
|
||||
: `${entry.serviceLabel} ${key.label} deaktiviert`,
|
||||
2200
|
||||
);
|
||||
}, (error) => {
|
||||
showToast(`${entry.serviceLabel} ${key.label}: Umschalten fehlgeschlagen: ${String(error)}`, 3200);
|
||||
});
|
||||
};
|
||||
|
||||
const onAccountRowQuickAction = async (entry: ConfiguredAccountEntry): Promise<void> => {
|
||||
const meta = getAccountQuickActionMeta(entry.kind);
|
||||
if (!meta) {
|
||||
@ -4005,6 +4222,9 @@ export function App(): ReactElement {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="account-header-cell">
|
||||
<span>Info</span>
|
||||
</div>
|
||||
<div className="account-header-cell">
|
||||
<span>Zugang</span>
|
||||
<button
|
||||
@ -4016,7 +4236,9 @@ export function App(): ReactElement {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span>Aktionen</span>
|
||||
<div className="account-header-cell">
|
||||
<span>Aktionen</span>
|
||||
</div>
|
||||
</div>
|
||||
{configuredAccounts.map((entry) => {
|
||||
const option = findAccountOption(entry.kind);
|
||||
@ -4034,46 +4256,21 @@ export function App(): ReactElement {
|
||||
<span className="account-mode-pill">{entry.modeLabel}</span>
|
||||
</div>
|
||||
<div className="account-cell account-status-cell">
|
||||
<span className={`account-status-pill${allDebridStateClass}`}>{entry.statusLabel}</span>
|
||||
<span className={`account-status-pill${entry.disabled ? " account-status-disabled" : ""}${allDebridStateClass}`}>{entry.statusLabel}</span>
|
||||
{entry.note && <span className="account-note">{entry.note}</span>}
|
||||
<div className={`account-usage-stats${entry.dailyLimitReached ? " warning" : ""}`}>
|
||||
<span>Heute: {humanSize(entry.dailyUsedBytes)}</span>
|
||||
<span>{entry.dailyLimitBytes > 0 ? `Limit: ${humanSize(entry.dailyLimitBytes)}` : "Kein Tageslimit"}</span>
|
||||
{entry.dailyLimitBytes > 0 && (
|
||||
<span>{entry.dailyLimitReached ? "Fallback aktiv" : `Rest: ${humanSize(entry.dailyRemainingBytes || 0)}`}</span>
|
||||
)}
|
||||
{entry.dailyLimitBytes <= 0 && entry.dailyLimitReached && entry.debridLinkKeys.length > 0 && (
|
||||
<span>Fallback aktiv</span>
|
||||
)}
|
||||
</div>
|
||||
{entry.debridLinkKeys.length > 0 && (
|
||||
<div className="account-subkey-list">
|
||||
{entry.debridLinkKeys.map((key) => (
|
||||
<div key={key.id} className={`account-subkey-row${key.dailyLimitReached ? " warning" : ""}`}>
|
||||
<div className="account-subkey-main">
|
||||
<div className="account-subkey-head">
|
||||
<strong>{key.label}</strong>
|
||||
<span>{key.masked}</span>
|
||||
</div>
|
||||
<div className="account-subkey-stats">
|
||||
<span>Heute: {humanSize(key.dailyUsedBytes)}</span>
|
||||
<span>{key.dailyLimitBytes > 0 ? `Limit: ${humanSize(key.dailyLimitBytes)}` : "Kein Limit"}</span>
|
||||
{key.dailyLimitBytes > 0 && (
|
||||
<span>{key.dailyLimitReached ? "Fallback aktiv" : `Rest: ${humanSize(key.dailyRemainingBytes || 0)}`}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="account-subkey-actions">
|
||||
<button
|
||||
className="btn"
|
||||
disabled={actionBusy || key.dailyUsedBytes <= 0}
|
||||
onClick={() => { void onResetDebridLinkApiKeyDailyUsage(entry, key.id, key.label); }}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="account-cell account-info-cell">
|
||||
{entry.debridLinkKeys.length > 0 ? (
|
||||
<button className="btn btn-sm" onClick={() => setKeyStatsPopup(entry.service)}>
|
||||
Statistik
|
||||
</button>
|
||||
) : (
|
||||
<div className={`account-usage-stats${entry.dailyLimitReached ? " warning" : ""}`}>
|
||||
<span>Heute: {humanSize(entry.dailyUsedBytes)}</span>
|
||||
<span>{entry.dailyLimitBytes > 0 ? `Limit: ${humanSize(entry.dailyLimitBytes)}` : "Kein Tageslimit"}</span>
|
||||
{entry.dailyLimitBytes > 0 && (
|
||||
<span>{entry.dailyLimitReached ? "Fallback aktiv" : `Rest: ${humanSize(entry.dailyRemainingBytes || 0)}`}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -5016,6 +5213,91 @@ export function App(): ReactElement {
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{keyStatsPopup && (() => {
|
||||
const entry = configuredAccounts.find((a) => a.service === keyStatsPopup);
|
||||
if (!entry || entry.debridLinkKeys.length === 0) return null;
|
||||
const totalUsed = entry.debridLinkKeys.reduce((s, k) => s + k.dailyUsedBytes, 0);
|
||||
const limitedCount = entry.debridLinkKeys.filter((k) => k.dailyLimitReached).length;
|
||||
const disabledCount = entry.debridLinkKeys.filter((k) => k.disabled).length;
|
||||
const loadedQuotaCount = entry.debridLinkKeys.filter((k) => Boolean(debridLinkHostLimits[k.id])).length;
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={() => setKeyStatsPopup(null)}>
|
||||
<div className="modal-card key-stats-popup" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="key-stats-popup-header">
|
||||
<div>
|
||||
<h3>API-Key Statistik</h3>
|
||||
<p className="key-stats-summary">
|
||||
{entry.debridLinkKeys.length} Keys · Heute: {humanSize(totalUsed)}
|
||||
{limitedCount > 0 && <span className="key-stats-warn"> · {limitedCount} am Limit</span>}
|
||||
{disabledCount > 0 && <span className="key-stats-warn"> · {disabledCount} deaktiviert</span>}
|
||||
{debridLinkHostLimitsLoading && <span> · Rapidgator-Quota wird geladen ({loadedQuotaCount}/{entry.debridLinkKeys.length})</span>}
|
||||
{!debridLinkHostLimitsLoading && !debridLinkHostLimitsError && <span> · Rapidgator API-Quota</span>}
|
||||
{debridLinkHostLimitsError && <span className="key-stats-warn"> · API-Quota konnte nicht geladen werden</span>}
|
||||
</p>
|
||||
</div>
|
||||
<button className="update-popup-close" onClick={() => setKeyStatsPopup(null)}>×</button>
|
||||
</div>
|
||||
<div className="account-subkey-table">
|
||||
<div className="account-subkey-table-head">
|
||||
<span className="col-key">#</span>
|
||||
<span className="col-masked">Key</span>
|
||||
<span className="col-usage">Heute</span>
|
||||
<span className="col-limit">Lokal</span>
|
||||
<span className="col-traffic">RG Traffic</span>
|
||||
<span className="col-links">RG Links</span>
|
||||
<span className="col-action"></span>
|
||||
</div>
|
||||
{entry.debridLinkKeys.map((key, ki) => (
|
||||
<div key={key.id} className={`account-subkey-table-row${key.dailyLimitReached ? " warning" : ""}${key.disabled ? " disabled" : ""}`}>
|
||||
{(() => {
|
||||
const hostInfo = debridLinkHostLimits[key.id];
|
||||
return (
|
||||
<>
|
||||
<span className="col-key">{ki + 1}</span>
|
||||
<span
|
||||
className="col-masked link-popup-click"
|
||||
title={`${key.masked}\nKlicken zum Kopieren`}
|
||||
onClick={() => {
|
||||
void navigator.clipboard.writeText(key.token)
|
||||
.then(() => showToast(`${key.label} kopiert`, 1800))
|
||||
.catch(() => showToast("Kopieren fehlgeschlagen", 2200));
|
||||
}}
|
||||
>
|
||||
{key.masked}
|
||||
</span>
|
||||
<span className="col-usage">{humanSize(key.dailyUsedBytes)}</span>
|
||||
<span className="col-limit">{key.disabled ? "Deaktiviert" : key.dailyLimitBytes > 0 ? humanSize(key.dailyLimitBytes) : "Kein Limit"}</span>
|
||||
<span className="col-traffic" title={hostInfo?.note || ""}>{formatDebridLinkTraffic(hostInfo)}</span>
|
||||
<span className="col-links" title={hostInfo?.note || ""}>{formatDebridLinkCountQuota(hostInfo)}</span>
|
||||
<span className="col-action">
|
||||
<button
|
||||
className={`btn btn-sm ${key.disabled ? "success" : "danger"}`}
|
||||
disabled={actionBusy}
|
||||
onClick={() => { void onToggleDebridLinkApiKeyEnabled(entry, key); }}
|
||||
>
|
||||
{key.disabled ? "Aktivieren" : "Deaktivieren"}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm"
|
||||
disabled={actionBusy || key.dailyUsedBytes <= 0}
|
||||
onClick={() => { void onResetDebridLinkApiKeyDailyUsage(entry, key.id, key.label); }}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button className="btn" onClick={() => setKeyStatsPopup(null)}>Schließen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{linkPopup && (
|
||||
<div className="modal-backdrop" onClick={() => setLinkPopup(null)}>
|
||||
<div className="modal-card link-popup" onClick={(e) => e.stopPropagation()}>
|
||||
|
||||
@ -482,11 +482,21 @@ body,
|
||||
color: #fda4af;
|
||||
}
|
||||
|
||||
.btn.success {
|
||||
border-color: rgba(74, 222, 128, 0.7);
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .btn.danger {
|
||||
border-color: color-mix(in srgb, var(--danger) 60%, transparent);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
:root[data-theme="light"] .btn.success {
|
||||
border-color: color-mix(in srgb, #16a34a 60%, transparent);
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.btn.btn-active {
|
||||
border-color: var(--accent);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
|
||||
@ -1198,7 +1208,9 @@ body,
|
||||
--account-col-service: 220px;
|
||||
--account-col-mode: 96px;
|
||||
--account-col-status: 300px;
|
||||
--account-col-info: 220px;
|
||||
--account-col-secret: 180px;
|
||||
--account-col-actions: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -1206,30 +1218,41 @@ body,
|
||||
.account-row {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(180px, var(--account-col-service))
|
||||
minmax(80px, var(--account-col-mode))
|
||||
minmax(180px, var(--account-col-status))
|
||||
minmax(120px, var(--account-col-secret))
|
||||
minmax(260px, 1fr);
|
||||
minmax(130px, var(--account-col-service))
|
||||
minmax(72px, var(--account-col-mode))
|
||||
minmax(140px, var(--account-col-status))
|
||||
minmax(160px, var(--account-col-info))
|
||||
minmax(180px, var(--account-col-secret))
|
||||
minmax(280px, var(--account-col-actions));
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.account-table-head {
|
||||
padding: 0 4px;
|
||||
padding: 6px 12px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.account-table-head > *:nth-child(n+2) {
|
||||
border-left: 2px solid color-mix(in srgb, var(--border) 60%, transparent);
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.account-header-cell {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.account-header-cell > span {
|
||||
@ -1248,9 +1271,11 @@ body,
|
||||
background: transparent;
|
||||
cursor: col-resize;
|
||||
padding: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.account-resize-handle::after {
|
||||
.account-resize-handle:hover::after,
|
||||
.account-resize-handle:focus-visible::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
@ -1259,17 +1284,11 @@ body,
|
||||
width: 2px;
|
||||
transform: translateX(-50%);
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--border) 92%, transparent);
|
||||
transition: background 0.12s ease;
|
||||
}
|
||||
|
||||
.account-resize-handle:hover::after,
|
||||
.account-resize-handle:focus-visible::after {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.account-row {
|
||||
padding: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--border) 88%, transparent);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, color-mix(in srgb, var(--card) 97%, transparent), color-mix(in srgb, var(--surface) 96%, transparent));
|
||||
@ -1278,32 +1297,74 @@ body,
|
||||
.account-cell {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.account-service-cell,
|
||||
.account-status-cell {
|
||||
.account-row > .account-cell:nth-child(n+2) {
|
||||
border-left: 2px solid color-mix(in srgb, var(--border) 60%, transparent);
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.account-row > .account-cell.account-row-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.account-service-cell {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
justify-items: center;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.account-status-cell {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px 8px;
|
||||
}
|
||||
|
||||
.account-info-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.account-service-cell strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.account-service-cell span,
|
||||
.account-note {
|
||||
.account-service-cell span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.account-note {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.account-usage-stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 10px;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.account-usage-stats span + span::before {
|
||||
content: "·";
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.account-usage-stats.warning {
|
||||
@ -1335,6 +1396,12 @@ body,
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.account-status-pill.account-status-disabled {
|
||||
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
||||
border-color: color-mix(in srgb, var(--danger) 40%, transparent);
|
||||
color: color-mix(in srgb, var(--danger) 75%, var(--text));
|
||||
}
|
||||
|
||||
.account-status-pill.account-status-down {
|
||||
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
||||
border-color: color-mix(in srgb, var(--danger) 40%, transparent);
|
||||
@ -1349,7 +1416,7 @@ body,
|
||||
.account-secret {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
border-radius: 12px;
|
||||
@ -1366,91 +1433,178 @@ body,
|
||||
|
||||
.account-row-actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: nowrap;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.account-row-actions .btn {
|
||||
padding: 5px 8px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
min-width: 82px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.account-row-disabled {
|
||||
opacity: 0.45;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.account-row-disabled .account-row-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.account-subkey-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.account-subkey-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 7px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid color-mix(in srgb, var(--border) 72%, transparent);
|
||||
background: color-mix(in srgb, var(--field) 72%, transparent);
|
||||
}
|
||||
|
||||
.account-subkey-row.warning {
|
||||
border-color: color-mix(in srgb, #f59e0b 42%, transparent);
|
||||
}
|
||||
|
||||
.account-subkey-main {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.account-subkey-head {
|
||||
.key-stats-popup {
|
||||
width: min(1360px, calc(100vw - 20px));
|
||||
max-width: min(1360px, calc(100vw - 20px));
|
||||
max-height: calc(100vh - 24px);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.account-subkey-head strong {
|
||||
.modal-card.key-stats-popup {
|
||||
width: min(1360px, calc(100vw - 20px));
|
||||
max-width: min(1360px, calc(100vw - 20px));
|
||||
}
|
||||
|
||||
.key-stats-popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.key-stats-popup-header h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.key-stats-summary {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
flex: 0 0 auto;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.account-subkey-head span {
|
||||
color: var(--muted);
|
||||
.key-stats-warn {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.key-stats-popup .account-subkey-table {
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 140px);
|
||||
}
|
||||
|
||||
.account-subkey-table {
|
||||
margin-top: 6px;
|
||||
border: 1px solid color-mix(in srgb, var(--border) 72%, transparent);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.account-subkey-table-head,
|
||||
.account-subkey-table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 24px minmax(340px, 1fr) 72px 96px 180px 110px 168px;
|
||||
align-items: center;
|
||||
padding: 3px 8px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.account-subkey-table-head {
|
||||
background: color-mix(in srgb, var(--field) 90%, transparent);
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.account-subkey-table-head .col-usage,
|
||||
.account-subkey-table-head .col-limit,
|
||||
.account-subkey-table-head .col-traffic,
|
||||
.account-subkey-table-head .col-links,
|
||||
.account-subkey-table-row .col-usage,
|
||||
.account-subkey-table-row .col-limit,
|
||||
.account-subkey-table-row .col-traffic,
|
||||
.account-subkey-table-row .col-links {
|
||||
text-align: right;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.account-subkey-table-row {
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--border) 30%, transparent);
|
||||
}
|
||||
|
||||
.account-subkey-table-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.account-subkey-table-row.warning {
|
||||
background: color-mix(in srgb, #f59e0b 8%, transparent);
|
||||
}
|
||||
|
||||
.account-subkey-table-row.disabled {
|
||||
background: color-mix(in srgb, var(--field) 45%, transparent);
|
||||
}
|
||||
|
||||
.account-subkey-table-row .col-key {
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.account-subkey-table-row .col-masked {
|
||||
font-family: "JetBrains Mono", "Consolas", monospace;
|
||||
color: var(--muted);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.account-subkey-stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 10px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
.account-subkey-table-row .col-masked.link-popup-click {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.account-subkey-actions {
|
||||
.account-subkey-table-row .col-usage {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.account-subkey-table-row .col-limit {
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.account-subkey-table-row .col-traffic,
|
||||
.account-subkey-table-row .col-links {
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.account-subkey-table-row .col-action {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.account-subkey-actions .btn {
|
||||
padding: 4px 8px;
|
||||
.account-subkey-table-row .col-action .btn {
|
||||
padding: 1px 6px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 3px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@ -2708,7 +2862,13 @@ td {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.account-subkey-row,
|
||||
.account-subkey-table-head,
|
||||
.account-subkey-table-row {
|
||||
grid-template-columns: 24px minmax(0, 1fr) 64px 56px 44px;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.account-dl-key-limit-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@ -40,6 +40,7 @@ export const IPC_CHANNELS = {
|
||||
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",
|
||||
RETRY_EXTRACTION: "queue:retry-extraction",
|
||||
EXTRACT_NOW: "queue:extract-now",
|
||||
RESET_PACKAGE: "queue:reset-package",
|
||||
|
||||
@ -2,6 +2,7 @@ import type {
|
||||
AddLinksPayload,
|
||||
AllDebridHostInfo,
|
||||
AppSettings,
|
||||
DebridLinkHostLimitInfo,
|
||||
DebridProvider,
|
||||
DuplicatePolicy,
|
||||
HistoryEntry,
|
||||
@ -54,6 +55,7 @@ export interface ElectronApi {
|
||||
openAllDebridLogin: () => Promise<void>;
|
||||
importBestDebridCookies: () => Promise<number>;
|
||||
getAllDebridHostInfo: () => Promise<AllDebridHostInfo>;
|
||||
getDebridLinkHostLimits: () => Promise<DebridLinkHostLimitInfo[]>;
|
||||
retryExtraction: (packageId: string) => Promise<void>;
|
||||
extractNow: (packageId: string) => Promise<void>;
|
||||
resetPackage: (packageId: string) => Promise<void>;
|
||||
|
||||
@ -62,6 +62,7 @@ export interface AppSettings {
|
||||
ddownloadPassword: string;
|
||||
oneFichierApiKey: string;
|
||||
debridLinkApiKeys: string;
|
||||
debridLinkDisabledKeyIds: string[];
|
||||
linkSnappyLogin: string;
|
||||
linkSnappyPassword: string;
|
||||
archivePasswordList: string;
|
||||
@ -290,6 +291,18 @@ export interface AllDebridHostInfo {
|
||||
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;
|
||||
}
|
||||
|
||||
export interface ParsedHashEntry {
|
||||
fileName: string;
|
||||
algorithm: "crc32" | "md5" | "sha1";
|
||||
|
||||
@ -78,6 +78,10 @@ describe("extractEpisodeToken", () => {
|
||||
it("extracts double episode with single-digit numbers", () => {
|
||||
expect(extractEpisodeToken("show-s1e1e2-720p")).toBe("S01E01E02");
|
||||
});
|
||||
|
||||
it("extracts episode when title and season token are joined", () => {
|
||||
expect(extractEpisodeToken("mdgp-carters02e01-720p")).toBe("S02E01");
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyEpisodeTokenToFolderName", () => {
|
||||
@ -691,4 +695,13 @@ describe("buildAutoRenameBaseNameFromFolders", () => {
|
||||
);
|
||||
expect(result).toBe("Room.104.S04E01.GERMAN.DL.720p.WEBRiP.x264-LAW");
|
||||
});
|
||||
|
||||
it("renames Carter when source joins title and season token", () => {
|
||||
const result = buildAutoRenameBaseNameFromFoldersWithOptions(
|
||||
["Carter.S02.GERMAN.DL.720p.HDTV.x264-MDGP"],
|
||||
"mdgp-carters02e01-720p",
|
||||
{ forceEpisodeForSeasonFolder: true }
|
||||
);
|
||||
expect(result).toBe("Carter.S02E01.GERMAN.DL.720p.HDTV.x264-MDGP");
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { defaultSettings, REQUEST_RETRIES } from "../src/main/constants";
|
||||
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
||||
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
||||
import { DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, filenameFromRapidgatorUrlPath, normalizeResolvedFilename } from "../src/main/debrid";
|
||||
import { DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, normalizeResolvedFilename } from "../src/main/debrid";
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
@ -379,6 +379,39 @@ describe("debrid service", () => {
|
||||
expect(info.limitSimuDl).toBe(2);
|
||||
});
|
||||
|
||||
it("loads Debrid-Link rapidgator limits per api key", async () => {
|
||||
globalThis.fetch = (async (input: RequestInfo | URL): Promise<Response> => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
if (url.includes("debrid-link.com/api/v2/downloader/limits/all")) {
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
value: {
|
||||
hosters: [
|
||||
{
|
||||
name: "rapidgator",
|
||||
daySize: { current: 0, value: 150323855360 },
|
||||
dayCount: { current: 0, value: 500 }
|
||||
}
|
||||
]
|
||||
}
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
return new Response("not-found", { status: 404 });
|
||||
}) as typeof fetch;
|
||||
|
||||
const info = await fetchDebridLinkHostLimits("key-a", "rapidgator");
|
||||
expect(info).toHaveLength(1);
|
||||
expect(info[0].keyLabel).toBe("Key 1");
|
||||
expect(info[0].host).toBe("rapidgator");
|
||||
expect(info[0].trafficCurrentBytes).toBe(0);
|
||||
expect(info[0].trafficMaxBytes).toBe(150323855360);
|
||||
expect(info[0].linksCurrent).toBe(0);
|
||||
expect(info[0].linksMax).toBe(500);
|
||||
});
|
||||
|
||||
it("uses AllDebrid web path when enabled", async () => {
|
||||
const settings = {
|
||||
...defaultSettings(),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user