From c811649b9d8ad5125ce8a3942357213fce375cc8 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 6 Mar 2026 19:14:16 +0100 Subject: [PATCH] feat: add per-hoster provider routing (Hoster-Zuordnung) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New settings field hosterRouting maps file hosters to specific debrid providers - 27 known hosters predefined (Rapidgator, Uploaded, Turbobit, Nitroflare, etc.) - Custom hoster support via prompt dialog - Routing takes priority over default provider chain - Falls back to normal chain on error when autoProviderFallback is enabled - Logs routing decisions: "Hoster-Zuordnung: rapidgator → Debrid-Link" - Full UI section in settings with add/remove/change provider per hoster - Storage validation and normalization for hosterRouting Co-Authored-By: Claude Opus 4.6 --- src/main/constants.ts | 3 +- src/main/debrid.ts | 46 +++++++++++++++ src/main/storage.ts | 16 +++++- src/renderer/App.tsx | 120 +++++++++++++++++++++++++++++++++++++++- src/renderer/styles.css | 63 +++++++++++++++++++++ src/shared/types.ts | 1 + 6 files changed, 246 insertions(+), 3 deletions(-) diff --git a/src/main/constants.ts b/src/main/constants.ts index b956533..fddcea3 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -98,6 +98,7 @@ export function defaultSettings(): AppSettings { columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"], extractCpuPriority: "high", autoExtractWhenStopped: true, - disabledProviders: [] + disabledProviders: [], + hosterRouting: {} }; } diff --git a/src/main/debrid.ts b/src/main/debrid.ts index fe0a7bb..dc37cb7 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -33,6 +33,16 @@ const PROVIDER_LABELS: Record = { linksnappy: "LinkSnappy" }; +function extractHosterFromUrl(url: string): string { + try { + const host = new URL(url).hostname.replace(/^www\./, "").toLowerCase(); + const parts = host.split("."); + return parts.length >= 2 ? parts[parts.length - 2] : host; + } catch { + return ""; + } +} + interface ProviderUnrestrictedLink extends UnrestrictedLink { provider: DebridProvider; providerLabel: string; @@ -1875,6 +1885,42 @@ export class DebridService { public async unrestrictLink(link: string, signal?: AbortSignal, settingsSnapshot?: AppSettings): Promise { const settings = settingsSnapshot ? cloneSettings(settingsSnapshot) : cloneSettings(this.settings); + // Hoster-Zuordnung: prüfe ob für diesen Hoster ein bestimmter Provider konfiguriert ist + const routing = settings.hosterRouting || {}; + const hosterKey = extractHosterFromUrl(link); + if (hosterKey && routing[hosterKey]) { + const routedProvider = routing[hosterKey]; + if (this.isProviderConfiguredFor(settings, routedProvider)) { + logger.info(`Hoster-Zuordnung: ${hosterKey} → ${PROVIDER_LABELS[routedProvider]}`); + try { + const result = await this.unrestrictViaProvider(settings, routedProvider, link, signal); + let fileName = result.fileName; + if (isRapidgatorLink(link) && looksLikeOpaqueFilename(fileName || filenameFromUrl(link))) { + const fromPage = await resolveRapidgatorFilename(link, signal); + if (fromPage) fileName = fromPage; + } + return { + ...result, + fileName, + provider: routedProvider, + providerLabel: PROVIDER_LABELS[routedProvider] + (result.sourceLabel ? ` (${result.sourceLabel})` : "") + }; + } catch (error) { + const errorText = compactErrorText(error); + if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) { + throw error; + } + if (!settings.autoProviderFallback) { + throw new Error(`Hoster-Zuordnung fehlgeschlagen (${hosterKey} → ${PROVIDER_LABELS[routedProvider]}): ${errorText}`); + } + logger.warn(`Hoster-Zuordnung ${hosterKey} → ${PROVIDER_LABELS[routedProvider]} fehlgeschlagen, Fallback auf Provider-Kette: ${errorText}`); + // Fall through to normal provider chain + } + } else { + logger.warn(`Hoster-Zuordnung ${hosterKey} → ${PROVIDER_LABELS[routedProvider]} übersprungen (Provider nicht konfiguriert/deaktiviert)`); + } + } + // 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")) { diff --git a/src/main/storage.ts b/src/main/storage.ts index 8df22ee..3334805 100644 --- a/src/main/storage.ts +++ b/src/main/storage.ts @@ -91,6 +91,19 @@ function normalizeColumnOrder(raw: unknown): string[] { return result; } +function normalizeHosterRouting(raw: unknown): Record { + if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}; + const result: Record = {}; + for (const [key, value] of Object.entries(raw as Record)) { + const hoster = String(key).trim().toLowerCase(); + const provider = String(value ?? "").trim(); + if (hoster && VALID_PRIMARY_PROVIDERS.has(provider)) { + result[hoster] = provider as DebridProvider; + } + } + return result; +} + const DEPRECATED_UPDATE_REPOS = new Set([ "sucukdeluxe/real-debrid-downloader" ]); @@ -164,7 +177,8 @@ export function normalizeSettings(settings: AppSettings): AppSettings { columnOrder: normalizeColumnOrder(settings.columnOrder), extractCpuPriority: settings.extractCpuPriority, autoExtractWhenStopped: settings.autoExtractWhenStopped !== undefined ? Boolean(settings.autoExtractWhenStopped) : defaults.autoExtractWhenStopped, - disabledProviders: Array.isArray(settings.disabledProviders) ? settings.disabledProviders.filter((p: unknown) => VALID_PRIMARY_PROVIDERS.has(p as string)) as DebridProvider[] : [] + disabledProviders: Array.isArray(settings.disabledProviders) ? settings.disabledProviders.filter((p: unknown) => VALID_PRIMARY_PROVIDERS.has(p as string)) as DebridProvider[] : [], + hosterRouting: normalizeHosterRouting(settings.hosterRouting) }; if (!VALID_PRIMARY_PROVIDERS.has(normalized.providerPrimary)) { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index aed70e5..8c32f55 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -536,7 +536,9 @@ const emptySnapshot = (): UiSnapshot => ({ theme: "dark", collapseNewPackages: true, autoSkipExtracted: false, confirmDeleteSelection: true, bandwidthSchedules: [], totalDownloadedAllTime: 0, columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"], - autoExtractWhenStopped: true + autoExtractWhenStopped: true, + disabledProviders: [], + hosterRouting: {} }, session: { version: 2, packageOrder: [], packages: {}, items: {}, runStartedAt: 0, @@ -557,6 +559,36 @@ const providerLabels: Record = { realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid", ddownload: "DDownload", onefichier: "1Fichier", debridlink: "Debrid-Link", linksnappy: "LinkSnappy" }; +const KNOWN_HOSTERS: { id: string; label: string }[] = [ + { id: "rapidgator", label: "Rapidgator" }, + { id: "uploaded", label: "Uploaded" }, + { id: "1fichier", label: "1Fichier" }, + { id: "ddownload", label: "DDownload" }, + { id: "ddl", label: "DDL.to" }, + { id: "turbobit", label: "Turbobit" }, + { id: "nitroflare", label: "Nitroflare" }, + { id: "filefactory", label: "FileFactory" }, + { id: "katfile", label: "Katfile" }, + { id: "hitfile", label: "Hitfile" }, + { id: "alfafile", label: "Alfafile" }, + { id: "k2s", label: "Keep2Share" }, + { id: "keep2share", label: "Keep2Share (alt)" }, + { id: "tezfiles", label: "Tezfiles" }, + { id: "fileboom", label: "Fileboom" }, + { id: "mexashare", label: "Mexashare" }, + { id: "wdupload", label: "WDUpload" }, + { id: "rosefile", label: "Rosefile" }, + { id: "filejoker", label: "FileJoker" }, + { id: "worldbytez", label: "Worldbytez" }, + { id: "fileland", label: "Fileland" }, + { id: "depositfiles", label: "DepositFiles" }, + { id: "mediafire", label: "MediaFire" }, + { id: "mega", label: "Mega.nz" }, + { id: "frdl", label: "FreeDownload" }, + { id: "hexupload", label: "HexUpload" }, + { id: "isra", label: "Isra.cloud" } +]; + function providerLabelWithMode(provider: DebridProvider, settings: AppSettings): string { const base = providerLabels[provider]; const kind = getConfiguredAccountKind(settings, provider); @@ -3716,6 +3748,92 @@ export function App(): ReactElement { + {configuredProviders.length >= 1 && ( +
+

Hoster-Zuordnung

+
Lege fest, welcher Debrid-Provider sich um welchen Filehoster kümmert. Nicht zugeordnete Hoster nutzen die Standard-Reihenfolge oben.
+ {(() => { + const routing: Record = settingsDraft.hosterRouting || {}; + const routingEntries = Object.entries(routing).sort(([a], [b]) => a.localeCompare(b)); + const usedHosters = new Set(routingEntries.map(([h]) => h)); + const availableHosters = KNOWN_HOSTERS.filter((h) => !usedHosters.has(h.id)); + + const setRouting = (newRouting: Record) => { + setSettingsDraft((prev) => ({ ...prev, hosterRouting: newRouting })); + }; + + const addEntry = (hosterId: string) => { + if (!hosterId || routing[hosterId]) return; + setRouting({ ...routing, [hosterId]: configuredProviders[0] }); + }; + + const removeEntry = (hosterId: string) => { + const copy = { ...routing }; + delete copy[hosterId]; + setRouting(copy); + }; + + const changeProvider = (hosterId: string, provider: DebridProvider) => { + setRouting({ ...routing, [hosterId]: provider }); + }; + + return ( + <> + {routingEntries.length > 0 && ( +
+
+ Filehoster + Zuständiger Provider + +
+ {routingEntries.map(([hosterId, provider]) => { + const hosterLabel = KNOWN_HOSTERS.find((h) => h.id === hosterId)?.label || hosterId; + return ( +
+ {hosterLabel} + + +
+ ); + })} +
+ )} + {routingEntries.length === 0 && ( +
Noch keine Zuordnungen. Alle Hoster nutzen die Standard-Reihenfolge.
+ )} +
+ +
+ + ); + })()} +
+ )} +