From 008f16a05ddc8bea5e20c1189696ceade8ab8e76 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Thu, 5 Mar 2026 16:59:15 +0100 Subject: [PATCH] Add 1Fichier as direct file hoster provider with API key auth Co-Authored-By: Claude Opus 4.6 --- src/main/app-controller.ts | 5 ++- src/main/constants.ts | 1 + src/main/debrid.ts | 91 +++++++++++++++++++++++++++++++++++++- src/main/storage.ts | 10 +++-- src/renderer/App.tsx | 12 +++-- src/shared/types.ts | 3 +- 6 files changed, 111 insertions(+), 11 deletions(-) diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index b372841..dcb90b4 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -106,6 +106,7 @@ export class AppController { || settings.bestToken.trim() || settings.allDebridToken.trim() || (settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim()) + || settings.oneFichierApiKey.trim() ); } @@ -286,7 +287,7 @@ export class AppController { public exportBackup(): string { const settings = { ...this.settings }; - const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaLogin", "megaPassword", "bestToken", "allDebridToken", "ddownloadLogin", "ddownloadPassword"]; + const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaLogin", "megaPassword", "bestToken", "allDebridToken", "ddownloadLogin", "ddownloadPassword", "oneFichierApiKey"]; for (const key of SENSITIVE_KEYS) { const val = settings[key]; if (typeof val === "string" && val.length > 0) { @@ -308,7 +309,7 @@ export class AppController { return { restored: false, message: "Kein gültiges Backup (settings/session fehlen)" }; } const importedSettings = parsed.settings as AppSettings; - const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaLogin", "megaPassword", "bestToken", "allDebridToken", "ddownloadLogin", "ddownloadPassword"]; + const SENSITIVE_KEYS: (keyof AppSettings)[] = ["token", "megaLogin", "megaPassword", "bestToken", "allDebridToken", "ddownloadLogin", "ddownloadPassword", "oneFichierApiKey"]; for (const key of SENSITIVE_KEYS) { const val = (importedSettings as Record)[key]; if (typeof val === "string" && val.startsWith("***")) { diff --git a/src/main/constants.ts b/src/main/constants.ts index c401517..c40b7f4 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -47,6 +47,7 @@ export function defaultSettings(): AppSettings { allDebridToken: "", ddownloadLogin: "", ddownloadPassword: "", + oneFichierApiKey: "", archivePasswordList: "", rememberToken: true, providerPrimary: "realdebrid", diff --git a/src/main/debrid.ts b/src/main/debrid.ts index 73c16a2..400d139 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -11,12 +11,16 @@ 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 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; + const PROVIDER_LABELS: Record = { realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid", - ddownload: "DDownload" + ddownload: "DDownload", + onefichier: "1Fichier" }; interface ProviderUnrestrictedLink extends UnrestrictedLink { @@ -959,6 +963,66 @@ class AllDebridClient { } } +// ── 1Fichier Client ── + +class OneFichierClient { + private apiKey: string; + + public constructor(apiKey: string) { + this.apiKey = apiKey; + } + + public async unrestrictLink(link: string, signal?: AbortSignal): Promise { + if (!ONEFICHIER_URL_RE.test(link)) { + throw new Error("Kein 1Fichier-Link"); + } + + let lastError = ""; + for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) { + if (signal?.aborted) throw new Error("aborted:debrid"); + try { + const res = await fetch(`${ONEFICHIER_API_BASE}/download/get_token.cgi`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.apiKey}` + }, + body: JSON.stringify({ url: link, pretty: 1 }), + signal: withTimeoutSignal(signal, API_TIMEOUT_MS) + }); + + const json = await res.json() as Record; + + if (json.status === "KO" || json.error) { + const msg = String(json.message || json.error || "Unbekannter 1Fichier-Fehler"); + throw new Error(msg); + } + + const directUrl = String(json.url || ""); + if (!directUrl) { + throw new Error("1Fichier: Keine Download-URL in Antwort"); + } + + return { + fileName: filenameFromUrl(directUrl) || filenameFromUrl(link), + directUrl, + fileSize: null, + retriesUsed: attempt - 1 + }; + } catch (error) { + lastError = compactErrorText(error); + if (signal?.aborted || (/aborted/i.test(lastError) && !/timeout/i.test(lastError))) { + throw error; + } + if (attempt < REQUEST_RETRIES) { + await sleep(retryDelay(attempt), signal); + } + } + } + throw new Error(`1Fichier-Unrestrict fehlgeschlagen: ${lastError}`); + } +} + const DDOWNLOAD_URL_RE = /^https?:\/\/(?:www\.)?(?:ddownload\.com|ddl\.to)\/([a-z0-9]+)/i; const DDOWNLOAD_WEB_BASE = "https://ddownload.com"; const DDOWNLOAD_WEB_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"; @@ -1229,6 +1293,25 @@ export class DebridService { public async unrestrictLink(link: string, signal?: AbortSignal, settingsSnapshot?: AppSettings): Promise { const settings = settingsSnapshot ? cloneSettings(settingsSnapshot) : cloneSettings(this.settings); + // 1Fichier is a direct file hoster. If the link is a 1fichier.com URL + // and the API key is configured, use 1Fichier directly before debrid providers. + if (ONEFICHIER_URL_RE.test(link) && this.isProviderConfiguredFor(settings, "onefichier")) { + try { + const result = await this.unrestrictViaProvider(settings, "onefichier", link, signal); + return { + ...result, + provider: "onefichier", + providerLabel: PROVIDER_LABELS["onefichier"] + }; + } catch (error) { + const errorText = compactErrorText(error); + if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) { + throw error; + } + // Fall through to normal provider chain + } + } + // DDownload is a direct file hoster, not a debrid service. // If the link is a ddownload.com/ddl.to URL and the account is configured, // use DDownload directly before trying any debrid providers. @@ -1337,6 +1420,9 @@ export class DebridService { if (provider === "ddownload") { return Boolean(settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim()); } + if (provider === "onefichier") { + return Boolean(settings.oneFichierApiKey.trim()); + } return Boolean(settings.bestToken.trim()); } @@ -1353,6 +1439,9 @@ export class DebridService { if (provider === "ddownload") { return this.getDdownloadClient(settings.ddownloadLogin, settings.ddownloadPassword).unrestrictLink(link, signal); } + if (provider === "onefichier") { + return new OneFichierClient(settings.oneFichierApiKey).unrestrictLink(link, signal); + } return new BestDebridClient(settings.bestToken).unrestrictLink(link, signal); } } diff --git a/src/main/storage.ts b/src/main/storage.ts index cbc3d73..498c017 100644 --- a/src/main/storage.ts +++ b/src/main/storage.ts @@ -5,8 +5,8 @@ import { AppSettings, BandwidthScheduleEntry, DebridProvider, DownloadItem, Down import { defaultSettings } from "./constants"; import { logger } from "./logger"; -const VALID_PRIMARY_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload"]); -const VALID_FALLBACK_PROVIDERS = new Set(["none", "realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload"]); +const VALID_PRIMARY_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier"]); +const VALID_FALLBACK_PROVIDERS = new Set(["none", "realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier"]); const VALID_CLEANUP_MODES = new Set(["none", "trash", "delete"]); const VALID_CONFLICT_MODES = new Set(["overwrite", "skip", "rename", "ask"]); const VALID_FINISHED_POLICIES = new Set(["never", "immediate", "on_start", "package_done"]); @@ -17,7 +17,7 @@ const VALID_PACKAGE_PRIORITIES = new Set(["high", "normal", "low"]); const VALID_DOWNLOAD_STATUSES = new Set([ "queued", "validating", "downloading", "paused", "reconnect_wait", "extracting", "integrity_check", "completed", "failed", "cancelled" ]); -const VALID_ITEM_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload"]); +const VALID_ITEM_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier"]); const VALID_ONLINE_STATUSES = new Set(["online", "offline", "checking"]); function asText(value: unknown): string { @@ -113,6 +113,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings { allDebridToken: asText(settings.allDebridToken), ddownloadLogin: asText(settings.ddownloadLogin), ddownloadPassword: asText(settings.ddownloadPassword), + oneFichierApiKey: asText(settings.oneFichierApiKey), archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n|\r/g, "\n"), rememberToken: Boolean(settings.rememberToken), providerPrimary: settings.providerPrimary, @@ -204,7 +205,8 @@ function sanitizeCredentialPersistence(settings: AppSettings): AppSettings { bestToken: "", allDebridToken: "", ddownloadLogin: "", - ddownloadPassword: "" + ddownloadPassword: "", + oneFichierApiKey: "" }; } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 5df2b6c..583e48e 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -62,7 +62,7 @@ const emptyStats = (): DownloadStats => ({ const emptySnapshot = (): UiSnapshot => ({ settings: { - token: "", megaLogin: "", megaPassword: "", bestToken: "", allDebridToken: "", ddownloadLogin: "", ddownloadPassword: "", + token: "", megaLogin: "", megaPassword: "", bestToken: "", allDebridToken: "", ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "", archivePasswordList: "", rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid", providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "", @@ -94,7 +94,7 @@ const cleanupLabels: Record = { const AUTO_RENDER_PACKAGE_LIMIT = 260; const providerLabels: Record = { - realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid", ddownload: "DDownload" + realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid", ddownload: "DDownload", onefichier: "1Fichier" }; function formatDateTime(ts: number): string { @@ -930,7 +930,11 @@ export function App(): ReactElement { Boolean((settingsDraft.ddownloadLogin || "").trim() && (settingsDraft.ddownloadPassword || "").trim()), [settingsDraft.ddownloadLogin, settingsDraft.ddownloadPassword]); - const totalConfiguredAccounts = configuredProviders.length + (hasDdownloadAccount ? 1 : 0); + const hasOneFichierAccount = useMemo(() => + Boolean((settingsDraft.oneFichierApiKey || "").trim()), + [settingsDraft.oneFichierApiKey]); + + const totalConfiguredAccounts = configuredProviders.length + (hasDdownloadAccount ? 1 : 0) + (hasOneFichierAccount ? 1 : 0); const primaryProviderValue: DebridProvider = useMemo(() => { if (configuredProviders.includes(settingsDraft.providerPrimary)) { @@ -2744,6 +2748,8 @@ export function App(): ReactElement { setText("ddownloadLogin", e.target.value)} /> setText("ddownloadPassword", e.target.value)} /> + + setText("oneFichierApiKey", e.target.value)} /> {configuredProviders.length === 0 && (
Füge mindestens einen Account hinzu, dann erscheint die Hoster-Auswahl.
)} diff --git a/src/shared/types.ts b/src/shared/types.ts index 407b083..b6283a1 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -14,7 +14,7 @@ export type CleanupMode = "none" | "trash" | "delete"; export type ConflictMode = "overwrite" | "skip" | "rename" | "ask"; export type SpeedMode = "global" | "per_download"; export type FinishedCleanupPolicy = "never" | "immediate" | "on_start" | "package_done"; -export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload"; +export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier"; export type DebridFallbackProvider = DebridProvider | "none"; export type AppTheme = "dark" | "light"; export type PackagePriority = "high" | "normal" | "low"; @@ -44,6 +44,7 @@ export interface AppSettings { allDebridToken: string; ddownloadLogin: string; ddownloadPassword: string; + oneFichierApiKey: string; archivePasswordList: string; rememberToken: boolean; providerPrimary: DebridProvider;