Compare commits
2 Commits
661b1e8c21
...
4a883fb93f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a883fb93f | ||
|
|
66878174e6 |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.7.169",
|
"version": "1.7.170",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -115,6 +115,9 @@ 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 {
|
||||||
@ -584,7 +587,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"> = { megaAccounts: [], megaNewLogin: "", megaNewPassword: "" };
|
const baseMega: Pick<AccountDialogState, "megaAccounts" | "megaNewLogin" | "megaNewPassword" | "megaDisabledIds"> = { megaAccounts: [], megaNewLogin: "", megaNewPassword: "", megaDisabledIds: [] };
|
||||||
if (!kind) {
|
if (!kind) {
|
||||||
return {
|
return {
|
||||||
mode,
|
mode,
|
||||||
@ -613,7 +616,9 @@ 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 }));
|
||||||
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":
|
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 };
|
||||||
@ -678,14 +683,18 @@ 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 : "";
|
||||||
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": {
|
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 : "";
|
||||||
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":
|
case "bestdebrid-api":
|
||||||
return { ...settings, bestToken: token, bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes };
|
return { ...settings, bestToken: token, bestDebridUseWebLogin: false, providerDailyLimitBytes: nextProviderDailyLimitBytes };
|
||||||
@ -5682,13 +5691,17 @@ 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) => {
|
||||||
<div key={index} className="account-dl-key-limit-row">
|
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">
|
<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?.[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>;
|
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}`;
|
||||||
@ -5697,18 +5710,35 @@ 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>
|
||||||
<button
|
<div style={{ display: "flex", gap: 6, flexShrink: 0 }}>
|
||||||
className="btn danger"
|
<button
|
||||||
onClick={() => setAccountDialog((prev) => {
|
className={accDisabled ? "btn success" : "btn"}
|
||||||
if (!prev) return prev;
|
title={accDisabled ? "Account wieder aktivieren" : "Account temporär deaktivieren — wird beim Download übersprungen, aber nicht gelöscht"}
|
||||||
const nextAccounts = prev.megaAccounts.filter((_, i) => i !== index);
|
onClick={() => setAccountDialog((prev) => {
|
||||||
return { ...prev, megaAccounts: nextAccounts, token: serializeMegaDebridAccounts(nextAccounts) };
|
if (!prev) return prev;
|
||||||
})}
|
const cur = prev.megaDisabledIds || [];
|
||||||
>
|
const nextDisabled = cur.includes(accId)
|
||||||
Entfernen
|
? cur.filter((id) => id !== accId)
|
||||||
</button>
|
: [...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, megaDisabledIds: (prev.megaDisabledIds || []).filter((id) => id !== accId), token: serializeMegaDebridAccounts(nextAccounts) };
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Entfernen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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.
|
- 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.
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
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";
|
||||||
|
|
||||||
@ -1440,6 +1441,47 @@ 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(),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user