Add 1Fichier as direct file hoster provider with API key auth
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
927013d9a6
commit
008f16a05d
@ -106,6 +106,7 @@ export class AppController {
|
|||||||
|| settings.bestToken.trim()
|
|| settings.bestToken.trim()
|
||||||
|| settings.allDebridToken.trim()
|
|| settings.allDebridToken.trim()
|
||||||
|| (settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim())
|
|| (settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim())
|
||||||
|
|| settings.oneFichierApiKey.trim()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,7 +287,7 @@ export class AppController {
|
|||||||
|
|
||||||
public exportBackup(): string {
|
public exportBackup(): string {
|
||||||
const settings = { ...this.settings };
|
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) {
|
for (const key of SENSITIVE_KEYS) {
|
||||||
const val = settings[key];
|
const val = settings[key];
|
||||||
if (typeof val === "string" && val.length > 0) {
|
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)" };
|
return { restored: false, message: "Kein gültiges Backup (settings/session fehlen)" };
|
||||||
}
|
}
|
||||||
const importedSettings = parsed.settings as AppSettings;
|
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) {
|
for (const key of SENSITIVE_KEYS) {
|
||||||
const val = (importedSettings as Record<string, unknown>)[key];
|
const val = (importedSettings as Record<string, unknown>)[key];
|
||||||
if (typeof val === "string" && val.startsWith("***")) {
|
if (typeof val === "string" && val.startsWith("***")) {
|
||||||
|
|||||||
@ -47,6 +47,7 @@ export function defaultSettings(): AppSettings {
|
|||||||
allDebridToken: "",
|
allDebridToken: "",
|
||||||
ddownloadLogin: "",
|
ddownloadLogin: "",
|
||||||
ddownloadPassword: "",
|
ddownloadPassword: "",
|
||||||
|
oneFichierApiKey: "",
|
||||||
archivePasswordList: "",
|
archivePasswordList: "",
|
||||||
rememberToken: true,
|
rememberToken: true,
|
||||||
providerPrimary: "realdebrid",
|
providerPrimary: "realdebrid",
|
||||||
|
|||||||
@ -11,12 +11,16 @@ const RAPIDGATOR_SCAN_MAX_BYTES = 512 * 1024;
|
|||||||
const BEST_DEBRID_API_BASE = "https://bestdebrid.com/api/v1";
|
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 = "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<DebridProvider, string> = {
|
const PROVIDER_LABELS: Record<DebridProvider, string> = {
|
||||||
realdebrid: "Real-Debrid",
|
realdebrid: "Real-Debrid",
|
||||||
megadebrid: "Mega-Debrid",
|
megadebrid: "Mega-Debrid",
|
||||||
bestdebrid: "BestDebrid",
|
bestdebrid: "BestDebrid",
|
||||||
alldebrid: "AllDebrid",
|
alldebrid: "AllDebrid",
|
||||||
ddownload: "DDownload"
|
ddownload: "DDownload",
|
||||||
|
onefichier: "1Fichier"
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ProviderUnrestrictedLink extends UnrestrictedLink {
|
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<UnrestrictedLink> {
|
||||||
|
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<string, unknown>;
|
||||||
|
|
||||||
|
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_URL_RE = /^https?:\/\/(?:www\.)?(?:ddownload\.com|ddl\.to)\/([a-z0-9]+)/i;
|
||||||
const DDOWNLOAD_WEB_BASE = "https://ddownload.com";
|
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";
|
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<ProviderUnrestrictedLink> {
|
public async unrestrictLink(link: string, signal?: AbortSignal, settingsSnapshot?: AppSettings): Promise<ProviderUnrestrictedLink> {
|
||||||
const settings = settingsSnapshot ? cloneSettings(settingsSnapshot) : cloneSettings(this.settings);
|
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.
|
// 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,
|
// If the link is a ddownload.com/ddl.to URL and the account is configured,
|
||||||
// use DDownload directly before trying any debrid providers.
|
// use DDownload directly before trying any debrid providers.
|
||||||
@ -1337,6 +1420,9 @@ export class DebridService {
|
|||||||
if (provider === "ddownload") {
|
if (provider === "ddownload") {
|
||||||
return Boolean(settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim());
|
return Boolean(settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim());
|
||||||
}
|
}
|
||||||
|
if (provider === "onefichier") {
|
||||||
|
return Boolean(settings.oneFichierApiKey.trim());
|
||||||
|
}
|
||||||
return Boolean(settings.bestToken.trim());
|
return Boolean(settings.bestToken.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1353,6 +1439,9 @@ export class DebridService {
|
|||||||
if (provider === "ddownload") {
|
if (provider === "ddownload") {
|
||||||
return this.getDdownloadClient(settings.ddownloadLogin, settings.ddownloadPassword).unrestrictLink(link, signal);
|
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);
|
return new BestDebridClient(settings.bestToken).unrestrictLink(link, signal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import { AppSettings, BandwidthScheduleEntry, DebridProvider, DownloadItem, Down
|
|||||||
import { defaultSettings } from "./constants";
|
import { defaultSettings } from "./constants";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
|
|
||||||
const VALID_PRIMARY_PROVIDERS = new Set(["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"]);
|
const VALID_FALLBACK_PROVIDERS = new Set(["none", "realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier"]);
|
||||||
const VALID_CLEANUP_MODES = new Set(["none", "trash", "delete"]);
|
const VALID_CLEANUP_MODES = new Set(["none", "trash", "delete"]);
|
||||||
const VALID_CONFLICT_MODES = new Set(["overwrite", "skip", "rename", "ask"]);
|
const VALID_CONFLICT_MODES = new Set(["overwrite", "skip", "rename", "ask"]);
|
||||||
const VALID_FINISHED_POLICIES = new Set(["never", "immediate", "on_start", "package_done"]);
|
const VALID_FINISHED_POLICIES = new Set(["never", "immediate", "on_start", "package_done"]);
|
||||||
@ -17,7 +17,7 @@ const VALID_PACKAGE_PRIORITIES = new Set<string>(["high", "normal", "low"]);
|
|||||||
const VALID_DOWNLOAD_STATUSES = new Set<DownloadStatus>([
|
const VALID_DOWNLOAD_STATUSES = new Set<DownloadStatus>([
|
||||||
"queued", "validating", "downloading", "paused", "reconnect_wait", "extracting", "integrity_check", "completed", "failed", "cancelled"
|
"queued", "validating", "downloading", "paused", "reconnect_wait", "extracting", "integrity_check", "completed", "failed", "cancelled"
|
||||||
]);
|
]);
|
||||||
const VALID_ITEM_PROVIDERS = new Set<DebridProvider>(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload"]);
|
const VALID_ITEM_PROVIDERS = new Set<DebridProvider>(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier"]);
|
||||||
const VALID_ONLINE_STATUSES = new Set(["online", "offline", "checking"]);
|
const VALID_ONLINE_STATUSES = new Set(["online", "offline", "checking"]);
|
||||||
|
|
||||||
function asText(value: unknown): string {
|
function asText(value: unknown): string {
|
||||||
@ -113,6 +113,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
|||||||
allDebridToken: asText(settings.allDebridToken),
|
allDebridToken: asText(settings.allDebridToken),
|
||||||
ddownloadLogin: asText(settings.ddownloadLogin),
|
ddownloadLogin: asText(settings.ddownloadLogin),
|
||||||
ddownloadPassword: asText(settings.ddownloadPassword),
|
ddownloadPassword: asText(settings.ddownloadPassword),
|
||||||
|
oneFichierApiKey: asText(settings.oneFichierApiKey),
|
||||||
archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n|\r/g, "\n"),
|
archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n|\r/g, "\n"),
|
||||||
rememberToken: Boolean(settings.rememberToken),
|
rememberToken: Boolean(settings.rememberToken),
|
||||||
providerPrimary: settings.providerPrimary,
|
providerPrimary: settings.providerPrimary,
|
||||||
@ -204,7 +205,8 @@ function sanitizeCredentialPersistence(settings: AppSettings): AppSettings {
|
|||||||
bestToken: "",
|
bestToken: "",
|
||||||
allDebridToken: "",
|
allDebridToken: "",
|
||||||
ddownloadLogin: "",
|
ddownloadLogin: "",
|
||||||
ddownloadPassword: ""
|
ddownloadPassword: "",
|
||||||
|
oneFichierApiKey: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -62,7 +62,7 @@ const emptyStats = (): DownloadStats => ({
|
|||||||
|
|
||||||
const emptySnapshot = (): UiSnapshot => ({
|
const emptySnapshot = (): UiSnapshot => ({
|
||||||
settings: {
|
settings: {
|
||||||
token: "", megaLogin: "", megaPassword: "", bestToken: "", allDebridToken: "", ddownloadLogin: "", ddownloadPassword: "",
|
token: "", megaLogin: "", megaPassword: "", bestToken: "", allDebridToken: "", ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "",
|
||||||
archivePasswordList: "",
|
archivePasswordList: "",
|
||||||
rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid",
|
rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid",
|
||||||
providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "",
|
providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "",
|
||||||
@ -94,7 +94,7 @@ const cleanupLabels: Record<string, string> = {
|
|||||||
const AUTO_RENDER_PACKAGE_LIMIT = 260;
|
const AUTO_RENDER_PACKAGE_LIMIT = 260;
|
||||||
|
|
||||||
const providerLabels: Record<DebridProvider, string> = {
|
const providerLabels: Record<DebridProvider, string> = {
|
||||||
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 {
|
function formatDateTime(ts: number): string {
|
||||||
@ -930,7 +930,11 @@ export function App(): ReactElement {
|
|||||||
Boolean((settingsDraft.ddownloadLogin || "").trim() && (settingsDraft.ddownloadPassword || "").trim()),
|
Boolean((settingsDraft.ddownloadLogin || "").trim() && (settingsDraft.ddownloadPassword || "").trim()),
|
||||||
[settingsDraft.ddownloadLogin, settingsDraft.ddownloadPassword]);
|
[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(() => {
|
const primaryProviderValue: DebridProvider = useMemo(() => {
|
||||||
if (configuredProviders.includes(settingsDraft.providerPrimary)) {
|
if (configuredProviders.includes(settingsDraft.providerPrimary)) {
|
||||||
@ -2744,6 +2748,8 @@ export function App(): ReactElement {
|
|||||||
<input value={settingsDraft.ddownloadLogin || ""} onChange={(e) => setText("ddownloadLogin", e.target.value)} />
|
<input value={settingsDraft.ddownloadLogin || ""} onChange={(e) => setText("ddownloadLogin", e.target.value)} />
|
||||||
<label>DDownload Passwort</label>
|
<label>DDownload Passwort</label>
|
||||||
<input type="password" value={settingsDraft.ddownloadPassword || ""} onChange={(e) => setText("ddownloadPassword", e.target.value)} />
|
<input type="password" value={settingsDraft.ddownloadPassword || ""} onChange={(e) => setText("ddownloadPassword", e.target.value)} />
|
||||||
|
<label>1Fichier API Key</label>
|
||||||
|
<input type="password" value={settingsDraft.oneFichierApiKey || ""} onChange={(e) => setText("oneFichierApiKey", e.target.value)} />
|
||||||
{configuredProviders.length === 0 && (
|
{configuredProviders.length === 0 && (
|
||||||
<div className="hint">Füge mindestens einen Account hinzu, dann erscheint die Hoster-Auswahl.</div>
|
<div className="hint">Füge mindestens einen Account hinzu, dann erscheint die Hoster-Auswahl.</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export type CleanupMode = "none" | "trash" | "delete";
|
|||||||
export type ConflictMode = "overwrite" | "skip" | "rename" | "ask";
|
export type ConflictMode = "overwrite" | "skip" | "rename" | "ask";
|
||||||
export type SpeedMode = "global" | "per_download";
|
export type SpeedMode = "global" | "per_download";
|
||||||
export type FinishedCleanupPolicy = "never" | "immediate" | "on_start" | "package_done";
|
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 DebridFallbackProvider = DebridProvider | "none";
|
||||||
export type AppTheme = "dark" | "light";
|
export type AppTheme = "dark" | "light";
|
||||||
export type PackagePriority = "high" | "normal" | "low";
|
export type PackagePriority = "high" | "normal" | "low";
|
||||||
@ -44,6 +44,7 @@ export interface AppSettings {
|
|||||||
allDebridToken: string;
|
allDebridToken: string;
|
||||||
ddownloadLogin: string;
|
ddownloadLogin: string;
|
||||||
ddownloadPassword: string;
|
ddownloadPassword: string;
|
||||||
|
oneFichierApiKey: string;
|
||||||
archivePasswordList: string;
|
archivePasswordList: string;
|
||||||
rememberToken: boolean;
|
rememberToken: boolean;
|
||||||
providerPrimary: DebridProvider;
|
providerPrimary: DebridProvider;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user