Compare commits

..

No commits in common. "4a883fb93f8d60439f83a9383c6d0369b44c5114" and "661b1e8c21fd269af8056d26a7390989a2402e4f" have entirely different histories.

4 changed files with 19 additions and 108 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.7.170", "version": "1.7.169",
"description": "Desktop downloader", "description": "Desktop downloader",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -115,9 +115,6 @@ interface AccountDialogState {
megaAccounts: MegaDialogAccount[]; megaAccounts: MegaDialogAccount[];
megaNewLogin: string; megaNewLogin: string;
megaNewPassword: 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 { interface DebridLinkAccountKeyEntry {
@ -587,7 +584,7 @@ function summarizeAccountLines(kind: AccountKind, settings: AppSettings): string
} }
function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | null, settings: AppSettings): AccountDialogState { function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | null, settings: AppSettings): AccountDialogState {
const baseMega: Pick<AccountDialogState, "megaAccounts" | "megaNewLogin" | "megaNewPassword" | "megaDisabledIds"> = { megaAccounts: [], megaNewLogin: "", megaNewPassword: "", megaDisabledIds: [] }; const baseMega: Pick<AccountDialogState, "megaAccounts" | "megaNewLogin" | "megaNewPassword"> = { megaAccounts: [], megaNewLogin: "", megaNewPassword: "" };
if (!kind) { if (!kind) {
return { return {
mode, mode,
@ -616,9 +613,7 @@ function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | n
} }
const parsed = parseMegaDebridAccounts(megaToken); const parsed = parseMegaDebridAccounts(megaToken);
const megaAccounts = parsed.map((a) => ({ login: a.login, password: a.password })); const megaAccounts = parsed.map((a) => ({ login: a.login, password: a.password }));
const loadedIds = new Set(parsed.map((a) => a.id)); return { mode, kind, token: megaToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, megaAccounts, megaNewLogin: "", megaNewPassword: "" };
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": case "bestdebrid-api":
return { mode, kind, token: settings.bestToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega }; return { mode, kind, token: settings.bestToken, login: "", password: "", dailyLimitGb, keyDailyLimitGbById: {}, ...baseMega };
@ -683,18 +678,14 @@ function applyAccountDialogToSettings(settings: AppSettings, dialog: AccountDial
const megaParsed = parseMegaDebridAccounts(megaSerialized); const megaParsed = parseMegaDebridAccounts(megaSerialized);
const firstLogin = megaParsed.length > 0 ? megaParsed[0].login : ""; const firstLogin = megaParsed.length > 0 ? megaParsed[0].login : "";
const firstPassword = megaParsed.length > 0 ? megaParsed[0].password : ""; const firstPassword = megaParsed.length > 0 ? megaParsed[0].password : "";
const validIds = new Set(megaParsed.map((a) => a.id)); return { ...settings, megaCredentials: megaSerialized, megaLogin: firstLogin, megaPassword: firstPassword, megaDebridApiEnabled: true, megaDebridPreferApi: true, providerDailyLimitBytes: nextProviderDailyLimitBytes };
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": { case "megadebrid-web": {
const megaSerialized = serializeMegaDebridAccounts(dialog.megaAccounts); const megaSerialized = serializeMegaDebridAccounts(dialog.megaAccounts);
const megaParsed = parseMegaDebridAccounts(megaSerialized); const megaParsed = parseMegaDebridAccounts(megaSerialized);
const firstLogin = megaParsed.length > 0 ? megaParsed[0].login : ""; const firstLogin = megaParsed.length > 0 ? megaParsed[0].login : "";
const firstPassword = megaParsed.length > 0 ? megaParsed[0].password : ""; const firstPassword = megaParsed.length > 0 ? megaParsed[0].password : "";
const validIds = new Set(megaParsed.map((a) => a.id)); return { ...settings, megaCredentials: megaSerialized, megaLogin: firstLogin, megaPassword: firstPassword, megaDebridWebEnabled: true, megaDebridPreferApi: false, providerDailyLimitBytes: nextProviderDailyLimitBytes };
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": case "bestdebrid-api":
return { ...settings, bestToken: token, bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes }; return { ...settings, bestToken: token, bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes };
@ -5691,17 +5682,13 @@ export function App(): ReactElement {
<div style={{ marginTop: 12 }}> <div style={{ marginTop: 12 }}>
<label>Konfigurierte Accounts ({accountDialog.megaAccounts.length})</label> <label>Konfigurierte Accounts ({accountDialog.megaAccounts.length})</label>
<div className="account-dl-key-limit-list"> <div className="account-dl-key-limit-list">
{accountDialog.megaAccounts.map((account, index) => { {accountDialog.megaAccounts.map((account, index) => (
const accId = getMegaDebridAccountId(account.login); <div key={index} className="account-dl-key-limit-row">
const accDisabled = (accountDialog.megaDisabledIds || []).includes(accId);
return (
<div key={index} className={`account-dl-key-limit-row${accDisabled ? " disabled" : ""}`}>
<div className="account-dl-key-meta"> <div className="account-dl-key-meta">
<strong>Account {index + 1}</strong> <strong>Account {index + 1}</strong>
<span>{maskMegaDebridLogin(account.login)}</span> <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?.[accId]; const st = snapshot?.settings?.debridAccountStatuses?.[getMegaDebridAccountId(account.login)];
if (!st) return <span className="account-validity-badge unknown" title="Noch nicht geprüft auf „Alle prüfen“ klicken">Noch nicht geprüft</span>; 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 checkedAgo = formatCheckedAgo(st.checkedAt);
const tip = `${st.message}${st.email ? ` · ${st.email}` : ""} · geprüft ${checkedAgo}`; const tip = `${st.message}${st.email ? ` · ${st.email}` : ""} · geprüft ${checkedAgo}`;
@ -5710,35 +5697,18 @@ export function App(): ReactElement {
return <span className="account-validity-badge ok" title={tip}>{st.message}</span>; return <span className="account-validity-badge ok" title={tip}>{st.message}</span>;
})()} })()}
</div> </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 <button
className="btn danger" className="btn danger"
onClick={() => setAccountDialog((prev) => { onClick={() => setAccountDialog((prev) => {
if (!prev) return prev; if (!prev) return prev;
const nextAccounts = prev.megaAccounts.filter((_, i) => i !== index); const nextAccounts = prev.megaAccounts.filter((_, i) => i !== index);
return { ...prev, megaAccounts: nextAccounts, megaDisabledIds: (prev.megaDisabledIds || []).filter((id) => id !== accId), token: serializeMegaDebridAccounts(nextAccounts) }; return { ...prev, megaAccounts: nextAccounts, token: serializeMegaDebridAccounts(nextAccounts) };
})} })}
> >
Entfernen Entfernen
</button> </button>
</div> </div>
</div> ))}
);
})}
</div> </div>
</div> </div>
)} )}

View File

@ -90,20 +90,3 @@ 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. - 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). - 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. - 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,7 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import { defaultSettings, REQUEST_RETRIES } from "../src/main/constants"; import { defaultSettings, REQUEST_RETRIES } from "../src/main/constants";
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys"; 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 { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
import { DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyRuntimeStateForTests, normalizeResolvedFilename, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid"; import { DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyRuntimeStateForTests, normalizeResolvedFilename, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid";
@ -1441,47 +1440,6 @@ describe("debrid service", () => {
expect(webCalls).toBeGreaterThanOrEqual(4); expect(webCalls).toBeGreaterThanOrEqual(4);
}, 30000); }, 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 () => { it("respects provider selection and does not append hidden providers", async () => {
const settings = { const settings = {
...defaultSettings(), ...defaultSettings(),