feat(realdebrid): rotate accounts during unrestrict
This commit is contained in:
+63
-31
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user