Release v1.6.69

This commit is contained in:
Sucukdeluxe
2026-03-06 04:17:22 +01:00
parent 18e4b6cd58
commit e4a60a033b
16 changed files with 960 additions and 14 deletions
+156 -2
View File
@@ -1,4 +1,4 @@
import { AppSettings, DebridFallbackProvider, DebridProvider } from "../shared/types";
import { AllDebridHostInfo, AppSettings, DebridFallbackProvider, DebridProvider } from "../shared/types";
import { APP_VERSION, REQUEST_RETRIES } from "./constants";
import { logger } from "./logger";
import { RealDebridClient, UnrestrictedLink } from "./realdebrid";
@@ -10,6 +10,7 @@ const RAPIDGATOR_SCAN_MAX_BYTES = 512 * 1024;
const BEST_DEBRID_API_BASE = "https://bestdebrid.com/api/v1";
const ALL_DEBRID_API_BASE = "https://api.alldebrid.com/v4";
const ALL_DEBRID_API_BASE_V41 = "https://api.alldebrid.com/v4.1";
const ONEFICHIER_API_BASE = "https://api.1fichier.com/v1";
const ONEFICHIER_URL_RE = /^https?:\/\/(?:www\.)?(?:1fichier\.com|alterupload\.com|cjoint\.net|desfichiers\.com|dfichiers\.com|megadl\.fr|mesfichiers\.org|piecejointe\.net|pjointe\.com|tenvoi\.com|dl4free\.com)\/\?([a-z0-9]{5,20})$/i;
@@ -29,9 +30,11 @@ interface ProviderUnrestrictedLink extends UnrestrictedLink {
}
export type MegaWebUnrestrictor = (link: string, signal?: AbortSignal) => Promise<UnrestrictedLink | null>;
export type AllDebridWebUnrestrictor = (link: string, signal?: AbortSignal) => Promise<UnrestrictedLink | null>;
interface DebridServiceOptions {
megaWebUnrestrict?: MegaWebUnrestrictor;
allDebridWebUnrestrict?: AllDebridWebUnrestrictor;
}
function cloneSettings(settings: AppSettings): AppSettings {
@@ -201,6 +204,43 @@ function parseAllDebridError(payload: Record<string, unknown> | null): string {
return pickString(errorObj, ["message", "code"]) || "AllDebrid API error";
}
function normalizeAllDebridHostKey(value: string): string {
return String(value || "").replace(/[^a-z0-9]+/gi, "").toLowerCase();
}
function toAllDebridHostState(value: unknown): AllDebridHostInfo["state"] {
if (value === true) {
return "up";
}
if (value === false) {
return "down";
}
const normalized = String(value || "").trim().toLowerCase();
if (normalized === "up" || normalized === "online" || normalized === "available") {
return "up";
}
if (normalized === "down" || normalized === "offline" || normalized === "unavailable") {
return "down";
}
if (normalized === "not_tracked" || normalized === "not tracked") {
return "not_tracked";
}
return "unknown";
}
function toAllDebridHostStatusLabel(state: AllDebridHostInfo["state"]): string {
if (state === "up") {
return "Verfügbar";
}
if (state === "down") {
return "Unverfügbar";
}
if (state === "not_tracked") {
return "Nicht getrackt";
}
return "Unbekannt";
}
function uniqueProviderOrder(order: DebridProvider[]): DebridProvider[] {
const seen = new Set<DebridProvider>();
const result: DebridProvider[] = [];
@@ -886,6 +926,105 @@ class AllDebridClient {
return result;
}
public async getHostInfo(host: string, signal?: AbortSignal): Promise<AllDebridHostInfo> {
const wanted = normalizeAllDebridHostKey(host);
let lastError = "";
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
try {
const response = await fetch(`${ALL_DEBRID_API_BASE_V41}/user/hosts`, {
method: "GET",
headers: {
Authorization: `Bearer ${this.token}`,
"User-Agent": DEBRID_USER_AGENT
},
signal: withTimeoutSignal(signal, API_TIMEOUT_MS)
});
const text = await response.text();
const payload = asRecord(parseJson(text));
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);
}
const contentType = String(response.headers.get("content-type") || "").toLowerCase();
const looksHtml = contentType.includes("text/html") || /^\s*<(!doctype\s+html|html\b)/i.test(text);
if (looksHtml) {
throw new Error("AllDebrid lieferte HTML statt JSON");
}
if (!payload) {
throw new Error("AllDebrid Antwort ist kein JSON-Objekt");
}
const status = pickString(payload, ["status"]);
if (status && status.toLowerCase() === "error") {
throw new Error(parseAllDebridError(payload));
}
const data = asRecord(payload.data);
const hosts = asRecord(data?.hosts);
if (!hosts) {
throw new Error("AllDebrid Antwort ohne Host-Liste");
}
let hostEntry = asRecord(hosts[host]) || asRecord(hosts[wanted]);
if (!hostEntry) {
for (const entry of Object.values(hosts)) {
const candidate = asRecord(entry);
const candidateName = normalizeAllDebridHostKey(pickString(candidate, ["name"]));
if (candidateName === wanted) {
hostEntry = candidate;
break;
}
}
}
if (!hostEntry) {
throw new Error(`AllDebrid Host ${host} nicht gefunden`);
}
const state = toAllDebridHostState(hostEntry.status);
const quota = pickNumber(hostEntry, ["quota"]);
const quotaMax = pickNumber(hostEntry, ["quotaMax"]);
const limitSimuDl = pickNumber(hostEntry, ["limitSimuDl"]);
const quotaType = pickString(hostEntry, ["quotaType"]);
const note = quota === null && quotaMax === null && limitSimuDl === null
? "AllDebrid liefert für diesen Host aktuell keine Quota- oder Slot-Daten."
: "";
return {
host: pickString(hostEntry, ["name"]) || host,
source: "api",
state,
statusLabel: toAllDebridHostStatusLabel(state),
fetchedAt: Date.now(),
lastCheckedAt: null,
quota,
quotaMax,
quotaType,
limitSimuDl,
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 || "AllDebrid Host-Info fehlgeschlagen").replace(/^Error:\s*/i, ""));
}
public async unrestrictLink(link: string, signal?: AbortSignal): Promise<UnrestrictedLink> {
let lastError = "";
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
@@ -963,6 +1102,10 @@ class AllDebridClient {
}
}
export async function fetchAllDebridHostInfo(token: string, host = "rapidgator", signal?: AbortSignal): Promise<AllDebridHostInfo> {
return new AllDebridClient(token).getHostInfo(host, signal);
}
// ── 1Fichier Client ──
class OneFichierClient {
@@ -1290,6 +1433,10 @@ export class DebridService {
return clean;
}
private shouldUseAllDebridWeb(settings: AppSettings): boolean {
return Boolean(settings.allDebridUseWebLogin && this.options.allDebridWebUnrestrict);
}
public async unrestrictLink(link: string, signal?: AbortSignal, settingsSnapshot?: AppSettings): Promise<ProviderUnrestrictedLink> {
const settings = settingsSnapshot ? cloneSettings(settingsSnapshot) : cloneSettings(this.settings);
@@ -1415,7 +1562,7 @@ export class DebridService {
return Boolean(settings.megaLogin.trim() && settings.megaPassword.trim() && this.options.megaWebUnrestrict);
}
if (provider === "alldebrid") {
return Boolean(settings.allDebridToken.trim());
return Boolean(this.shouldUseAllDebridWeb(settings) || settings.allDebridToken.trim());
}
if (provider === "ddownload") {
return Boolean(settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim());
@@ -1434,6 +1581,13 @@ export class DebridService {
return new MegaDebridClient(this.options.megaWebUnrestrict).unrestrictLink(link, signal);
}
if (provider === "alldebrid") {
if (this.shouldUseAllDebridWeb(settings) && this.options.allDebridWebUnrestrict) {
const result = await this.options.allDebridWebUnrestrict(link, signal);
if (!result) {
throw new Error("AllDebrid-Web-Fallback nicht verfügbar");
}
return result;
}
return new AllDebridClient(settings.allDebridToken).unrestrictLink(link, signal);
}
if (provider === "ddownload") {