From 66878174e6413cc6bed306779717470c8ae625c0 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sun, 31 May 2026 13:08:09 +0200 Subject: [PATCH] =?UTF-8?q?Feature:=20Mega-Debrid-Accounts=20einzeln=20(te?= =?UTF-8?q?mporaer)=20deaktivieren=20=E2=80=94=20UI-Toggle?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend war bereits vorhanden (megaDebridDisabledAccountIds + Rotation-Skip + Storage-Normalisierung); es fehlte nur das UI. Spiegelt das Debrid-Link-Muster: im Account-Bearbeiten-Dialog bekommt jeder Mega-Account einen Aktivieren/ Deaktivieren-Toggle (+ "Deaktiviert"-Badge). Der Disabled-Zustand wird im Dialog- Draft gehalten (megaDisabledIds) und beim Speichern via applyAccountDialogToSettings in megaDebridDisabledAccountIds uebernommen (gefiltert auf vorhandene Accounts). Kein Live-Persist mitten im Dialog -> kohaerent mit dem draft-then-Save-Modell. Wirkt OHNE Neustart: DebridService.unrestrictLink liest this.settings live (setSettings propagiert die Liste), unrestrictWithAccounts ueberspringt deaktivierte Accounts (gleicher Mechanismus wie Daily-Limit/Cooldown-Skip). Test: "skips a manually disabled Mega-Debrid account" — acc1 disabled -> acc2 loest auf (beweist den ID-Seam getMegaDebridAccountId). 643 Tests gruen, tsc 9, Build sauber. GUI-Toggle compile-/build-verifiziert, im laufenden Electron noch nicht click-getestet. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/renderer/App.tsx | 66 ++++++++++++++++++++++++++++++++------------ tasks/todo.md | 17 ++++++++++++ tests/debrid.test.ts | 42 ++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 18 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 1e839cc..35d9868 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -115,6 +115,9 @@ interface AccountDialogState { megaAccounts: MegaDialogAccount[]; megaNewLogin: string; megaNewPassword: string; + // IDs der im Bearbeiten-Dialog (temporär) deaktivierten Mega-Debrid-Accounts. + // Draft-State: wird erst beim Speichern in settings.megaDebridDisabledAccountIds übernommen. + megaDisabledIds: string[]; } interface DebridLinkAccountKeyEntry { @@ -584,7 +587,7 @@ function summarizeAccountLines(kind: AccountKind, settings: AppSettings): string } function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | null, settings: AppSettings): AccountDialogState { - const baseMega: Pick = { megaAccounts: [], megaNewLogin: "", megaNewPassword: "" }; + const baseMega: Pick = { megaAccounts: [], megaNewLogin: "", megaNewPassword: "", megaDisabledIds: [] }; if (!kind) { return { mode, @@ -613,7 +616,9 @@ function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | n } const parsed = parseMegaDebridAccounts(megaToken); const megaAccounts = parsed.map((a) => ({ login: a.login, password: a.password })); - return { mode, kind, token: megaToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, megaAccounts, megaNewLogin: "", megaNewPassword: "" }; + const loadedIds = new Set(parsed.map((a) => a.id)); + const megaDisabledIds = (settings.megaDebridDisabledAccountIds || []).filter((id) => loadedIds.has(id)); + return { mode, kind, token: megaToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, megaAccounts, megaNewLogin: "", megaNewPassword: "", megaDisabledIds }; } case "bestdebrid-api": return { mode, kind, token: settings.bestToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; @@ -678,14 +683,18 @@ function applyAccountDialogToSettings(settings: AppSettings, dialog: AccountDial const megaParsed = parseMegaDebridAccounts(megaSerialized); const firstLogin = megaParsed.length > 0 ? megaParsed[0].login : ""; const firstPassword = megaParsed.length > 0 ? megaParsed[0].password : ""; - return { ...settings, megaCredentials: megaSerialized, megaLogin: firstLogin, megaPassword: firstPassword, megaDebridApiEnabled: true, megaDebridPreferApi: true, providerDailyLimitBytes: nextProviderDailyLimitBytes }; + const validIds = new Set(megaParsed.map((a) => a.id)); + const megaDebridDisabledAccountIds = (dialog.megaDisabledIds || []).filter((id) => validIds.has(id)); + return { ...settings, megaCredentials: megaSerialized, megaLogin: firstLogin, megaPassword: firstPassword, megaDebridApiEnabled: true, megaDebridPreferApi: true, megaDebridDisabledAccountIds, providerDailyLimitBytes: nextProviderDailyLimitBytes }; } case "megadebrid-web": { const megaSerialized = serializeMegaDebridAccounts(dialog.megaAccounts); const megaParsed = parseMegaDebridAccounts(megaSerialized); const firstLogin = megaParsed.length > 0 ? megaParsed[0].login : ""; const firstPassword = megaParsed.length > 0 ? megaParsed[0].password : ""; - return { ...settings, megaCredentials: megaSerialized, megaLogin: firstLogin, megaPassword: firstPassword, megaDebridWebEnabled: true, megaDebridPreferApi: false, providerDailyLimitBytes: nextProviderDailyLimitBytes }; + const validIds = new Set(megaParsed.map((a) => a.id)); + const megaDebridDisabledAccountIds = (dialog.megaDisabledIds || []).filter((id) => validIds.has(id)); + return { ...settings, megaCredentials: megaSerialized, megaLogin: firstLogin, megaPassword: firstPassword, megaDebridWebEnabled: true, megaDebridPreferApi: false, megaDebridDisabledAccountIds, providerDailyLimitBytes: nextProviderDailyLimitBytes }; } case "bestdebrid-api": return { ...settings, bestToken: token, bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes }; @@ -5682,13 +5691,17 @@ export function App(): ReactElement {
- {accountDialog.megaAccounts.map((account, index) => ( -
+ {accountDialog.megaAccounts.map((account, index) => { + const accId = getMegaDebridAccountId(account.login); + const accDisabled = (accountDialog.megaDisabledIds || []).includes(accId); + return ( +
Account {index + 1} {maskMegaDebridLogin(account.login)} + {accDisabled && Deaktiviert} {(() => { - const st = snapshot?.settings?.debridAccountStatuses?.[getMegaDebridAccountId(account.login)]; + const st = snapshot?.settings?.debridAccountStatuses?.[accId]; if (!st) return Noch nicht geprüft; const checkedAgo = formatCheckedAgo(st.checkedAt); const tip = `${st.message}${st.email ? ` · ${st.email}` : ""} · geprüft ${checkedAgo}`; @@ -5697,18 +5710,35 @@ export function App(): ReactElement { return {st.message}; })()}
- +
+ + +
- ))} + ); + })}
)} diff --git a/tasks/todo.md b/tasks/todo.md index c536117..210a855 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -90,3 +90,20 @@ App läuft headless auf Windows-Server → Nutzer sitzt nicht davor. Größte L - Gestashtes Crash-Debris (`stash@{0}`): enthält Revert von 08372f9/18eada9/98dc366 + log.old. Bei Bedarf inspizierbar/recoverbar; sonst irgendwann verwerfbar. - 08372f9 (Passwort-Daemon-Reset) bewusst nicht neu aufgerollt (außerhalb dieses Goals, kein Hinweis auf Defekt). - Untracked `*-postprocess/` + `fix-library-renames.mjs`: alte Experimente (Apr/Mai), unverändert gelassen. + +--- + +## E. Mega-Debrid Account temporär deaktivieren (UI) — 2026-05-31 + +**Goal:** Einzelne Mega-Debrid-Accounts deaktivieren (statt löschen) → Rotation überspringt sie, nutzt die anderen. + +**Befund:** Backend KOMPLETT vorhanden — `megaDebridDisabledAccountIds` (Typ/Defaults/Storage-Normalisierung) + Rotation-Skip (debrid.ts:1944) + Verfügbarkeits-Checks. Es fehlt NUR das UI-Toggle (Debrid-Link hat es bereits via keyStatsPopup). ID-Seam verifiziert: `getMegaDebridAccountId(login)` (trim+lowercase) ist auf beiden Seiten identisch → Test grün. + +**Ansatz (B-kohärent):** Toggle in die bestehende Mega-Account-Liste im Bearbeiten-Dialog falten (draft-then-Save, KEIN Live-Persist → kohärent, minimal). Schritte: +1. [x] TDD: Test „skips a manually disabled Mega-Debrid account" (acc1 disabled → acc2) — grün gegen Backend. +2. [ ] `AccountDialogState`: Feld `megaDisabledIds: string[]`. +3. [ ] Dialog-Init (createAccountDialogState): aus `settings.megaDebridDisabledAccountIds`. +4. [ ] JSX Mega-Account-Liste: Aktivieren/Deaktivieren-Button + disabled-Styling pro Account. +5. [ ] Entfernen-Handler: ID auch aus megaDisabledIds entfernen. +6. [ ] `buildSettingsFromDialog` (megadebrid-api/web): `megaDebridDisabledAccountIds` aus Draft (gefiltert auf vorhandene Accounts) übernehmen. +7. [ ] Verifizieren: tsc unverändert, volle Suite grün, Toggle-Test grün. diff --git a/tests/debrid.test.ts b/tests/debrid.test.ts index 7a8a702..0638378 100644 --- a/tests/debrid.test.ts +++ b/tests/debrid.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { defaultSettings, REQUEST_RETRIES } from "../src/main/constants"; import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys"; +import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts"; import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits"; import { DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyRuntimeStateForTests, normalizeResolvedFilename, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid"; @@ -1440,6 +1441,47 @@ describe("debrid service", () => { expect(webCalls).toBeGreaterThanOrEqual(4); }, 30000); + it("skips a manually disabled Mega-Debrid account and uses the next one", async () => { + // User-Feature: einen Account temporaer deaktivieren (statt loeschen) -> die + // Rotation ueberspringt ihn und nutzt die anderen. Beweist den ID-Seam: die ID in + // megaDebridDisabledAccountIds MUSS exakt der ID entsprechen, die die Rotation via + // getMegaDebridAccountId(login) liest (sonst greift das Deaktivieren nicht). + const settings = { + ...defaultSettings(), + token: "", + bestToken: "", + allDebridToken: "", + megaLogin: "user1", + megaPassword: "pass1", + megaCredentials: "user1:pass1\nuser2:pass2", + megaDebridDisabledAccountIds: [getMegaDebridAccountId("user1")], // acc1 deaktiviert + megaDebridPreferApi: false, + providerOrder: [] as const, + providerPrimary: "megadebrid" as const, + providerSecondary: "none" as const, + providerTertiary: "none" as const, + autoProviderFallback: false + }; + + globalThis.fetch = (async () => new Response("error", { status: 500 })) as typeof fetch; + + const megaWeb = vi.fn(async () => ({ + fileName: "from-acc2.rar", + directUrl: "https://mega-web.example/from-acc2.rar", + fileSize: null, + retriesUsed: 0 + })); + + const service = new DebridService(settings, { megaWebUnrestrict: megaWeb }); + const result = await service.unrestrictLink("https://rapidgator.net/file/disabled-acc-test"); + + // Der deaktivierte acc1 wird uebersprungen -> acc2 loest den Link auf. + expect((result as { sourceAccountId?: string }).sourceAccountId).toBe(getMegaDebridAccountId("user2")); + expect(result.directUrl).toBe("https://mega-web.example/from-acc2.rar"); + // acc1 wurde gar nicht erst versucht -> megaWeb nur 1x (fuer acc2) aufgerufen. + expect(megaWeb).toHaveBeenCalledTimes(1); + }, 20000); + it("respects provider selection and does not append hidden providers", async () => { const settings = { ...defaultSettings(),