Feature: Mega-Debrid-Accounts einzeln (temporaer) deaktivieren — UI-Toggle

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) <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-05-31 13:08:09 +02:00
parent 661b1e8c21
commit 66878174e6
3 changed files with 107 additions and 18 deletions

View File

@ -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<AccountDialogState, "megaAccounts" | "megaNewLogin" | "megaNewPassword"> = { megaAccounts: [], megaNewLogin: "", megaNewPassword: "" };
const baseMega: Pick<AccountDialogState, "megaAccounts" | "megaNewLogin" | "megaNewPassword" | "megaDisabledIds"> = { 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 {
<div style={{ marginTop: 12 }}>
<label>Konfigurierte Accounts ({accountDialog.megaAccounts.length})</label>
<div className="account-dl-key-limit-list">
{accountDialog.megaAccounts.map((account, index) => (
<div key={index} className="account-dl-key-limit-row">
{accountDialog.megaAccounts.map((account, index) => {
const accId = getMegaDebridAccountId(account.login);
const accDisabled = (accountDialog.megaDisabledIds || []).includes(accId);
return (
<div key={index} className={`account-dl-key-limit-row${accDisabled ? " disabled" : ""}`}>
<div className="account-dl-key-meta">
<strong>Account {index + 1}</strong>
<span>{maskMegaDebridLogin(account.login)}</span>
{accDisabled && <span className="account-validity-badge invalid" title="Dieser Account wird beim Download übersprungen, bleibt aber gespeichert.">Deaktiviert</span>}
{(() => {
const st = snapshot?.settings?.debridAccountStatuses?.[getMegaDebridAccountId(account.login)];
const st = snapshot?.settings?.debridAccountStatuses?.[accId];
if (!st) return <span className="account-validity-badge unknown" title="Noch nicht geprüft auf „Alle prüfen“ klicken">Noch nicht geprüft</span>;
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 <span className="account-validity-badge ok" title={tip}>{st.message}</span>;
})()}
</div>
<div style={{ display: "flex", gap: 6, flexShrink: 0 }}>
<button
className={accDisabled ? "btn success" : "btn"}
title={accDisabled ? "Account wieder aktivieren" : "Account temporär deaktivieren — wird beim Download übersprungen, aber nicht gelöscht"}
onClick={() => setAccountDialog((prev) => {
if (!prev) return prev;
const cur = prev.megaDisabledIds || [];
const nextDisabled = cur.includes(accId)
? cur.filter((id) => id !== accId)
: [...cur, accId];
return { ...prev, megaDisabledIds: nextDisabled };
})}
>
{accDisabled ? "Aktivieren" : "Deaktivieren"}
</button>
<button
className="btn danger"
onClick={() => setAccountDialog((prev) => {
if (!prev) return prev;
const nextAccounts = prev.megaAccounts.filter((_, i) => i !== index);
return { ...prev, megaAccounts: nextAccounts, token: serializeMegaDebridAccounts(nextAccounts) };
return { ...prev, megaAccounts: nextAccounts, megaDisabledIds: (prev.megaDisabledIds || []).filter((id) => id !== accId), token: serializeMegaDebridAccounts(nextAccounts) };
})}
>
Entfernen
</button>
</div>
))}
</div>
);
})}
</div>
</div>
)}

View File

@ -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.

View File

@ -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(),