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",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.6.90",
|
"version": "1.7.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.6.90",
|
"version": "1.7.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.7.0",
|
"version": "1.7.1",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import { resetDebridLinkApiKeyDailyUsage, resetProviderDailyUsage } from "../sha
|
|||||||
import { importDlcContainers } from "./container";
|
import { importDlcContainers } from "./container";
|
||||||
import { APP_VERSION } from "./constants";
|
import { APP_VERSION } from "./constants";
|
||||||
import { DownloadManager } from "./download-manager";
|
import { DownloadManager } from "./download-manager";
|
||||||
import { fetchAllDebridHostInfo } from "./debrid";
|
import { fetchAllDebridHostInfo, fetchDebridLinkHostLimits } from "./debrid";
|
||||||
import { parseCollectorInput } from "./link-parser";
|
import { parseCollectorInput } from "./link-parser";
|
||||||
import { configureLogger, getLogFilePath, logger } from "./logger";
|
import { configureLogger, getLogFilePath, logger } from "./logger";
|
||||||
import { AllDebridWebFallback } from "./all-debrid-web";
|
import { AllDebridWebFallback } from "./all-debrid-web";
|
||||||
@ -248,6 +248,10 @@ export class AppController {
|
|||||||
return fetchAllDebridHostInfo(token, host);
|
return fetchAllDebridHostInfo(token, host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getDebridLinkHostLimits(host = "rapidgator") {
|
||||||
|
return fetchDebridLinkHostLimits(this.settings.debridLinkApiKeys, host);
|
||||||
|
}
|
||||||
|
|
||||||
public async checkUpdates(): Promise<UpdateCheckResult> {
|
public async checkUpdates(): Promise<UpdateCheckResult> {
|
||||||
const result = await checkGitHubUpdate(this.settings.updateRepo);
|
const result = await checkGitHubUpdate(this.settings.updateRepo);
|
||||||
if (!result.error) {
|
if (!result.error) {
|
||||||
|
|||||||
@ -56,6 +56,7 @@ export function defaultSettings(): AppSettings {
|
|||||||
ddownloadPassword: "",
|
ddownloadPassword: "",
|
||||||
oneFichierApiKey: "",
|
oneFichierApiKey: "",
|
||||||
debridLinkApiKeys: "",
|
debridLinkApiKeys: "",
|
||||||
|
debridLinkDisabledKeyIds: [],
|
||||||
linkSnappyLogin: "",
|
linkSnappyLogin: "",
|
||||||
linkSnappyPassword: "",
|
linkSnappyPassword: "",
|
||||||
archivePasswordList: "",
|
archivePasswordList: "",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
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 { isDebridLinkApiKeyDailyLimitReached, isProviderDailyLimitReached } from "../shared/provider-daily-limits";
|
||||||
import { APP_VERSION, REQUEST_RETRIES } from "./constants";
|
import { APP_VERSION, REQUEST_RETRIES } from "./constants";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
@ -68,6 +68,7 @@ function cloneSettings(settings: AppSettings): AppSettings {
|
|||||||
return {
|
return {
|
||||||
...settings,
|
...settings,
|
||||||
bandwidthSchedules: (settings.bandwidthSchedules || []).map((entry) => ({ ...entry })),
|
bandwidthSchedules: (settings.bandwidthSchedules || []).map((entry) => ({ ...entry })),
|
||||||
|
debridLinkDisabledKeyIds: [...(settings.debridLinkDisabledKeyIds || [])],
|
||||||
providerDailyLimitBytes: { ...(settings.providerDailyLimitBytes || {}) },
|
providerDailyLimitBytes: { ...(settings.providerDailyLimitBytes || {}) },
|
||||||
providerDailyUsageBytes: { ...(settings.providerDailyUsageBytes || {}) },
|
providerDailyUsageBytes: { ...(settings.providerDailyUsageBytes || {}) },
|
||||||
debridLinkApiKeyDailyLimitBytes: { ...(settings.debridLinkApiKeyDailyLimitBytes || {}) },
|
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(
|
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";
|
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[] {
|
function uniqueProviderOrder(order: DebridProvider[]): DebridProvider[] {
|
||||||
const seen = new Set<DebridProvider>();
|
const seen = new Set<DebridProvider>();
|
||||||
const result: DebridProvider[] = [];
|
const result: DebridProvider[] = [];
|
||||||
@ -1314,6 +1439,19 @@ export async function fetchAllDebridHostInfo(token: string, host = "rapidgator",
|
|||||||
return new AllDebridClient(token).getHostInfo(host, signal);
|
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 ──
|
// ── Debrid-Link Client ──
|
||||||
|
|
||||||
class DebridLinkClient {
|
class DebridLinkClient {
|
||||||
@ -1330,7 +1468,7 @@ class DebridLinkClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (getAvailableDebridLinkApiKeys(settings).length === 0) {
|
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;
|
let checkedKeys = 0;
|
||||||
@ -1338,7 +1476,13 @@ class DebridLinkClient {
|
|||||||
const apiKey = this.apiKeys[this.currentKeyIndex];
|
const apiKey = this.apiKeys[this.currentKeyIndex];
|
||||||
checkedKeys += 1;
|
checkedKeys += 1;
|
||||||
const keyLabel = this.apiKeys.length > 1 ? ` (${apiKey.label})` : "";
|
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)) {
|
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;
|
this.currentKeyIndex = (this.currentKeyIndex + 1) % this.apiKeys.length;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -1364,6 +1508,7 @@ class DebridLinkClient {
|
|||||||
const errorDesc = String(json.error_description || json.error || "Unbekannter Debrid-Link-Fehler");
|
const errorDesc = String(json.error_description || json.error || "Unbekannter Debrid-Link-Fehler");
|
||||||
|
|
||||||
if (DEBRID_LINK_QUOTA_ERRORS.has(errorCode)) {
|
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}`);
|
logger.warn(`Debrid-Link Quota erreicht${keyLabel}: ${errorCode} – ${errorDesc}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1411,6 +1556,7 @@ class DebridLinkClient {
|
|||||||
if (/Ungültig|abgelaufen/i.test(lastError)) {
|
if (/Ungültig|abgelaufen/i.test(lastError)) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
logger.warn(`Debrid-Link${keyLabel}: Fehler bei Unrestrict-Versuch ${attempt}/${REQUEST_RETRIES}: ${lastError}`);
|
||||||
if (attempt < REQUEST_RETRIES) {
|
if (attempt < REQUEST_RETRIES) {
|
||||||
await sleep(retryDelay(attempt), signal);
|
await sleep(retryDelay(attempt), signal);
|
||||||
}
|
}
|
||||||
@ -1418,9 +1564,14 @@ class DebridLinkClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.currentKeyIndex = (this.currentKeyIndex + 1) % this.apiKeys.length;
|
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 {
|
private formatProviderLimitMessage(settings: AppSettings, provider: DebridProvider): string {
|
||||||
const effectiveProvider = resolveMegaDebridProvider(settings, provider);
|
const effectiveProvider = resolveMegaDebridProvider(settings, provider);
|
||||||
if (effectiveProvider === "debridlink" && parseDebridLinkApiKeys(settings.debridLinkApiKeys).length > 0 && getAvailableDebridLinkApiKeys(settings).length === 0) {
|
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`;
|
return `${PROVIDER_LABELS[effectiveProvider]} Tageslimit erreicht`;
|
||||||
}
|
}
|
||||||
@ -2084,11 +2235,13 @@ export class DebridService {
|
|||||||
configuredFound = true;
|
configuredFound = true;
|
||||||
if (this.isProviderDailyLimited(settings, provider)) {
|
if (this.isProviderDailyLimited(settings, provider)) {
|
||||||
limitReachedFound = true;
|
limitReachedFound = true;
|
||||||
|
logger.info(`Provider-Kette: ${PROVIDER_LABELS[provider]} uebersprungen (${this.formatProviderLimitMessage(settings, provider)})`);
|
||||||
attempts.push(this.formatProviderLimitMessage(settings, provider));
|
attempts.push(this.formatProviderLimitMessage(settings, provider));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
logger.info(`Provider-Kette: versuche ${PROVIDER_LABELS[provider]}`);
|
||||||
const result = await this.unrestrictViaProvider(settings, provider, link, signal);
|
const result = await this.unrestrictViaProvider(settings, provider, link, signal);
|
||||||
let fileName = result.fileName;
|
let fileName = result.fileName;
|
||||||
if (isRapidgatorLink(link) && looksLikeOpaqueFilename(fileName || filenameFromUrl(link))) {
|
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))) {
|
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
|
||||||
throw error;
|
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)}`);
|
attempts.push(`${PROVIDER_LABELS[provider]}: ${compactErrorText(error)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import {
|
|||||||
UiSnapshot
|
UiSnapshot
|
||||||
} from "../shared/types";
|
} from "../shared/types";
|
||||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
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";
|
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
|
// 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 { 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 { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree } from "./extractor";
|
||||||
import { validateFileAgainstManifest } from "./integrity";
|
import { validateFileAgainstManifest } from "./integrity";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
@ -459,6 +459,7 @@ function toWindowsLongPathIfNeeded(filePath: string): string {
|
|||||||
const SCENE_RELEASE_FOLDER_RE = /-(?:4sf|4sj)$/i;
|
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_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_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_ONLY_RE = /(^|[._\-\s])s\d{1,2}(?=[._\-\s]|$)/i;
|
||||||
const SCENE_SEASON_CAPTURE_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;
|
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 {
|
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) {
|
if (!match) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -4483,7 +4485,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
if (effectiveProvider === "debridlink") {
|
if (effectiveProvider === "debridlink") {
|
||||||
const configuredKeys = parseDebridLinkApiKeys(this.settings.debridLinkApiKeys);
|
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") {
|
if (provider === "linksnappy") {
|
||||||
return Boolean(this.settings.linkSnappyLogin.trim() && this.settings.linkSnappyPassword.trim());
|
return Boolean(this.settings.linkSnappyLogin.trim() && this.settings.linkSnappyPassword.trim());
|
||||||
|
|||||||
@ -74,8 +74,8 @@ function isDevMode(): boolean {
|
|||||||
|
|
||||||
function createWindow(): BrowserWindow {
|
function createWindow(): BrowserWindow {
|
||||||
const window = new BrowserWindow({
|
const window = new BrowserWindow({
|
||||||
width: 1440,
|
width: 1920,
|
||||||
height: 940,
|
height: 1080,
|
||||||
minWidth: 1120,
|
minWidth: 1120,
|
||||||
minHeight: 760,
|
minHeight: 760,
|
||||||
backgroundColor: "#070b14",
|
backgroundColor: "#070b14",
|
||||||
@ -94,7 +94,7 @@ function createWindow(): BrowserWindow {
|
|||||||
responseHeaders: {
|
responseHeaders: {
|
||||||
...details.responseHeaders,
|
...details.responseHeaders,
|
||||||
"Content-Security-Policy": [
|
"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();
|
return controller.getAllDebridHostInfo();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.GET_DEBRIDLINK_HOST_LIMITS, async () => {
|
||||||
|
return controller.getDebridLinkHostLimits();
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.IMPORT_BACKUP, async () => {
|
ipcMain.handle(IPC_CHANNELS.IMPORT_BACKUP, async () => {
|
||||||
const options = {
|
const options = {
|
||||||
properties: ["openFile"] as Array<"openFile">,
|
properties: ["openFile"] as Array<"openFile">,
|
||||||
|
|||||||
@ -196,6 +196,25 @@ function normalizeNamedByteMap(raw: unknown, allowedKeys: readonly string[]): Re
|
|||||||
return result;
|
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> {
|
function normalizeHosterRouting(raw: unknown, megaDebridPreferApi: boolean, megaDebridApiEnabled: boolean, megaDebridWebEnabled: boolean): Record<string, DebridProvider> {
|
||||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
|
||||||
const result: Record<string, DebridProvider> = {};
|
const result: Record<string, DebridProvider> = {};
|
||||||
@ -287,6 +306,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
|||||||
settings.debridLinkApiKeyDailyUsageBytes,
|
settings.debridLinkApiKeyDailyUsageBytes,
|
||||||
debridLinkApiKeyIds
|
debridLinkApiKeyIds
|
||||||
);
|
);
|
||||||
|
const debridLinkDisabledKeyIds = normalizeStringList(settings.debridLinkDisabledKeyIds, debridLinkApiKeyIds);
|
||||||
const normalized: AppSettings = {
|
const normalized: AppSettings = {
|
||||||
token: asText(settings.token),
|
token: asText(settings.token),
|
||||||
realDebridUseWebLogin: Boolean(settings.realDebridUseWebLogin),
|
realDebridUseWebLogin: Boolean(settings.realDebridUseWebLogin),
|
||||||
@ -303,6 +323,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
|||||||
ddownloadPassword: asText(settings.ddownloadPassword),
|
ddownloadPassword: asText(settings.ddownloadPassword),
|
||||||
oneFichierApiKey: asText(settings.oneFichierApiKey),
|
oneFichierApiKey: asText(settings.oneFichierApiKey),
|
||||||
debridLinkApiKeys: String(settings.debridLinkApiKeys ?? "").replace(/\r\n|\r/g, "\n").trim(),
|
debridLinkApiKeys: String(settings.debridLinkApiKeys ?? "").replace(/\r\n|\r/g, "\n").trim(),
|
||||||
|
debridLinkDisabledKeyIds,
|
||||||
linkSnappyLogin: asText(settings.linkSnappyLogin),
|
linkSnappyLogin: asText(settings.linkSnappyLogin),
|
||||||
linkSnappyPassword: asText(settings.linkSnappyPassword),
|
linkSnappyPassword: asText(settings.linkSnappyPassword),
|
||||||
archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n|\r/g, "\n"),
|
archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n|\r/g, "\n"),
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import {
|
|||||||
AddLinksPayload,
|
AddLinksPayload,
|
||||||
AllDebridHostInfo,
|
AllDebridHostInfo,
|
||||||
AppSettings,
|
AppSettings,
|
||||||
|
DebridLinkHostLimitInfo,
|
||||||
DebridProvider,
|
DebridProvider,
|
||||||
DuplicatePolicy,
|
DuplicatePolicy,
|
||||||
HistoryEntry,
|
HistoryEntry,
|
||||||
@ -59,6 +60,7 @@ const api: ElectronApi = {
|
|||||||
openAllDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ALLDEBRID_LOGIN),
|
openAllDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ALLDEBRID_LOGIN),
|
||||||
importBestDebridCookies: (): Promise<number> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES),
|
importBestDebridCookies: (): Promise<number> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES),
|
||||||
getAllDebridHostInfo: (): Promise<AllDebridHostInfo> => ipcRenderer.invoke(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO),
|
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),
|
retryExtraction: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId),
|
||||||
extractNow: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, 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),
|
resetPackage: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RESET_PACKAGE, packageId),
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import type {
|
|||||||
AppTheme,
|
AppTheme,
|
||||||
BandwidthScheduleEntry,
|
BandwidthScheduleEntry,
|
||||||
DebridFallbackProvider,
|
DebridFallbackProvider,
|
||||||
|
DebridLinkHostLimitInfo,
|
||||||
DebridProvider,
|
DebridProvider,
|
||||||
DownloadItem,
|
DownloadItem,
|
||||||
DownloadStats,
|
DownloadStats,
|
||||||
@ -105,7 +106,9 @@ interface AccountDialogState {
|
|||||||
interface DebridLinkAccountKeyEntry {
|
interface DebridLinkAccountKeyEntry {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
token: string;
|
||||||
masked: string;
|
masked: string;
|
||||||
|
disabled: boolean;
|
||||||
dailyUsedBytes: number;
|
dailyUsedBytes: number;
|
||||||
dailyLimitBytes: number;
|
dailyLimitBytes: number;
|
||||||
dailyRemainingBytes: number | null;
|
dailyRemainingBytes: number | null;
|
||||||
@ -691,6 +694,7 @@ const emptyStats = (): DownloadStats => ({
|
|||||||
const emptySnapshot = (): UiSnapshot => ({
|
const emptySnapshot = (): UiSnapshot => ({
|
||||||
settings: {
|
settings: {
|
||||||
token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaDebridApiEnabled: false, megaDebridWebEnabled: false, megaDebridPreferApi: true, bestToken: "", bestDebridUseWebLogin: false, allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "", debridLinkApiKeys: "", linkSnappyLogin: "", linkSnappyPassword: "",
|
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: "",
|
archivePasswordList: "",
|
||||||
rememberToken: true, providerOrder: [], providerPrimary: "realdebrid", providerSecondary: "none",
|
rememberToken: true, providerOrder: [], providerPrimary: "realdebrid", providerSecondary: "none",
|
||||||
providerTertiary: "none", autoProviderFallback: true, outputDir: "", packageName: "",
|
providerTertiary: "none", autoProviderFallback: true, outputDir: "", packageName: "",
|
||||||
@ -875,6 +879,39 @@ function formatAllDebridTimestamp(info: AllDebridHostInfo): string {
|
|||||||
return formatDateTime(info.lastCheckedAt || info.fetchedAt);
|
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 {
|
interface BandwidthChartProps {
|
||||||
items: Record<string, DownloadItem>;
|
items: Record<string, DownloadItem>;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
@ -1254,6 +1291,10 @@ export function App(): ReactElement {
|
|||||||
const [linkPopup, setLinkPopup] = useState<LinkPopupState | null>(null);
|
const [linkPopup, setLinkPopup] = useState<LinkPopupState | null>(null);
|
||||||
const [accountDialog, setAccountDialog] = useState<AccountDialogState | null>(null);
|
const [accountDialog, setAccountDialog] = useState<AccountDialogState | null>(null);
|
||||||
const [accountDialogSearch, setAccountDialogSearch] = useState("");
|
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 [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<{ ids: Set<string>; dontAsk: boolean } | null>(null);
|
const [deleteConfirm, setDeleteConfirm] = useState<{ ids: Set<string>; dontAsk: boolean } | null>(null);
|
||||||
const [columnOrder, setColumnOrder] = useState<string[]>(() => DEFAULT_COLUMN_ORDER);
|
const [columnOrder, setColumnOrder] = useState<string[]>(() => DEFAULT_COLUMN_ORDER);
|
||||||
@ -1271,6 +1312,7 @@ export function App(): ReactElement {
|
|||||||
const [allDebridHostInfo, setAllDebridHostInfo] = useState<AllDebridHostInfo | null>(null);
|
const [allDebridHostInfo, setAllDebridHostInfo] = useState<AllDebridHostInfo | null>(null);
|
||||||
const [allDebridHostLoading, setAllDebridHostLoading] = useState(false);
|
const [allDebridHostLoading, setAllDebridHostLoading] = useState(false);
|
||||||
const allDebridHostRequestRef = useRef(0);
|
const allDebridHostRequestRef = useRef(0);
|
||||||
|
const debridLinkHostLimitsRequestRef = useRef(0);
|
||||||
const accountColumnResizeRef = useRef<{ key: AccountColumnKey; startX: number; startWidth: number } | null>(null);
|
const accountColumnResizeRef = useRef<{ key: AccountColumnKey; startX: number; startWidth: number } | null>(null);
|
||||||
const onAccountColumnResizeMove = useCallback((event: MouseEvent): void => {
|
const onAccountColumnResizeMove = useCallback((event: MouseEvent): void => {
|
||||||
const active = accountColumnResizeRef.current;
|
const active = accountColumnResizeRef.current;
|
||||||
@ -1435,6 +1477,149 @@ export function App(): ReactElement {
|
|||||||
}
|
}
|
||||||
}, [showToast]);
|
}, [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 clearImportQueueFocusListener = useCallback((): void => {
|
||||||
const handler = importQueueFocusHandlerRef.current;
|
const handler = importQueueFocusHandlerRef.current;
|
||||||
if (!handler) {
|
if (!handler) {
|
||||||
@ -1764,7 +1949,7 @@ export function App(): ReactElement {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const option = findAccountOption(kind);
|
const option = findAccountOption(kind);
|
||||||
let statusLabel = "Konfiguriert";
|
let statusLabel = "Aktiviert";
|
||||||
let note = "";
|
let note = "";
|
||||||
if (kind === "megadebrid-api") {
|
if (kind === "megadebrid-api") {
|
||||||
note = "Nur API aktiv. Kein Web-Fallback.";
|
note = "Nur API aktiv. Kein Web-Fallback.";
|
||||||
@ -1790,7 +1975,7 @@ export function App(): ReactElement {
|
|||||||
}
|
}
|
||||||
if (kind === "debridlink-api") {
|
if (kind === "debridlink-api") {
|
||||||
const keyCount = parseDebridLinkApiKeys(settingsDraft.debridLinkApiKeys || "").length;
|
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 provider = getAccountServiceProvider(service);
|
||||||
const dailyUsedBytes = getProviderDailyUsageBytes(snapshot.settings, provider);
|
const dailyUsedBytes = getProviderDailyUsageBytes(snapshot.settings, provider);
|
||||||
@ -1816,7 +2001,9 @@ export function App(): ReactElement {
|
|||||||
return {
|
return {
|
||||||
id: key.id,
|
id: key.id,
|
||||||
label: key.label,
|
label: key.label,
|
||||||
|
token: key.token,
|
||||||
masked: key.masked,
|
masked: key.masked,
|
||||||
|
disabled: (settingsDraft.debridLinkDisabledKeyIds || []).includes(key.id),
|
||||||
dailyUsedBytes: keyDailyUsedBytes,
|
dailyUsedBytes: keyDailyUsedBytes,
|
||||||
dailyLimitBytes: keyDailyLimitBytes,
|
dailyLimitBytes: keyDailyLimitBytes,
|
||||||
dailyRemainingBytes: keyDailyRemainingBytes,
|
dailyRemainingBytes: keyDailyRemainingBytes,
|
||||||
@ -1826,11 +2013,19 @@ export function App(): ReactElement {
|
|||||||
: [];
|
: [];
|
||||||
if (kind === "debridlink-api" && debridLinkKeys.length > 0) {
|
if (kind === "debridlink-api" && debridLinkKeys.length > 0) {
|
||||||
const limitedCount = debridLinkKeys.filter((entry) => entry.dailyLimitReached).length;
|
const limitedCount = debridLinkKeys.filter((entry) => entry.dailyLimitReached).length;
|
||||||
|
const disabledKeyCount = debridLinkKeys.filter((entry) => entry.disabled).length;
|
||||||
|
const keyNotes: string[] = [];
|
||||||
if (limitedCount > 0) {
|
if (limitedCount > 0) {
|
||||||
const limitNote = `${limitedCount}/${debridLinkKeys.length} API-Keys am Limit.`;
|
keyNotes.push(`${limitedCount}/${debridLinkKeys.length} API-Keys am Limit.`);
|
||||||
note = note ? `${limitNote} ${note}` : limitNote;
|
|
||||||
}
|
}
|
||||||
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;
|
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 onAccountRowQuickAction = async (entry: ConfiguredAccountEntry): Promise<void> => {
|
||||||
const meta = getAccountQuickActionMeta(entry.kind);
|
const meta = getAccountQuickActionMeta(entry.kind);
|
||||||
if (!meta) {
|
if (!meta) {
|
||||||
@ -4005,6 +4222,9 @@ export function App(): ReactElement {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="account-header-cell">
|
||||||
|
<span>Info</span>
|
||||||
|
</div>
|
||||||
<div className="account-header-cell">
|
<div className="account-header-cell">
|
||||||
<span>Zugang</span>
|
<span>Zugang</span>
|
||||||
<button
|
<button
|
||||||
@ -4016,7 +4236,9 @@ export function App(): ReactElement {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span>Aktionen</span>
|
<div className="account-header-cell">
|
||||||
|
<span>Aktionen</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{configuredAccounts.map((entry) => {
|
{configuredAccounts.map((entry) => {
|
||||||
const option = findAccountOption(entry.kind);
|
const option = findAccountOption(entry.kind);
|
||||||
@ -4034,46 +4256,21 @@ export function App(): ReactElement {
|
|||||||
<span className="account-mode-pill">{entry.modeLabel}</span>
|
<span className="account-mode-pill">{entry.modeLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="account-cell account-status-cell">
|
<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>}
|
{entry.note && <span className="account-note">{entry.note}</span>}
|
||||||
<div className={`account-usage-stats${entry.dailyLimitReached ? " warning" : ""}`}>
|
</div>
|
||||||
<span>Heute: {humanSize(entry.dailyUsedBytes)}</span>
|
<div className="account-cell account-info-cell">
|
||||||
<span>{entry.dailyLimitBytes > 0 ? `Limit: ${humanSize(entry.dailyLimitBytes)}` : "Kein Tageslimit"}</span>
|
{entry.debridLinkKeys.length > 0 ? (
|
||||||
{entry.dailyLimitBytes > 0 && (
|
<button className="btn btn-sm" onClick={() => setKeyStatsPopup(entry.service)}>
|
||||||
<span>{entry.dailyLimitReached ? "Fallback aktiv" : `Rest: ${humanSize(entry.dailyRemainingBytes || 0)}`}</span>
|
Statistik
|
||||||
)}
|
</button>
|
||||||
{entry.dailyLimitBytes <= 0 && entry.dailyLimitReached && entry.debridLinkKeys.length > 0 && (
|
) : (
|
||||||
<span>Fallback aktiv</span>
|
<div className={`account-usage-stats${entry.dailyLimitReached ? " warning" : ""}`}>
|
||||||
)}
|
<span>Heute: {humanSize(entry.dailyUsedBytes)}</span>
|
||||||
</div>
|
<span>{entry.dailyLimitBytes > 0 ? `Limit: ${humanSize(entry.dailyLimitBytes)}` : "Kein Tageslimit"}</span>
|
||||||
{entry.debridLinkKeys.length > 0 && (
|
{entry.dailyLimitBytes > 0 && (
|
||||||
<div className="account-subkey-list">
|
<span>{entry.dailyLimitReached ? "Fallback aktiv" : `Rest: ${humanSize(entry.dailyRemainingBytes || 0)}`}</span>
|
||||||
{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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -5016,6 +5213,91 @@ export function App(): ReactElement {
|
|||||||
</div>
|
</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 && (
|
{linkPopup && (
|
||||||
<div className="modal-backdrop" onClick={() => setLinkPopup(null)}>
|
<div className="modal-backdrop" onClick={() => setLinkPopup(null)}>
|
||||||
<div className="modal-card link-popup" onClick={(e) => e.stopPropagation()}>
|
<div className="modal-card link-popup" onClick={(e) => e.stopPropagation()}>
|
||||||
|
|||||||
@ -482,11 +482,21 @@ body,
|
|||||||
color: #fda4af;
|
color: #fda4af;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn.success {
|
||||||
|
border-color: rgba(74, 222, 128, 0.7);
|
||||||
|
color: #86efac;
|
||||||
|
}
|
||||||
|
|
||||||
:root[data-theme="light"] .btn.danger {
|
:root[data-theme="light"] .btn.danger {
|
||||||
border-color: color-mix(in srgb, var(--danger) 60%, transparent);
|
border-color: color-mix(in srgb, var(--danger) 60%, transparent);
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] .btn.success {
|
||||||
|
border-color: color-mix(in srgb, #16a34a 60%, transparent);
|
||||||
|
color: #15803d;
|
||||||
|
}
|
||||||
|
|
||||||
.btn.btn-active {
|
.btn.btn-active {
|
||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
|
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-service: 220px;
|
||||||
--account-col-mode: 96px;
|
--account-col-mode: 96px;
|
||||||
--account-col-status: 300px;
|
--account-col-status: 300px;
|
||||||
|
--account-col-info: 220px;
|
||||||
--account-col-secret: 180px;
|
--account-col-secret: 180px;
|
||||||
|
--account-col-actions: 400px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1206,30 +1218,41 @@ body,
|
|||||||
.account-row {
|
.account-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns:
|
grid-template-columns:
|
||||||
minmax(180px, var(--account-col-service))
|
minmax(130px, var(--account-col-service))
|
||||||
minmax(80px, var(--account-col-mode))
|
minmax(72px, var(--account-col-mode))
|
||||||
minmax(180px, var(--account-col-status))
|
minmax(140px, var(--account-col-status))
|
||||||
minmax(120px, var(--account-col-secret))
|
minmax(160px, var(--account-col-info))
|
||||||
minmax(260px, 1fr);
|
minmax(180px, var(--account-col-secret))
|
||||||
|
minmax(280px, var(--account-col-actions));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-table-head {
|
.account-table-head {
|
||||||
padding: 0 4px;
|
padding: 6px 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.04em;
|
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 {
|
.account-header-cell {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-header-cell > span {
|
.account-header-cell > span {
|
||||||
@ -1248,9 +1271,11 @@ body,
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-resize-handle::after {
|
.account-resize-handle:hover::after,
|
||||||
|
.account-resize-handle:focus-visible::after {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 6px;
|
||||||
@ -1259,17 +1284,11 @@ body,
|
|||||||
width: 2px;
|
width: 2px;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
border-radius: 999px;
|
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);
|
background: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-row {
|
.account-row {
|
||||||
padding: 12px;
|
padding: 10px 12px;
|
||||||
border: 1px solid color-mix(in srgb, var(--border) 88%, transparent);
|
border: 1px solid color-mix(in srgb, var(--border) 88%, transparent);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: linear-gradient(180deg, color-mix(in srgb, var(--card) 97%, transparent), color-mix(in srgb, var(--surface) 96%, transparent));
|
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 {
|
.account-cell {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-service-cell,
|
.account-row > .account-cell:nth-child(n+2) {
|
||||||
.account-status-cell {
|
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;
|
display: grid;
|
||||||
gap: 3px;
|
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 {
|
.account-service-cell strong {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-service-cell span,
|
.account-service-cell span {
|
||||||
.account-note {
|
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-note {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.account-usage-stats {
|
.account-usage-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: nowrap;
|
||||||
gap: 6px 10px;
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
line-height: 1.4;
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-usage-stats span + span::before {
|
||||||
|
content: "·";
|
||||||
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-usage-stats.warning {
|
.account-usage-stats.warning {
|
||||||
@ -1335,6 +1396,12 @@ body,
|
|||||||
color: var(--text);
|
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 {
|
.account-status-pill.account-status-down {
|
||||||
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
||||||
border-color: color-mix(in srgb, var(--danger) 40%, transparent);
|
border-color: color-mix(in srgb, var(--danger) 40%, transparent);
|
||||||
@ -1349,7 +1416,7 @@ body,
|
|||||||
.account-secret {
|
.account-secret {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 100%;
|
justify-content: center;
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@ -1366,91 +1433,178 @@ body,
|
|||||||
|
|
||||||
.account-row-actions {
|
.account-row-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: center;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
align-content: center;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-row-actions .btn {
|
.account-row-actions .btn {
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
min-width: 82px;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-row-disabled {
|
.account-row-disabled {
|
||||||
opacity: 0.45;
|
opacity: 0.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-row-disabled .account-row-actions {
|
.account-row-disabled .account-row-actions {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-subkey-list {
|
|
||||||
display: grid;
|
|
||||||
gap: 8px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-subkey-row {
|
.key-stats-popup {
|
||||||
display: grid;
|
width: min(1360px, calc(100vw - 20px));
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
max-width: min(1360px, calc(100vw - 20px));
|
||||||
gap: 8px;
|
max-height: calc(100vh - 24px);
|
||||||
align-items: center;
|
overflow: hidden;
|
||||||
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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 12px;
|
||||||
min-width: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
font-size: 12px;
|
||||||
flex: 0 0 auto;
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-subkey-head span {
|
.key-stats-warn {
|
||||||
color: var(--muted);
|
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;
|
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;
|
font-family: "JetBrains Mono", "Consolas", monospace;
|
||||||
|
color: var(--muted);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-subkey-stats {
|
.account-subkey-table-row .col-masked.link-popup-click {
|
||||||
display: flex;
|
display: block;
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 4px 10px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account-subkey-actions .btn {
|
.account-subkey-table-row .col-action .btn {
|
||||||
padding: 4px 8px;
|
padding: 1px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 3px 8px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2708,7 +2862,13 @@ td {
|
|||||||
justify-content: flex-start;
|
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 {
|
.account-dl-key-limit-row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export const IPC_CHANNELS = {
|
|||||||
OPEN_ALLDEBRID_LOGIN: "app:open-alldebrid-login",
|
OPEN_ALLDEBRID_LOGIN: "app:open-alldebrid-login",
|
||||||
IMPORT_BESTDEBRID_COOKIES: "app:import-bestdebrid-cookies",
|
IMPORT_BESTDEBRID_COOKIES: "app:import-bestdebrid-cookies",
|
||||||
GET_ALLDEBRID_HOST_INFO: "app:get-alldebrid-host-info",
|
GET_ALLDEBRID_HOST_INFO: "app:get-alldebrid-host-info",
|
||||||
|
GET_DEBRIDLINK_HOST_LIMITS: "app:get-debridlink-host-limits",
|
||||||
RETRY_EXTRACTION: "queue:retry-extraction",
|
RETRY_EXTRACTION: "queue:retry-extraction",
|
||||||
EXTRACT_NOW: "queue:extract-now",
|
EXTRACT_NOW: "queue:extract-now",
|
||||||
RESET_PACKAGE: "queue:reset-package",
|
RESET_PACKAGE: "queue:reset-package",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type {
|
|||||||
AddLinksPayload,
|
AddLinksPayload,
|
||||||
AllDebridHostInfo,
|
AllDebridHostInfo,
|
||||||
AppSettings,
|
AppSettings,
|
||||||
|
DebridLinkHostLimitInfo,
|
||||||
DebridProvider,
|
DebridProvider,
|
||||||
DuplicatePolicy,
|
DuplicatePolicy,
|
||||||
HistoryEntry,
|
HistoryEntry,
|
||||||
@ -54,6 +55,7 @@ export interface ElectronApi {
|
|||||||
openAllDebridLogin: () => Promise<void>;
|
openAllDebridLogin: () => Promise<void>;
|
||||||
importBestDebridCookies: () => Promise<number>;
|
importBestDebridCookies: () => Promise<number>;
|
||||||
getAllDebridHostInfo: () => Promise<AllDebridHostInfo>;
|
getAllDebridHostInfo: () => Promise<AllDebridHostInfo>;
|
||||||
|
getDebridLinkHostLimits: () => Promise<DebridLinkHostLimitInfo[]>;
|
||||||
retryExtraction: (packageId: string) => Promise<void>;
|
retryExtraction: (packageId: string) => Promise<void>;
|
||||||
extractNow: (packageId: string) => Promise<void>;
|
extractNow: (packageId: string) => Promise<void>;
|
||||||
resetPackage: (packageId: string) => Promise<void>;
|
resetPackage: (packageId: string) => Promise<void>;
|
||||||
|
|||||||
@ -62,6 +62,7 @@ export interface AppSettings {
|
|||||||
ddownloadPassword: string;
|
ddownloadPassword: string;
|
||||||
oneFichierApiKey: string;
|
oneFichierApiKey: string;
|
||||||
debridLinkApiKeys: string;
|
debridLinkApiKeys: string;
|
||||||
|
debridLinkDisabledKeyIds: string[];
|
||||||
linkSnappyLogin: string;
|
linkSnappyLogin: string;
|
||||||
linkSnappyPassword: string;
|
linkSnappyPassword: string;
|
||||||
archivePasswordList: string;
|
archivePasswordList: string;
|
||||||
@ -290,6 +291,18 @@ export interface AllDebridHostInfo {
|
|||||||
note: string;
|
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 {
|
export interface ParsedHashEntry {
|
||||||
fileName: string;
|
fileName: string;
|
||||||
algorithm: "crc32" | "md5" | "sha1";
|
algorithm: "crc32" | "md5" | "sha1";
|
||||||
|
|||||||
@ -78,6 +78,10 @@ describe("extractEpisodeToken", () => {
|
|||||||
it("extracts double episode with single-digit numbers", () => {
|
it("extracts double episode with single-digit numbers", () => {
|
||||||
expect(extractEpisodeToken("show-s1e1e2-720p")).toBe("S01E01E02");
|
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", () => {
|
describe("applyEpisodeTokenToFolderName", () => {
|
||||||
@ -691,4 +695,13 @@ describe("buildAutoRenameBaseNameFromFolders", () => {
|
|||||||
);
|
);
|
||||||
expect(result).toBe("Room.104.S04E01.GERMAN.DL.720p.WEBRiP.x264-LAW");
|
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 { defaultSettings, REQUEST_RETRIES } from "../src/main/constants";
|
||||||
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
||||||
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
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;
|
const originalFetch = globalThis.fetch;
|
||||||
|
|
||||||
@ -379,6 +379,39 @@ describe("debrid service", () => {
|
|||||||
expect(info.limitSimuDl).toBe(2);
|
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 () => {
|
it("uses AllDebrid web path when enabled", async () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user