feat: add per-hoster provider routing (Hoster-Zuordnung)

- 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 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe
2026-03-06 19:14:16 +01:00
co-authored by Claude Opus 4.6
parent 9f94404435
commit c811649b9d
6 changed files with 246 additions and 3 deletions
+119 -1
View File
@@ -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<DebridProvider, string> = {
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 {
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.rememberToken} onChange={(e) => setBool("rememberToken", e.target.checked)} /> Zugangsdaten lokal speichern</label>
</div>
{configuredProviders.length >= 1 && (
<div className="settings-section card">
<h3>Hoster-Zuordnung</h3>
<div className="hint">Lege fest, welcher Debrid-Provider sich um welchen Filehoster kümmert. Nicht zugeordnete Hoster nutzen die Standard-Reihenfolge oben.</div>
{(() => {
const routing: Record<string, DebridProvider> = 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<string, DebridProvider>) => {
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 && (
<div className="hoster-routing-table">
<div className="hoster-routing-header">
<span>Filehoster</span>
<span>Zuständiger Provider</span>
<span></span>
</div>
{routingEntries.map(([hosterId, provider]) => {
const hosterLabel = KNOWN_HOSTERS.find((h) => h.id === hosterId)?.label || hosterId;
return (
<div key={hosterId} className="hoster-routing-row">
<span className="hoster-routing-label">{hosterLabel}</span>
<select value={provider} onChange={(e) => changeProvider(hosterId, e.target.value as DebridProvider)}>
{configuredProviders.map((p) => (
<option key={p} value={p}>{providerLabelWithMode(p, settingsDraft)}</option>
))}
</select>
<button className="btn btn-sm btn-danger" onClick={() => removeEntry(hosterId)} title="Zuordnung entfernen">&times;</button>
</div>
);
})}
</div>
)}
{routingEntries.length === 0 && (
<div className="hint" style={{ fontStyle: "italic", opacity: 0.7 }}>Noch keine Zuordnungen. Alle Hoster nutzen die Standard-Reihenfolge.</div>
)}
<div className="hoster-routing-add">
<select
value=""
onChange={(e) => {
const val = e.target.value;
if (val === "__custom") {
const name = window.prompt("Hoster-Domain eingeben (z.B. rapidgator, turbobit):");
const clean = (name || "").trim().toLowerCase().replace(/^www\./, "").split(".")[0];
if (clean) addEntry(clean);
} else {
addEntry(val);
}
e.target.value = "";
}}
>
<option value="" disabled>Hoster hinzufügen...</option>
{availableHosters.map((h) => (
<option key={h.id} value={h.id}>{h.label}</option>
))}
<option value="" disabled></option>
<option value="__custom">Eigener Hoster...</option>
</select>
</div>
</>
);
})()}
</div>
)}
<div hidden>
<div className="settings-section card">
<h3>Accounts</h3>
+63
View File
@@ -1305,6 +1305,69 @@ body,
opacity: 1;
}
.hoster-routing-table {
display: flex;
flex-direction: column;
gap: 0;
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
margin-bottom: 10px;
}
.hoster-routing-header {
display: grid;
grid-template-columns: 1fr 1fr 40px;
gap: 10px;
padding: 8px 12px;
background: color-mix(in srgb, var(--card) 80%, var(--surface));
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
border-bottom: 1px solid var(--border);
}
.hoster-routing-row {
display: grid;
grid-template-columns: 1fr 1fr 40px;
gap: 10px;
padding: 6px 12px;
align-items: center;
border-bottom: 1px solid color-mix(in srgb, var(--border) 40%, transparent);
}
.hoster-routing-row:last-child {
border-bottom: none;
}
.hoster-routing-label {
font-weight: 500;
font-size: 0.88rem;
}
.hoster-routing-row select {
padding: 4px 8px;
font-size: 0.84rem;
}
.hoster-routing-row .btn-sm {
padding: 2px 8px;
font-size: 0.9rem;
min-width: 28px;
line-height: 1.4;
border-radius: 6px;
}
.hoster-routing-add {
margin-top: 8px;
}
.hoster-routing-add select {
max-width: 260px;
}
.account-modal {
width: min(960px, calc(100vw - 36px));
max-height: calc(100vh - 36px);