feat(realdebrid): rotate accounts during unrestrict

This commit is contained in:
Sucukdeluxe
2026-08-15 21:16:18 +02:00
parent 53cdac1ded
commit 7e65195057
13 changed files with 1159 additions and 139 deletions
+63 -31
View File
@@ -113,13 +113,41 @@ function looksLikeHtmlResponse(contentType: string, body: string): boolean {
return /^\s*<(!doctype\s+html|html\b)/i.test(String(body || ""));
}
function parseErrorBody(status: number, body: string, contentType: string): string {
if (looksLikeHtmlResponse(contentType, body)) {
return `Real-Debrid lieferte HTML statt JSON (HTTP ${status})`;
}
const clean = compactErrorText(body);
return clean || `HTTP ${status}`;
}
function parseErrorBody(status: number, body: string, contentType: string): RealDebridApiError {
if (looksLikeHtmlResponse(contentType, body)) {
return new RealDebridApiError(status, "html_response", null, "Real-Debrid lieferte HTML statt JSON");
}
if (String(contentType || "").toLowerCase().includes("json") || /^\s*\{/.test(body)) {
try {
const payload = JSON.parse(body) as Record<string, unknown>;
const apiError = String(payload.error || "").trim();
const codeValue = Number(payload.error_code ?? NaN);
const apiErrorCode = Number.isFinite(codeValue) ? Math.floor(codeValue) : null;
if (apiError || apiErrorCode !== null) {
return new RealDebridApiError(status, apiError, apiErrorCode);
}
} catch {
}
}
const clean = compactErrorText(body);
return new RealDebridApiError(status, "", null, clean || `HTTP ${status}`);
}
export class RealDebridApiError extends Error {
public readonly status: number;
public readonly apiError: string;
public readonly apiErrorCode: number | null;
public constructor(status: number, apiError: string, apiErrorCode: number | null, fallbackMessage = "") {
const normalizedError = String(apiError || "").trim();
const codeText = apiErrorCode === null ? "" : ` (${apiErrorCode})`;
super(fallbackMessage || `Real-Debrid HTTP ${status}: ${normalizedError || "API-Fehler"}${codeText}`);
this.name = "RealDebridApiError";
this.status = status;
this.apiError = normalizedError;
this.apiErrorCode = apiErrorCode;
}
}
export class RealDebridClient {
private token: string;
@@ -128,8 +156,8 @@ export class RealDebridClient {
this.token = token;
}
public async unrestrictLink(link: string, signal?: AbortSignal): Promise<UnrestrictedLink> {
let lastError = "";
public async unrestrictLink(link: string, signal?: AbortSignal): Promise<UnrestrictedLink> {
let lastError: unknown = null;
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
try {
const body = new URLSearchParams({ link });
@@ -146,13 +174,13 @@ export class RealDebridClient {
const text = await response.text();
const contentType = String(response.headers.get("content-type") || "");
if (!response.ok) {
const parsed = parseErrorBody(response.status, text, contentType);
if (shouldRetryStatus(response.status) && attempt < REQUEST_RETRIES) {
await sleepWithSignal(retryDelayForResponse(response, attempt), signal);
continue;
}
throw new Error(parsed);
if (!response.ok) {
const parsed = parseErrorBody(response.status, text, contentType);
if (shouldRetryStatus(response.status) && attempt < REQUEST_RETRIES) {
await sleepWithSignal(retryDelayForResponse(response, attempt), signal);
continue;
}
throw parsed;
}
if (looksLikeHtmlResponse(contentType, text)) {
@@ -187,18 +215,22 @@ export class RealDebridClient {
fileSize: Number.isFinite(fileSizeRaw) && fileSizeRaw > 0 ? Math.floor(fileSizeRaw) : null,
retriesUsed: attempt - 1
};
} 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 || "Unrestrict fehlgeschlagen").replace(/^Error:\s*/i, ""));
}
}
} catch (error) {
lastError = error;
const lastErrorText = compactErrorText(error);
if (signal?.aborted || (/aborted/i.test(lastErrorText) && !/timeout/i.test(lastErrorText))) {
break;
}
if (attempt >= REQUEST_RETRIES || !isRetryableErrorText(lastErrorText)) {
break;
}
await sleepWithSignal(retryDelay(attempt), signal);
}
}
if (lastError instanceof Error) {
throw lastError;
}
throw new Error(compactErrorText(lastError) || "Unrestrict fehlgeschlagen");
}
}