From 716b5169008401b9e5fb2d6a03db839da0b77a79 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 6 Mar 2026 22:17:44 +0100 Subject: [PATCH] feat: dynamic provider order, hoster routing, MKV sample fix - Replace fixed primary/secondary/tertiary slots with unlimited ordered providerOrder: DebridProvider[] list; supports as many accounts as needed - Provider list reorderable via up/down buttons in Accounts settings tab - Migration: derives order from legacy primary/secondary/tertiary if empty - Mega-Debrid split into megadebrid-api and megadebrid-web as separate providers - Add per-hoster routing (hosterRouting) to assign specific debrid provider per hoster - Fix duplicate MKV files in library: filter out sample files from Sample subfolders - Remove legacy provider-selection dropdowns from hidden settings section - Add CSS for provider-order-list/row/num/label/actions classes - Update debrid tests: add providerOrder: [] to use legacy fallback path Co-Authored-By: Claude Opus 4.6 --- src/main/constants.ts | 1 + src/main/debrid.ts | 9 +-- src/main/storage.ts | 40 ++++++++++ src/renderer/App.tsx | 163 ++++++++++++++++++---------------------- src/renderer/styles.css | 41 ++++++++++ src/shared/types.ts | 1 + tests/debrid.test.ts | 7 ++ 7 files changed, 166 insertions(+), 96 deletions(-) diff --git a/src/main/constants.ts b/src/main/constants.ts index 6760109..06d56fb 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -59,6 +59,7 @@ export function defaultSettings(): AppSettings { linkSnappyPassword: "", archivePasswordList: "", rememberToken: true, + providerOrder: ["realdebrid", "megadebrid-api", "bestdebrid"], providerPrimary: "realdebrid", providerSecondary: "megadebrid-api", providerTertiary: "bestdebrid", diff --git a/src/main/debrid.ts b/src/main/debrid.ts index 84a3cbe..2a3cfa5 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -1993,11 +1993,10 @@ export class DebridService { } } - const order = toProviderOrder( - settings.providerPrimary, - settings.providerSecondary, - settings.providerTertiary - ); + // Dynamische Reihenfolge: providerOrder hat Vorrang, Fallback auf altes primary/secondary/tertiary + const order: DebridProvider[] = (settings.providerOrder && settings.providerOrder.length > 0) + ? uniqueProviderOrder(settings.providerOrder) + : toProviderOrder(settings.providerPrimary, settings.providerSecondary, settings.providerTertiary); const primary = order[0]; if (!settings.autoProviderFallback) { diff --git a/src/main/storage.ts b/src/main/storage.ts index 42cc02f..75c1b54 100644 --- a/src/main/storage.ts +++ b/src/main/storage.ts @@ -156,6 +156,41 @@ function normalizeHosterRouting(raw: unknown, megaDebridPreferApi: boolean, mega return result; } +function normalizeProviderOrder( + raw: unknown, + megaDebridPreferApi: boolean, + megaDebridApiEnabled: boolean, + megaDebridWebEnabled: boolean, + legacyPrimary: unknown, + legacySecondary: unknown, + legacyTertiary: unknown +): DebridProvider[] { + let list: unknown[] = []; + + if (Array.isArray(raw) && raw.length > 0) { + list = raw; + } else { + // Migrate from old primary/secondary/tertiary + const candidates = [legacyPrimary, legacySecondary, legacyTertiary].filter( + (v) => v && String(v).trim() && String(v).trim() !== "none" + ); + if (candidates.length > 0) { + list = candidates; + } + } + + const seen = new Set(); + const result: DebridProvider[] = []; + for (const entry of list) { + const provider = normalizeConfiguredProvider(entry, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled); + if (provider && !seen.has(provider)) { + seen.add(provider); + result.push(provider); + } + } + return result; +} + const DEPRECATED_UPDATE_REPOS = new Set([ "sucukdeluxe/real-debrid-downloader" ]); @@ -200,6 +235,11 @@ export function normalizeSettings(settings: AppSettings): AppSettings { linkSnappyPassword: asText(settings.linkSnappyPassword), archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n|\r/g, "\n"), rememberToken: Boolean(settings.rememberToken), + providerOrder: normalizeProviderOrder( + settings.providerOrder, + megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled, + settings.providerPrimary, settings.providerSecondary, settings.providerTertiary + ), providerPrimary: normalizeConfiguredProvider(settings.providerPrimary, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled) || defaults.providerPrimary, providerSecondary: normalizeFallbackProvider(settings.providerSecondary, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled), providerTertiary: normalizeFallbackProvider(settings.providerTertiary, megaDebridPreferApi, megaDebridApiEnabled, megaDebridWebEnabled), diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 512a8a7..8c07918 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -312,24 +312,21 @@ function getActiveProvidersFromSettings(settings: AppSettings): DebridProvider[] return getConfiguredProvidersFromSettings(settings).filter((p) => !disabled.has(p)); } -function normalizeProviderSelectionForSettings(settings: AppSettings): Pick { - const configuredProviders = getActiveProvidersFromSettings(settings); - const primaryProvider = configuredProviders.includes(settings.providerPrimary) - ? settings.providerPrimary - : (configuredProviders[0] ?? "realdebrid"); - const secondaryChoices = configuredProviders.filter((provider) => provider !== primaryProvider); - const secondaryProvider = secondaryChoices.includes(settings.providerSecondary as DebridProvider) - ? settings.providerSecondary - : "none"; - const tertiaryChoices = configuredProviders.filter((provider) => provider !== primaryProvider && provider !== secondaryProvider); - const tertiaryProvider = tertiaryChoices.includes(settings.providerTertiary as DebridProvider) - ? settings.providerTertiary - : "none"; - return { - providerPrimary: primaryProvider, - providerSecondary: configuredProviders.length >= 2 ? secondaryProvider : "none", - providerTertiary: configuredProviders.length >= 3 ? tertiaryProvider : "none" - }; +// Leitet die aktive Provider-Reihenfolge aus providerOrder ab, +// gefiltert auf tatsächlich konfigurierte und nicht deaktivierte Provider. +// Direkt-Hoster (onefichier, ddownload) werden ausgeschlossen. +const DIRECT_HOSTERS: ReadonlySet = new Set(["onefichier", "ddownload"]); + +function normalizeProviderOrderForSettings(settings: AppSettings): DebridProvider[] { + const active = new Set(getActiveProvidersFromSettings(settings).filter((p) => !DIRECT_HOSTERS.has(p))); + // Behalte bestehende Reihenfolge aus providerOrder, filtere nicht-konfigurierte heraus + const ordered = (settings.providerOrder || []).filter((p) => active.has(p)); + const inOrder = new Set(ordered); + // Füge neue Provider hinten an, die noch nicht in der Reihenfolge sind + for (const p of active) { + if (!inOrder.has(p)) ordered.push(p); + } + return ordered; } function getConfiguredAccountKind(settings: AppSettings, service: AccountService): AccountKind | null { @@ -536,10 +533,10 @@ const emptyStats = (): DownloadStats => ({ const emptySnapshot = (): UiSnapshot => ({ settings: { - token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaDebridApiEnabled: false, megaDebridWebEnabled: false, megaDebridPreferApi: true, bestToken: "", bestDebridUseWebLogin: false, allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "", + token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaDebridApiEnabled: false, megaDebridWebEnabled: false, megaDebridPreferApi: true, bestToken: "", bestDebridUseWebLogin: false, allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "", debridLinkApiKeys: "", linkSnappyLogin: "", linkSnappyPassword: "", archivePasswordList: "", - rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid-api", - providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "", + rememberToken: true, providerOrder: [], providerPrimary: "realdebrid", providerSecondary: "none", + providerTertiary: "none", autoProviderFallback: true, outputDir: "", packageName: "", autoExtract: true, autoRename4sf4sj: false, extractDir: "", createExtractSubfolder: true, hybridExtract: true, collectMkvToLibrary: false, mkvLibraryDir: "", cleanupMode: "none", extractConflictMode: "overwrite", removeLinkFilesAfterExtract: false, @@ -1561,32 +1558,28 @@ export function App(): ReactElement { [settingsDraft.oneFichierApiKey]); const totalConfiguredAccounts = configuredProviders.length + (hasDdownloadAccount ? 1 : 0) + (hasOneFichierAccount ? 1 : 0); - const providerSelection = useMemo(() => normalizeProviderSelectionForSettings(settingsDraft), [settingsDraft]); - const primaryProviderValue: DebridProvider = providerSelection.providerPrimary; - const secondaryProviderChoices = useMemo(() => ( - configuredProviders.filter((provider) => provider !== primaryProviderValue) - ), [configuredProviders, primaryProviderValue]); + // Dynamische Provider-Reihenfolge (ersetzt altes primary/secondary/tertiary) + const activeProviderOrder = useMemo(() => normalizeProviderOrderForSettings(settingsDraft), [settingsDraft]); - const secondaryProviderValue: DebridFallbackProvider = providerSelection.providerSecondary; - - const tertiaryProviderChoices = useMemo(() => { - const blocked = new Set([primaryProviderValue]); - if (secondaryProviderValue !== "none") { - blocked.add(secondaryProviderValue); - } - return configuredProviders.filter((provider) => !blocked.has(provider)); - }, [configuredProviders, primaryProviderValue, secondaryProviderValue]); - - const tertiaryProviderValue: DebridFallbackProvider = providerSelection.providerTertiary; + // Setzt providerOrder + backwards-kompatible Felder synchron + const setProviderOrder = useCallback((newOrder: DebridProvider[]) => { + setSettingsDraft((prev) => ({ + ...prev, + providerOrder: newOrder, + providerPrimary: newOrder[0] ?? prev.providerPrimary, + providerSecondary: newOrder[1] ?? "none", + providerTertiary: newOrder[2] ?? "none" + })); + }, []); const normalizedSettingsDraft: AppSettings = useMemo(() => ({ ...settingsDraft, - ...providerSelection - }), [ - settingsDraft, - providerSelection - ]); + providerOrder: activeProviderOrder, + providerPrimary: activeProviderOrder[0] ?? settingsDraft.providerPrimary, + providerSecondary: (activeProviderOrder[1] ?? "none") as DebridFallbackProvider, + providerTertiary: (activeProviderOrder[2] ?? "none") as DebridFallbackProvider + }), [settingsDraft, activeProviderOrder]); const configuredAccounts = useMemo(() => { const entries: ConfiguredAccountEntry[] = []; @@ -3755,37 +3748,46 @@ export function App(): ReactElement {

Hoster-Reihenfolge

-
Debrid-Accounts können hier priorisiert werden. Direkte Host-Accounts wie DDownload und 1Fichier laufen separat.
- {configuredProviders.length === 0 && ( +
+ Lege fest, in welcher Reihenfolge die Debrid-Accounts für Links genutzt werden. + Der erste Eintrag ist der Hauptaccount. Direkt-Hoster (1Fichier, DDownload) laufen separat und erscheinen nicht hier. +
+ {activeProviderOrder.length === 0 && (
- Keine Debrid-Reihenfolge verfuegbar - Füge mindestens einen Debrid-Account hinzu, dann kannst Du Hauptaccount und Alternativen festlegen. + Keine Debrid-Reihenfolge verfügbar + Füge mindestens einen Debrid-Account hinzu, dann kannst Du die Reihenfolge festlegen.
)} - {configuredProviders.length >= 1 && ( -
- - -
- )} - {configuredProviders.length >= 2 && ( -
- - -
- )} - {configuredProviders.length >= 3 && ( -
- - + {activeProviderOrder.length > 0 && ( +
+ {activeProviderOrder.map((provider, idx) => ( +
+ {idx + 1}. + {providerLabelWithMode(provider, settingsDraft)} +
+ + +
+
+ ))}
)} @@ -3968,27 +3970,6 @@ export function App(): ReactElement { setText("ddownloadPassword", e.target.value)} /> setText("oneFichierApiKey", e.target.value)} /> - {configuredProviders.length === 0 && ( -
Füge mindestens einen Account hinzu, dann erscheint die Hoster-Auswahl.
- )} - {configuredProviders.length >= 1 && ( -
- )} - {configuredProviders.length >= 2 && ( -
- )} - {configuredProviders.length >= 3 && ( -
- )} -
diff --git a/src/renderer/styles.css b/src/renderer/styles.css index 76006b9..c84c575 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -1368,6 +1368,47 @@ body, max-width: 260px; } +.provider-order-list { + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 10px; +} + +.provider-order-row { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 10px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid var(--border); +} + +.provider-order-num { + font-size: 0.8rem; + color: var(--muted); + min-width: 18px; + text-align: right; +} + +.provider-order-label { + flex: 1; + font-size: 0.9rem; + font-weight: 500; +} + +.provider-order-actions { + display: flex; + gap: 4px; +} + +.provider-order-actions .btn-sm { + padding: 2px 8px; + font-size: 0.9rem; + line-height: 1.4; +} + .account-modal { width: min(960px, calc(100vw - 36px)); max-height: calc(100vh - 36px); diff --git a/src/shared/types.ts b/src/shared/types.ts index 83a8243..3f123a1 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -66,6 +66,7 @@ export interface AppSettings { linkSnappyPassword: string; archivePasswordList: string; rememberToken: boolean; + providerOrder: DebridProvider[]; providerPrimary: DebridProvider; providerSecondary: DebridFallbackProvider; providerTertiary: DebridFallbackProvider; diff --git a/tests/debrid.test.ts b/tests/debrid.test.ts index 861178e..05ba6a2 100644 --- a/tests/debrid.test.ts +++ b/tests/debrid.test.ts @@ -17,6 +17,7 @@ describe("debrid service", () => { megaLogin: "user", megaPassword: "pass", bestToken: "", + providerOrder: [] as const, providerPrimary: "realdebrid" as const, providerSecondary: "megadebrid" as const, providerTertiary: "bestdebrid" as const, @@ -182,6 +183,7 @@ describe("debrid service", () => { const settings = { ...defaultSettings(), allDebridToken: "ad-token", + providerOrder: [] as const, providerPrimary: "alldebrid" as const, providerSecondary: "none" as const, providerTertiary: "none" as const, @@ -212,6 +214,7 @@ describe("debrid service", () => { token: "", bestToken: "", allDebridToken: "ad-token", + providerOrder: [] as const, providerPrimary: "alldebrid" as const, providerSecondary: "realdebrid" as const, providerTertiary: "megadebrid" as const, @@ -285,6 +288,7 @@ describe("debrid service", () => { ...defaultSettings(), allDebridToken: "ad-token", allDebridUseWebLogin: true, + providerOrder: [] as const, providerPrimary: "alldebrid" as const, providerSecondary: "none" as const, providerTertiary: "none" as const, @@ -420,6 +424,7 @@ describe("debrid service", () => { allDebridToken: "", megaLogin: "user", megaPassword: "pass", + providerOrder: [] as const, providerPrimary: "megadebrid" as const, providerSecondary: "megadebrid" as const, providerTertiary: "megadebrid" as const, @@ -484,6 +489,7 @@ describe("debrid service", () => { megaPassword: "pass", megaDebridApiEnabled: true, megaDebridWebEnabled: true, + providerOrder: [] as const, providerPrimary: "megadebrid-api" as const, providerSecondary: "megadebrid-web" as const, providerTertiary: "none" as const, @@ -514,6 +520,7 @@ describe("debrid service", () => { allDebridToken: "", megaLogin: "user", megaPassword: "pass", + providerOrder: [] as const, providerPrimary: "megadebrid" as const, providerSecondary: "none" as const, providerTertiary: "none" as const,