Compare commits
No commits in common. "3a8be961b0174ff181f382970e9ef7809f53ec6c" and "4d2f11a96df8d3e6dcffd5bd7fd54a8343849e81" have entirely different histories.
3a8be961b0
...
4d2f11a96d
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.7.142",
|
"version": "1.7.141",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -1,147 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
/** Dedicated log file for multi-account/key rotation events:
|
|
||||||
* Mega-Debrid account selection, Debrid-Link key selection, per-attempt
|
|
||||||
* test result, cooldown set, fallback to next account/key, etc.
|
|
||||||
* Separate from rd_downloader.log so the user can see the rotation flow
|
|
||||||
* without the noise of normal download activity. */
|
|
||||||
|
|
||||||
type RotationLevel = "INFO" | "WARN" | "ERROR";
|
|
||||||
|
|
||||||
const ROTATION_LOG_MAX_FILE_BYTES = Number(process.env.RD_ACCOUNT_ROTATION_LOG_MAX_BYTES || 5 * 1024 * 1024);
|
|
||||||
const ROTATION_LOG_RETENTION_DAYS = Number(process.env.RD_ACCOUNT_ROTATION_LOG_RETENTION_DAYS || 14);
|
|
||||||
|
|
||||||
let rotationLogPath: string | null = null;
|
|
||||||
|
|
||||||
function sanitizeFieldValue(value: unknown): string {
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value.replace(/\r?\n/g, "\\n");
|
|
||||||
}
|
|
||||||
if (typeof value === "number" || typeof value === "boolean") {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.stringify(value).replace(/\r?\n/g, "\\n");
|
|
||||||
} catch {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatFields(fields?: Record<string, unknown>): string {
|
|
||||||
if (!fields) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
const parts = Object.entries(fields)
|
|
||||||
.filter(([, value]) => value !== undefined && value !== null && sanitizeFieldValue(value) !== "")
|
|
||||||
.map(([key, value]) => `${key}=${sanitizeFieldValue(value)}`);
|
|
||||||
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotateIfNeeded(filePath: string): void {
|
|
||||||
try {
|
|
||||||
const stat = fs.statSync(filePath);
|
|
||||||
if (stat.size < ROTATION_LOG_MAX_FILE_BYTES) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const backup = `${filePath}.old`;
|
|
||||||
try {
|
|
||||||
fs.rmSync(backup, { force: true });
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
fs.renameSync(filePath, backup);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanupOldBackup(filePath: string): void {
|
|
||||||
const backup = `${filePath}.old`;
|
|
||||||
try {
|
|
||||||
const stat = fs.statSync(backup);
|
|
||||||
const cutoff = Date.now() - ROTATION_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
|
||||||
if (stat.mtimeMs < cutoff) {
|
|
||||||
fs.rmSync(backup, { force: true });
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initAccountRotationLog(baseDir: string): void {
|
|
||||||
rotationLogPath = path.join(baseDir, "account-rotation.log");
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(path.dirname(rotationLogPath), { recursive: true });
|
|
||||||
cleanupOldBackup(rotationLogPath);
|
|
||||||
if (!fs.existsSync(rotationLogPath)) {
|
|
||||||
fs.writeFileSync(rotationLogPath, "", "utf8");
|
|
||||||
}
|
|
||||||
rotateIfNeeded(rotationLogPath);
|
|
||||||
if (!fs.existsSync(rotationLogPath)) {
|
|
||||||
fs.writeFileSync(rotationLogPath, "", "utf8");
|
|
||||||
}
|
|
||||||
fs.appendFileSync(
|
|
||||||
rotationLogPath,
|
|
||||||
`=== Account-Rotation Log Start: ${new Date().toISOString()} ===\n`,
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
rotationLogPath = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Record an account/key rotation event. The format is intentionally compact
|
|
||||||
* and grep-friendly: timestamp + level + provider + accountLabel + event + fields.
|
|
||||||
* Example output:
|
|
||||||
* 2026-04-19T20:48:50.000Z [INFO] Mega-Debrid Web | Account 2 (fa**david@...) | TEST | link=https://...
|
|
||||||
* 2026-04-19T20:48:52.000Z [WARN] Mega-Debrid Web | Account 2 (fa**david@...) | FAILED reason="Antwort leer" cooldownSec=30 | link=https://...
|
|
||||||
* 2026-04-19T20:48:53.000Z [INFO] Mega-Debrid Web | Account 3 (am**@example.com) | TEST | link=https://...
|
|
||||||
* 2026-04-19T20:48:55.000Z [INFO] Mega-Debrid Web | Account 3 (am**@example.com) | OK directLink=https://... | link=https://... */
|
|
||||||
export function logAccountRotation(
|
|
||||||
level: RotationLevel,
|
|
||||||
provider: string,
|
|
||||||
accountLabel: string,
|
|
||||||
event: string,
|
|
||||||
fields?: Record<string, unknown>
|
|
||||||
): void {
|
|
||||||
if (!rotationLogPath) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
rotateIfNeeded(rotationLogPath);
|
|
||||||
if (!fs.existsSync(rotationLogPath)) {
|
|
||||||
fs.writeFileSync(rotationLogPath, "", "utf8");
|
|
||||||
}
|
|
||||||
const head = `${new Date().toISOString()} [${level}] ${provider} | ${accountLabel} | ${event}`;
|
|
||||||
fs.appendFileSync(rotationLogPath, `${head}${formatFields(fields)}\n`, "utf8");
|
|
||||||
} catch {
|
|
||||||
// ignore write errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAccountRotationLogPath(): string | null {
|
|
||||||
if (!rotationLogPath) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return fs.existsSync(rotationLogPath) ? rotationLogPath : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shutdownAccountRotationLog(): void {
|
|
||||||
if (!rotationLogPath) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
fs.appendFileSync(
|
|
||||||
rotationLogPath,
|
|
||||||
`=== Account-Rotation Log Ende: ${new Date().toISOString()} ===\n`,
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
rotationLogPath = null;
|
|
||||||
}
|
|
||||||
@ -37,7 +37,6 @@ import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } fro
|
|||||||
import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server";
|
import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server";
|
||||||
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
||||||
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log";
|
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log";
|
||||||
import { initAccountRotationLog, shutdownAccountRotationLog } from "./account-rotation-log";
|
|
||||||
import { getDebugSetupCheck } from "./debug-setup";
|
import { getDebugSetupCheck } from "./debug-setup";
|
||||||
import { buildLinkExportSelection, serializeLinkExportText } from "./link-export";
|
import { buildLinkExportSelection, serializeLinkExportText } from "./link-export";
|
||||||
import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "./rename-log";
|
import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "./rename-log";
|
||||||
@ -85,7 +84,6 @@ export class AppController {
|
|||||||
initPackageLogs(this.storagePaths.baseDir);
|
initPackageLogs(this.storagePaths.baseDir);
|
||||||
initItemLogs(this.storagePaths.baseDir);
|
initItemLogs(this.storagePaths.baseDir);
|
||||||
initAuditLog(this.storagePaths.baseDir);
|
initAuditLog(this.storagePaths.baseDir);
|
||||||
initAccountRotationLog(this.storagePaths.baseDir);
|
|
||||||
initRenameLog(this.storagePaths.baseDir);
|
initRenameLog(this.storagePaths.baseDir);
|
||||||
initTraceLog(this.storagePaths.baseDir);
|
initTraceLog(this.storagePaths.baseDir);
|
||||||
this.settings = loadSettings(this.storagePaths);
|
this.settings = loadSettings(this.storagePaths);
|
||||||
@ -559,21 +557,21 @@ export class AppController {
|
|||||||
return encryptBackup(payload);
|
return encryptBackup(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
public exportSupportBundle(): { buffer: Buffer; defaultFileName: string } {
|
public exportSupportBundle(): { buffer: Buffer; defaultFileName: string } {
|
||||||
this.audit("INFO", "Support-Bundle exportiert");
|
this.audit("INFO", "Support-Bundle exportiert");
|
||||||
logTraceEvent("INFO", "support", "Support-Bundle erstellt", {
|
logTraceEvent("INFO", "support", "Support-Bundle erstellt", {
|
||||||
packageCount: Object.keys(this.manager.getSnapshot().session.packages).length,
|
packageCount: Object.keys(this.manager.getSnapshot().session.packages).length,
|
||||||
itemCount: Object.keys(this.manager.getSnapshot().session.items).length
|
itemCount: Object.keys(this.manager.getSnapshot().session.items).length
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
buffer: buildSupportBundle(this.manager, this.storagePaths.baseDir, { hostDiagnosticsMode: "cached" }),
|
buffer: buildSupportBundle(this.manager, this.storagePaths.baseDir, { hostDiagnosticsMode: "cached" }),
|
||||||
defaultFileName: getSupportBundleDefaultFileName()
|
defaultFileName: getSupportBundleDefaultFileName()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSupportBundleDefaultFileName(): string {
|
public getSupportBundleDefaultFileName(): string {
|
||||||
return getSupportBundleDefaultFileName();
|
return getSupportBundleDefaultFileName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public importBackup(data: Buffer): { restored: boolean; message: string } {
|
public importBackup(data: Buffer): { restored: boolean; message: string } {
|
||||||
let parsed: Record<string, unknown>;
|
let parsed: Record<string, unknown>;
|
||||||
@ -681,7 +679,6 @@ export class AppController {
|
|||||||
shutdownRenameLog();
|
shutdownRenameLog();
|
||||||
this.audit("INFO", "App beendet");
|
this.audit("INFO", "App beendet");
|
||||||
shutdownTraceLog();
|
shutdownTraceLog();
|
||||||
shutdownAccountRotationLog();
|
|
||||||
shutdownAuditLog();
|
shutdownAuditLog();
|
||||||
if (this.settings.historyRetentionMode === "session") {
|
if (this.settings.historyRetentionMode === "session") {
|
||||||
clearHistory(this.storagePaths);
|
clearHistory(this.storagePaths);
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { AllDebridHostInfo, AppSettings, DebridFallbackProvider, DebridLinkHostL
|
|||||||
import { isDebridLinkApiKeyDailyLimitReached, isMegaDebridAccountDisabled, isMegaDebridAccountDailyLimitReached, isProviderDailyLimitReached } from "../shared/provider-daily-limits";
|
import { isDebridLinkApiKeyDailyLimitReached, isMegaDebridAccountDisabled, isMegaDebridAccountDailyLimitReached, 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";
|
||||||
import { logAccountRotation } from "./account-rotation-log";
|
|
||||||
import { RealDebridClient, UnrestrictedLink } from "./realdebrid";
|
import { RealDebridClient, UnrestrictedLink } from "./realdebrid";
|
||||||
import { compactErrorText, filenameFromUrl, looksLikeOpaqueFilename, sleep } from "./utils";
|
import { compactErrorText, filenameFromUrl, looksLikeOpaqueFilename, sleep } from "./utils";
|
||||||
|
|
||||||
@ -1809,39 +1808,26 @@ class MegaDebridClient {
|
|||||||
let usableAccountSeen = false;
|
let usableAccountSeen = false;
|
||||||
const cooldownFailures: string[] = [];
|
const cooldownFailures: string[] = [];
|
||||||
let earliestCooldownUntil = 0;
|
let earliestCooldownUntil = 0;
|
||||||
const totalAccounts = accounts.length;
|
const hasMultiple = accounts.length > 1;
|
||||||
const providerName = `Mega-Debrid ${mode === "api" ? "API" : "Web"}`;
|
|
||||||
const linkShort = String(link || "").slice(0, 80);
|
|
||||||
|
|
||||||
// Always start from first account — use first available, skip disabled/limited/cooldown.
|
// Always start from first account — use first available, skip disabled/limited/cooldown.
|
||||||
for (let idx = 0; idx < accounts.length; idx += 1) {
|
for (let idx = 0; idx < accounts.length; idx += 1) {
|
||||||
const account = accounts[idx];
|
const account = accounts[idx];
|
||||||
// Always show account number — even with 1 account — so user can tell at a
|
const accountLabel = hasMultiple ? ` (${account.label})` : "";
|
||||||
// glance which account is in play. Format: "(Account 2/3, fa**david@...)"
|
|
||||||
const accountLabel = ` (${account.label}/${totalAccounts}, ${account.maskedLogin})`;
|
|
||||||
const rotationLabel = `${account.label}/${totalAccounts} (${account.maskedLogin})`;
|
|
||||||
|
|
||||||
if (isMegaDebridAccountDisabled(settings, account.id)) {
|
if (isMegaDebridAccountDisabled(settings, account.id)) {
|
||||||
logger.info(`Mega-Debrid${accountLabel}: uebersprungen (manuell deaktiviert), pruefe naechsten Account`);
|
logger.info(`Mega-Debrid${accountLabel}: uebersprungen (manuell deaktiviert), pruefe naechsten Account`);
|
||||||
logAccountRotation("INFO", providerName, rotationLabel, "SKIP_DISABLED", { reason: "manually disabled" });
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isMegaDebridAccountDailyLimitReached(settings, account.id)) {
|
if (isMegaDebridAccountDailyLimitReached(settings, account.id)) {
|
||||||
logger.info(`Mega-Debrid${accountLabel}: uebersprungen (lokales Tageslimit erreicht), pruefe naechsten Account`);
|
logger.info(`Mega-Debrid${accountLabel}: uebersprungen (lokales Tageslimit erreicht), pruefe naechsten Account`);
|
||||||
logAccountRotation("INFO", providerName, rotationLabel, "SKIP_DAILY_LIMIT", { reason: "local daily limit reached" });
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Cooldown key includes mode so API failures don't block Web attempts
|
// Cooldown key includes mode so API failures don't block Web attempts
|
||||||
const cooldownKey = `${account.id}:${mode}`;
|
const cooldownKey = `${account.id}:${mode}`;
|
||||||
const accountCooldownState = getMegaDebridAccountCooldownState(cooldownKey);
|
const accountCooldownState = getMegaDebridAccountCooldownState(cooldownKey);
|
||||||
if (accountCooldownState) {
|
if (accountCooldownState) {
|
||||||
const untilStr = new Date(accountCooldownState.until).toLocaleTimeString();
|
logger.info(`Mega-Debrid${accountLabel}: uebersprungen (Cooldown bis ${new Date(accountCooldownState.until).toLocaleTimeString()}), pruefe naechsten Account`);
|
||||||
logger.info(`Mega-Debrid${accountLabel}: uebersprungen (Cooldown bis ${untilStr}), pruefe naechsten Account`);
|
|
||||||
logAccountRotation("INFO", providerName, rotationLabel, "SKIP_COOLDOWN", {
|
|
||||||
reason: accountCooldownState.message,
|
|
||||||
category: accountCooldownState.category,
|
|
||||||
until: untilStr
|
|
||||||
});
|
|
||||||
cooldownFailures.push(`Mega-Debrid${accountLabel}: ${accountCooldownState.message}`);
|
cooldownFailures.push(`Mega-Debrid${accountLabel}: ${accountCooldownState.message}`);
|
||||||
if (!earliestCooldownUntil || accountCooldownState.until < earliestCooldownUntil) {
|
if (!earliestCooldownUntil || accountCooldownState.until < earliestCooldownUntil) {
|
||||||
earliestCooldownUntil = accountCooldownState.until;
|
earliestCooldownUntil = accountCooldownState.until;
|
||||||
@ -1849,34 +1835,20 @@ class MegaDebridClient {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CLEAR per-account TEST log line BEFORE the network call, so the user
|
|
||||||
// can always see exactly which account is currently being tested for
|
|
||||||
// link generation — even if the call hangs or times out.
|
|
||||||
logger.info(`Mega-Debrid${accountLabel}: TESTE Account fuer Link-Generierung...`);
|
|
||||||
logAccountRotation("INFO", providerName, rotationLabel, "TEST", { link: linkShort });
|
|
||||||
const testStartedAt = Date.now();
|
|
||||||
|
|
||||||
usableAccountSeen = true;
|
usableAccountSeen = true;
|
||||||
try {
|
try {
|
||||||
const client = new MegaDebridClient(account.login, account.password, mode, allowApiFallback, megaWebUnrestrict);
|
const client = new MegaDebridClient(account.login, account.password, mode, allowApiFallback, megaWebUnrestrict);
|
||||||
const result = await client.unrestrictLink(link, signal);
|
const result = await client.unrestrictLink(link, signal);
|
||||||
clearMegaDebridAccountCooldownState(cooldownKey);
|
clearMegaDebridAccountCooldownState(cooldownKey);
|
||||||
const elapsedMs = Date.now() - testStartedAt;
|
logger.info(`Mega-Debrid${accountLabel}: Unrestrict OK -> ${result.fileName || "?"}`);
|
||||||
logger.info(`Mega-Debrid${accountLabel}: Unrestrict OK nach ${elapsedMs}ms -> ${result.fileName || "?"}`);
|
|
||||||
logAccountRotation("INFO", providerName, rotationLabel, "OK", {
|
|
||||||
elapsedMs,
|
|
||||||
fileName: result.fileName || "",
|
|
||||||
link: linkShort
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
sourceLabel: `${result.sourceLabel ? `${result.sourceLabel} ` : ""}${account.label}`,
|
sourceLabel: account.label,
|
||||||
sourceAccountId: account.id,
|
sourceAccountId: account.id,
|
||||||
sourceAccountLabel: account.label
|
sourceAccountLabel: account.label
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const failure = MegaDebridClient.classifyAccountFailure(error);
|
const failure = MegaDebridClient.classifyAccountFailure(error);
|
||||||
const elapsedMs = Date.now() - testStartedAt;
|
|
||||||
failures.push(`Mega-Debrid${accountLabel}: ${failure.message}`);
|
failures.push(`Mega-Debrid${accountLabel}: ${failure.message}`);
|
||||||
if (failure.cooldownMs > 0) {
|
if (failure.cooldownMs > 0) {
|
||||||
setMegaDebridAccountCooldownState(cooldownKey, failure.cooldownMs, failure.message, failure.category);
|
setMegaDebridAccountCooldownState(cooldownKey, failure.cooldownMs, failure.message, failure.category);
|
||||||
@ -1884,35 +1856,12 @@ class MegaDebridClient {
|
|||||||
clearMegaDebridAccountCooldownState(cooldownKey);
|
clearMegaDebridAccountCooldownState(cooldownKey);
|
||||||
}
|
}
|
||||||
if (failure.fatal) {
|
if (failure.fatal) {
|
||||||
logAccountRotation("ERROR", providerName, rotationLabel, "FATAL", {
|
|
||||||
elapsedMs,
|
|
||||||
reason: failure.message,
|
|
||||||
category: failure.category,
|
|
||||||
link: linkShort
|
|
||||||
});
|
|
||||||
throw new Error(`Mega-Debrid${accountLabel}: ${failure.message}`);
|
throw new Error(`Mega-Debrid${accountLabel}: ${failure.message}`);
|
||||||
}
|
}
|
||||||
const cooldownInfo = failure.cooldownMs > 0
|
const cooldownInfo = failure.cooldownMs > 0
|
||||||
? `, Cooldown ${Math.ceil(failure.cooldownMs / 1000)}s`
|
? `, Cooldown ${Math.ceil(failure.cooldownMs / 1000)}s`
|
||||||
: "";
|
: "";
|
||||||
// Find the next account that will be tried (for clearer log)
|
logger.warn(`Mega-Debrid${accountLabel}: ${failure.message}${cooldownInfo}, pruefe naechsten Account`);
|
||||||
let nextLabel = "ENDE";
|
|
||||||
for (let nextIdx = idx + 1; nextIdx < accounts.length; nextIdx += 1) {
|
|
||||||
const nextAcc = accounts[nextIdx];
|
|
||||||
if (!isMegaDebridAccountDisabled(settings, nextAcc.id) && !isMegaDebridAccountDailyLimitReached(settings, nextAcc.id) && !getMegaDebridAccountCooldownState(`${nextAcc.id}:${mode}`)) {
|
|
||||||
nextLabel = `${nextAcc.label}/${totalAccounts} (${nextAcc.maskedLogin})`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.warn(`Mega-Debrid${accountLabel}: ${failure.message}${cooldownInfo}, pruefe naechsten Account (${nextLabel})`);
|
|
||||||
logAccountRotation("WARN", providerName, rotationLabel, "FAILED", {
|
|
||||||
elapsedMs,
|
|
||||||
reason: failure.message,
|
|
||||||
category: failure.category,
|
|
||||||
cooldownSec: failure.cooldownMs > 0 ? Math.ceil(failure.cooldownMs / 1000) : 0,
|
|
||||||
next: nextLabel,
|
|
||||||
link: linkShort
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1975,25 +1924,11 @@ class MegaDebridClient {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mega-Web "Antwort leer" / empty body — server frequently returns transient
|
// Temporary/transport errors — short cooldown, try next account
|
||||||
// empty responses that recover within seconds. A 2-minute cooldown for this
|
|
||||||
// is way too long because the account is fundamentally healthy. Use a short
|
|
||||||
// 20s cooldown so the next download attempt can use this account again.
|
|
||||||
if (/antwort\s+leer|empty\s+response|leere\s+antwort/i.test(errorText)) {
|
|
||||||
return {
|
|
||||||
fatal: false,
|
|
||||||
cooldownMs: 20_000,
|
|
||||||
message: errorText || "Mega-Web transient empty response",
|
|
||||||
category: "temporary"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temporary/transport errors — short cooldown, try next account.
|
|
||||||
// Plain network blips deserve a much shorter cooldown than 2 min.
|
|
||||||
if (isRetryableErrorText(errorText) || /timeout|network|fetch|socket/i.test(errorText)) {
|
if (isRetryableErrorText(errorText) || /timeout|network|fetch|socket/i.test(errorText)) {
|
||||||
return {
|
return {
|
||||||
fatal: false,
|
fatal: false,
|
||||||
cooldownMs: 30_000,
|
cooldownMs: MEGA_DEBRID_ACCOUNT_COOLDOWN_MS,
|
||||||
message: errorText || "temporaerer Fehler",
|
message: errorText || "temporaerer Fehler",
|
||||||
category: "temporary"
|
category: "temporary"
|
||||||
};
|
};
|
||||||
@ -2002,7 +1937,7 @@ class MegaDebridClient {
|
|||||||
// Unknown errors — short cooldown, try next account (non-fatal)
|
// Unknown errors — short cooldown, try next account (non-fatal)
|
||||||
return {
|
return {
|
||||||
fatal: false,
|
fatal: false,
|
||||||
cooldownMs: 30_000,
|
cooldownMs: MEGA_DEBRID_ACCOUNT_COOLDOWN_MS,
|
||||||
message: errorText || "unbekannter Fehler",
|
message: errorText || "unbekannter Fehler",
|
||||||
category: "temporary"
|
category: "temporary"
|
||||||
};
|
};
|
||||||
@ -2454,37 +2389,23 @@ class DebridLinkClient {
|
|||||||
let earliestCooldownUntil = 0;
|
let earliestCooldownUntil = 0;
|
||||||
const attemptedKeyFailures: Array<{ message: string; cooldownMs: number; category?: DebridLinkCooldownCategory }> = [];
|
const attemptedKeyFailures: Array<{ message: string; cooldownMs: number; category?: DebridLinkCooldownCategory }> = [];
|
||||||
let consecutiveTransportFailures = 0;
|
let consecutiveTransportFailures = 0;
|
||||||
const totalKeys = this.apiKeys.length;
|
|
||||||
const providerName = "Debrid-Link";
|
|
||||||
const linkShort = String(link || "").slice(0, 80);
|
|
||||||
|
|
||||||
// Always start from first key — use first available, skip disabled/limited/cooldown.
|
// Always start from first key — use first available, skip disabled/limited/cooldown.
|
||||||
// This ensures all parallel items use the same key until it's actually exhausted.
|
// This ensures all parallel items use the same key until it's actually exhausted.
|
||||||
for (let keyIdx = 0; keyIdx < this.apiKeys.length; keyIdx += 1) {
|
for (let keyIdx = 0; keyIdx < this.apiKeys.length; keyIdx += 1) {
|
||||||
const apiKey = this.apiKeys[keyIdx];
|
const apiKey = this.apiKeys[keyIdx];
|
||||||
// Always show key number — even with 1 key — so user can tell at a
|
const keyLabel = this.apiKeys.length > 1 ? ` (${apiKey.label})` : "";
|
||||||
// glance which key is in play. Format: "(Key 2/3, abc***xyz)"
|
|
||||||
const keyLabel = ` (${apiKey.label}/${totalKeys}, ${apiKey.masked})`;
|
|
||||||
const rotationLabel = `${apiKey.label}/${totalKeys} (${apiKey.masked})`;
|
|
||||||
if (isDebridLinkApiKeyDisabled(settings, apiKey.id)) {
|
if (isDebridLinkApiKeyDisabled(settings, apiKey.id)) {
|
||||||
logger.info(`Debrid-Link${keyLabel}: uebersprungen (manuell deaktiviert), pruefe naechsten Key`);
|
logger.info(`Debrid-Link${keyLabel}: uebersprungen (manuell deaktiviert), pruefe naechsten Key`);
|
||||||
logAccountRotation("INFO", providerName, rotationLabel, "SKIP_DISABLED", { reason: "manually disabled" });
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (isDebridLinkApiKeyDailyLimitReached(settings, apiKey.id)) {
|
if (isDebridLinkApiKeyDailyLimitReached(settings, apiKey.id)) {
|
||||||
logger.info(`Debrid-Link${keyLabel}: uebersprungen (lokales Tageslimit erreicht), pruefe naechsten Key`);
|
logger.info(`Debrid-Link${keyLabel}: uebersprungen (lokales Tageslimit erreicht), pruefe naechsten Key`);
|
||||||
logAccountRotation("INFO", providerName, rotationLabel, "SKIP_DAILY_LIMIT", { reason: "local daily limit reached" });
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const keyCooldownState = getDebridLinkKeyCooldownState(apiKey.id);
|
const keyCooldownState = getDebridLinkKeyCooldownState(apiKey.id);
|
||||||
if (keyCooldownState) {
|
if (keyCooldownState) {
|
||||||
const untilStr = new Date(keyCooldownState.until).toLocaleTimeString();
|
logger.info(`Debrid-Link${keyLabel}: uebersprungen (Cooldown bis ${new Date(keyCooldownState.until).toLocaleTimeString()}), pruefe naechsten Key`);
|
||||||
logger.info(`Debrid-Link${keyLabel}: uebersprungen (Cooldown bis ${untilStr}), pruefe naechsten Key`);
|
|
||||||
logAccountRotation("INFO", providerName, rotationLabel, "SKIP_COOLDOWN", {
|
|
||||||
reason: keyCooldownState.message,
|
|
||||||
category: keyCooldownState.category,
|
|
||||||
until: untilStr
|
|
||||||
});
|
|
||||||
cooldownFailures.push(`Debrid-Link${keyLabel}: ${keyCooldownState.message}`);
|
cooldownFailures.push(`Debrid-Link${keyLabel}: ${keyCooldownState.message}`);
|
||||||
if (!earliestCooldownUntil || keyCooldownState.until < earliestCooldownUntil) {
|
if (!earliestCooldownUntil || keyCooldownState.until < earliestCooldownUntil) {
|
||||||
earliestCooldownUntil = keyCooldownState.until;
|
earliestCooldownUntil = keyCooldownState.until;
|
||||||
@ -2492,25 +2413,12 @@ class DebridLinkClient {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// CLEAR per-key TEST log line BEFORE the network call, so the user
|
|
||||||
// can always see exactly which key is currently being tested for
|
|
||||||
// link generation — even if the call hangs or times out.
|
|
||||||
logger.info(`Debrid-Link${keyLabel}: TESTE Key fuer Link-Generierung...`);
|
|
||||||
logAccountRotation("INFO", providerName, rotationLabel, "TEST", { link: linkShort });
|
|
||||||
const testStartedAt = Date.now();
|
|
||||||
|
|
||||||
usableKeySeen = true;
|
usableKeySeen = true;
|
||||||
try {
|
try {
|
||||||
const result = await this.unrestrictWithKey(apiKey, link, signal);
|
const result = await this.unrestrictWithKey(apiKey, link, signal);
|
||||||
clearDebridLinkKeyCooldownState(apiKey.id);
|
clearDebridLinkKeyCooldownState(apiKey.id);
|
||||||
setDebridLinkKeyRuntimeStatus(apiKey.id, "ready", "Unrestrict erfolgreich");
|
setDebridLinkKeyRuntimeStatus(apiKey.id, "ready", "Unrestrict erfolgreich");
|
||||||
const elapsedMs = Date.now() - testStartedAt;
|
logger.info(`Debrid-Link${keyLabel}: Unrestrict OK -> ${result.fileName || "?"}`);
|
||||||
logger.info(`Debrid-Link${keyLabel}: Unrestrict OK nach ${elapsedMs}ms -> ${result.fileName || "?"}`);
|
|
||||||
logAccountRotation("INFO", providerName, rotationLabel, "OK", {
|
|
||||||
elapsedMs,
|
|
||||||
fileName: result.fileName || "",
|
|
||||||
link: linkShort
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
sourceLabel: apiKey.label,
|
sourceLabel: apiKey.label,
|
||||||
@ -2519,7 +2427,6 @@ class DebridLinkClient {
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const failure = await this.classifyKeyFailure(error, apiKey, link, signal);
|
const failure = await this.classifyKeyFailure(error, apiKey, link, signal);
|
||||||
const elapsedMs = Date.now() - testStartedAt;
|
|
||||||
attemptedKeyFailures.push({
|
attemptedKeyFailures.push({
|
||||||
message: `Debrid-Link${keyLabel}: ${failure.message}`,
|
message: `Debrid-Link${keyLabel}: ${failure.message}`,
|
||||||
cooldownMs: failure.cooldownMs,
|
cooldownMs: failure.cooldownMs,
|
||||||
@ -2533,12 +2440,6 @@ class DebridLinkClient {
|
|||||||
setDebridLinkKeyRuntimeStatus(apiKey.id, failure.category === "invalid" ? "invalid" : "error", failure.message);
|
setDebridLinkKeyRuntimeStatus(apiKey.id, failure.category === "invalid" ? "invalid" : "error", failure.message);
|
||||||
}
|
}
|
||||||
if (failure.fatal) {
|
if (failure.fatal) {
|
||||||
logAccountRotation("ERROR", providerName, rotationLabel, "FATAL", {
|
|
||||||
elapsedMs,
|
|
||||||
reason: failure.message,
|
|
||||||
category: failure.category,
|
|
||||||
link: linkShort
|
|
||||||
});
|
|
||||||
throw new Error(`Debrid-Link${keyLabel}: ${failure.message}`);
|
throw new Error(`Debrid-Link${keyLabel}: ${failure.message}`);
|
||||||
}
|
}
|
||||||
if (failure.providerWide) {
|
if (failure.providerWide) {
|
||||||
@ -2546,13 +2447,6 @@ class DebridLinkClient {
|
|||||||
// Break immediately and apply a longer cooldown (5 min) to avoid burning all keys.
|
// Break immediately and apply a longer cooldown (5 min) to avoid burning all keys.
|
||||||
const providerWideCooldownMs = 5 * 60 * 1000;
|
const providerWideCooldownMs = 5 * 60 * 1000;
|
||||||
logger.warn(`Debrid-Link${keyLabel}: ${failure.message} (provider-wide, ueberspringe verbleibende Keys, Cooldown ${providerWideCooldownMs / 1000}s)`);
|
logger.warn(`Debrid-Link${keyLabel}: ${failure.message} (provider-wide, ueberspringe verbleibende Keys, Cooldown ${providerWideCooldownMs / 1000}s)`);
|
||||||
logAccountRotation("ERROR", providerName, rotationLabel, "PROVIDER_WIDE", {
|
|
||||||
elapsedMs,
|
|
||||||
reason: failure.message,
|
|
||||||
category: failure.category,
|
|
||||||
cooldownSec: Math.ceil(providerWideCooldownMs / 1000),
|
|
||||||
link: linkShort
|
|
||||||
});
|
|
||||||
throw new Error(`debrid_link_cooldown:${providerWideCooldownMs}:Debrid-Link${keyLabel}: ${failure.message}`);
|
throw new Error(`debrid_link_cooldown:${providerWideCooldownMs}:Debrid-Link${keyLabel}: ${failure.message}`);
|
||||||
}
|
}
|
||||||
// Track consecutive transport failures (timeout/network) to detect cascades.
|
// Track consecutive transport failures (timeout/network) to detect cascades.
|
||||||
@ -2562,35 +2456,12 @@ class DebridLinkClient {
|
|||||||
// 2+ keys timed out in a row — likely a server/network issue, not key-specific.
|
// 2+ keys timed out in a row — likely a server/network issue, not key-specific.
|
||||||
const cascadeCooldownMs = 3 * 60 * 1000;
|
const cascadeCooldownMs = 3 * 60 * 1000;
|
||||||
logger.warn(`Debrid-Link: ${consecutiveTransportFailures} Transport-Fehler in Folge, ueberspringe verbleibende Keys, Cooldown ${cascadeCooldownMs / 1000}s`);
|
logger.warn(`Debrid-Link: ${consecutiveTransportFailures} Transport-Fehler in Folge, ueberspringe verbleibende Keys, Cooldown ${cascadeCooldownMs / 1000}s`);
|
||||||
logAccountRotation("ERROR", providerName, rotationLabel, "TRANSPORT_CASCADE", {
|
|
||||||
elapsedMs,
|
|
||||||
consecutive: consecutiveTransportFailures,
|
|
||||||
cooldownSec: Math.ceil(cascadeCooldownMs / 1000),
|
|
||||||
link: linkShort
|
|
||||||
});
|
|
||||||
throw new Error(`debrid_link_cooldown:${cascadeCooldownMs}:Debrid-Link: Transport-Kaskade (${consecutiveTransportFailures}x)`);
|
throw new Error(`debrid_link_cooldown:${cascadeCooldownMs}:Debrid-Link: Transport-Kaskade (${consecutiveTransportFailures}x)`);
|
||||||
}
|
}
|
||||||
// Find the next key that will be tried (for clearer log)
|
|
||||||
let nextLabel = "ENDE";
|
|
||||||
for (let nextIdx = keyIdx + 1; nextIdx < this.apiKeys.length; nextIdx += 1) {
|
|
||||||
const nextKey = this.apiKeys[nextIdx];
|
|
||||||
if (!isDebridLinkApiKeyDisabled(settings, nextKey.id) && !isDebridLinkApiKeyDailyLimitReached(settings, nextKey.id) && !getDebridLinkKeyCooldownState(nextKey.id)) {
|
|
||||||
nextLabel = `${nextKey.label}/${totalKeys} (${nextKey.masked})`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const cooldownInfo = failure.cooldownMs > 0
|
const cooldownInfo = failure.cooldownMs > 0
|
||||||
? `, Cooldown ${Math.ceil(failure.cooldownMs / 1000)}s`
|
? `, Cooldown ${Math.ceil(failure.cooldownMs / 1000)}s`
|
||||||
: "";
|
: "";
|
||||||
logger.warn(`Debrid-Link${keyLabel}: ${failure.message}${cooldownInfo}, pruefe naechsten Key (${nextLabel})`);
|
logger.warn(`Debrid-Link${keyLabel}: ${failure.message}${cooldownInfo}, pruefe naechsten Key`);
|
||||||
logAccountRotation("WARN", providerName, rotationLabel, "FAILED", {
|
|
||||||
elapsedMs,
|
|
||||||
reason: failure.message,
|
|
||||||
category: failure.category,
|
|
||||||
cooldownSec: failure.cooldownMs > 0 ? Math.ceil(failure.cooldownMs / 1000) : 0,
|
|
||||||
next: nextLabel,
|
|
||||||
link: linkShort
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7660,15 +7660,6 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only items that are actually DOWNLOADING (have an open HTTP body that
|
|
||||||
// should be making progress) count toward the global stall watchdog.
|
|
||||||
// Items in "validating" are still in the unrestrict phase — they're handled
|
|
||||||
// by the per-item validating watchdog above (VALIDATING_STUCK_MS) which
|
|
||||||
// gives the multi-account/multi-key rotation enough time. Without this
|
|
||||||
// exclusion the global watchdog would abort the unrestrict mid-rotation
|
|
||||||
// (e.g. account 3 of 3 still being tested) just because no bytes had been
|
|
||||||
// downloaded recently — which is correct, since unrestrict doesn't
|
|
||||||
// produce download bytes.
|
|
||||||
let stalledCount = 0;
|
let stalledCount = 0;
|
||||||
let diskBlockedCount = 0;
|
let diskBlockedCount = 0;
|
||||||
for (const active of this.activeTasks.values()) {
|
for (const active of this.activeTasks.values()) {
|
||||||
@ -7680,7 +7671,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const item = this.session.items[active.itemId];
|
const item = this.session.items[active.itemId];
|
||||||
if (item && item.status === "downloading") {
|
if (item && (item.status === "downloading" || item.status === "validating")) {
|
||||||
stalledCount += 1;
|
stalledCount += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -7698,7 +7689,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const item = this.session.items[active.itemId];
|
const item = this.session.items[active.itemId];
|
||||||
if (item && item.status === "downloading") {
|
if (item && (item.status === "downloading" || item.status === "validating")) {
|
||||||
active.abortReason = "stall";
|
active.abortReason = "stall";
|
||||||
active.abortController.abort("stall");
|
active.abortController.abort("stall");
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user