release: publish v2.0.34 account recovery fixes

Add a manual Mega-Debrid cooldown reset that preserves active downloads while releasing affected queued work. Replace false per-account cooling for ambiguous Mega-Web session failures with one shared session backoff, attribute fallback failures to the provider actually attempted, and accept omitted optional renderer settings during provider changes. Expand regression coverage and stabilize archive integration checks under concurrent validation load.
This commit is contained in:
Sucukdeluxe
2026-08-13 22:06:11 +02:00
parent 4972858c9d
commit 4fe951de78
24 changed files with 503 additions and 64 deletions
+11 -5
View File
@@ -576,7 +576,7 @@ export class AppController {
return this.settings;
}
public resetDebridLinkApiKeyDailyUsage(keyId: string): AppSettings {
public resetDebridLinkApiKeyDailyUsage(keyId: string): AppSettings {
const liveSettings = this.manager.getSettings();
const nextSettings = normalizeSettings({
...liveSettings,
@@ -586,10 +586,16 @@ export class AppController {
saveSettings(this.storagePaths, this.settings);
this.manager.setSettings(this.settings);
this.audit("INFO", "Debrid-Link-Key-Tagesnutzung zurückgesetzt", { keyId });
return this.settings;
}
public async openRealDebridLoginWindow(): Promise<void> {
return this.settings;
}
public resetMegaDebridCooldowns(): { cleared: number } {
const cleared = this.manager.resetMegaDebridCooldowns();
this.audit("INFO", "Mega-Debrid-Cooldowns manuell zurückgesetzt", { cleared });
return { cleared };
}
public async openRealDebridLoginWindow(): Promise<void> {
this.audit("INFO", "Real-Debrid Login-Fenster geöffnet");
await this.realDebridWebFallback.openLoginWindow();
}
+57 -13
View File
@@ -308,8 +308,10 @@ function getMegaDebridAbortMinRunMs(): number {
return Number.isFinite(fromEnv) && fromEnv >= 0 ? Math.floor(fromEnv) : MEGA_DEBRID_ABORT_MIN_RUN_MS_DEFAULT;
}
const megaDebridEmptyResponseStreaks = new Map<string, number>();
export const MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART = 10;
const megaDebridEmptyResponseStreaks = new Map<string, number>();
export const MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART = 10;
const MEGA_DEBRID_WEB_SESSION_BACKOFF_MS = 20_000;
let megaDebridWebSessionBackoffUntil = 0;
const megaDebridRotationState: Record<"api" | "web", { cursor: number; stickyCount: number }> = {
api: { cursor: 0, stickyCount: 0 },
@@ -369,6 +371,14 @@ export function clearMegaDebridAccountRuntimeStates(accountKeys: Iterable<string
megaDebridEmptyResponseStreaks.delete(accountKey);
}
}
export function clearAllMegaDebridAccountRuntimeCooldowns(): number {
const cleared = megaDebridAccountCooldowns.size + (megaDebridWebSessionBackoffUntil > Date.now() ? 1 : 0);
megaDebridAccountCooldowns.clear();
megaDebridEmptyResponseStreaks.clear();
megaDebridWebSessionBackoffUntil = 0;
return cleared;
}
export function resetMegaDebridRuntimeStateForTests(): void {
megaDebridAccountCooldowns.clear();
@@ -376,6 +386,21 @@ export function resetMegaDebridRuntimeStateForTests(): void {
megaDebridRotationState.api = { cursor: 0, stickyCount: 0 };
megaDebridRotationState.web = { cursor: 0, stickyCount: 0 };
megaDebridInFlight.clear();
megaDebridWebSessionBackoffUntil = 0;
}
function getMegaDebridWebSessionBackoffRemaining(now = Date.now()): number {
const remaining = megaDebridWebSessionBackoffUntil - now;
if (remaining <= 0) {
megaDebridWebSessionBackoffUntil = 0;
return 0;
}
return remaining;
}
function startMegaDebridWebSessionBackoff(now = Date.now()): number {
megaDebridWebSessionBackoffUntil = Math.max(megaDebridWebSessionBackoffUntil, now + MEGA_DEBRID_WEB_SESSION_BACKOFF_MS);
return getMegaDebridWebSessionBackoffRemaining(now);
}
export function getMegaDebridInFlightCountForMode(mode: "api" | "web"): number {
@@ -2096,9 +2121,9 @@ class MegaDebridClient {
if (!lastError) {
lastError = "Mega-Web Antwort leer";
}
if (/permanent ungültig|hosternotavailable|file.?not.?found|file.?unavailable|link.?is.?dead/i.test(lastError) || MEGA_DEBRID_NO_SERVER_RE.test(lastError)) {
break;
}
if (/permanent ungültig|hosternotavailable|file.?not.?found|file.?unavailable|link.?is.?dead|mega-web login ung(?:ü|u)ltig oder session blockiert/i.test(lastError) || MEGA_DEBRID_NO_SERVER_RE.test(lastError)) {
break;
}
if (attempt < REQUEST_RETRIES) {
await sleepWithSignal(retryDelay(attempt), signal);
}
@@ -2145,8 +2170,15 @@ class MegaDebridClient {
}
if (getAvailableMegaDebridAccounts(settings, mode).length === 0) {
throw new Error("Mega-Debrid: Kein aktiver Account verfuegbar (deaktiviert oder am Tageslimit)");
}
throw new Error("Mega-Debrid: Kein aktiver Account verfuegbar (deaktiviert oder am Tageslimit)");
}
if (mode === "web") {
const sessionBackoffMs = getMegaDebridWebSessionBackoffRemaining();
if (sessionBackoffMs > 0) {
throw new Error(`mega_debrid_session_backoff:${sessionBackoffMs}:Mega-Web Login vorübergehend blockiert`);
}
}
const failures: string[] = [];
let usableAccountSeen = false;
@@ -2321,7 +2353,15 @@ class MegaDebridClient {
throw new Error(`Mega-Debrid${accountLabel}: ${abortText}`);
}
const failure = MegaDebridClient.classifyAccountFailure(error, redactions);
traceConversionPhase({
if (mode === "web" && /mega-web login ung(?:ü|u)ltig oder session blockiert/i.test(failure.message)) {
const sessionBackoffMs = startMegaDebridWebSessionBackoff();
logAccountRotation("WARN", providerName, rotationLabel, "SESSION_BACKOFF", {
reason: failure.message,
cooldownSec: Math.ceil(sessionBackoffMs / 1000)
});
throw new Error(`mega_debrid_session_backoff:${sessionBackoffMs}:${failure.message}`);
}
traceConversionPhase({
phase: "mega-account",
provider: providerName.includes("API") ? "megadebrid-api" : "megadebrid-web",
account: rotationLabel,
@@ -2411,11 +2451,15 @@ class MegaDebridClient {
): { fatal: boolean; cooldownMs: number; message: string; category: MegaDebridCooldownCategory; limitSignal?: boolean } {
const errorText = sanitizeProviderErrorText(error, redactions);
if (/aborted/i.test(errorText) && !/timeout/i.test(errorText)) {
return { fatal: true, cooldownMs: 0, message: errorText, category: "temporary" };
}
if (/token.?error|please log.?in/i.test(errorText)) {
if (/aborted/i.test(errorText) && !/timeout/i.test(errorText)) {
return { fatal: true, cooldownMs: 0, message: errorText, category: "temporary" };
}
if (/mega-web login ung(?:ü|u)ltig oder session blockiert/i.test(errorText)) {
return { fatal: false, cooldownMs: 0, message: errorText, category: "temporary" };
}
if (/token.?error|please log.?in/i.test(errorText)) {
return { fatal: false, cooldownMs: 15_000, message: errorText, category: "temporary" };
}
+110 -25
View File
@@ -54,7 +54,7 @@ function releaseTlsSkip(): void {
}
import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
import { planDownloadCompletion, reconcileFinalizedSize, validateDownloadedFileCompletion } from "./download-completion";
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, clearMegaDebridAccountRuntimeStates, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, getMegaDebridAccountAttemptTimeoutMs, getMegaDebridAccountCooldownState, getMegaDebridInFlightCountForMode, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState } from "./debrid";
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, clearAllMegaDebridAccountRuntimeCooldowns, clearMegaDebridAccountRuntimeStates, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, getMegaDebridAccountAttemptTimeoutMs, getMegaDebridAccountCooldownState, getMegaDebridInFlightCountForMode, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState } from "./debrid";
import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, detectArchiveSignature, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree, resetExtractorCachesForPasswordChange, type ExtractArchiveFailureInfo } from "./extractor";
import { validateFileAgainstManifest } from "./integrity";
import { classifyDiskError } from "./fs-error";
@@ -709,7 +709,7 @@ export function parseMegaDebridCooldownRetry(errorText: string): { delayMs: numb
return { delayMs, detail: text.replace(/mega_debrid_cooldown:\d+:/i, "").trim() };
}
export function parseMegaDebridSlowLinkRetry(errorText: string): { delayMs: number; detail: string } | null {
export function parseMegaDebridSlowLinkRetry(errorText: string): { delayMs: number; detail: string } | null {
const text = String(errorText || "");
const match = text.match(/mega_debrid_slow_link:(\d+)/i);
if (!match) {
@@ -774,9 +774,11 @@ function isHosterUnavailableError(errorText: string): boolean {
return String(errorText || "").toLowerCase().includes("hosternotavailable");
}
function isTemporaryUnrestrictError(errorText: string): boolean {
const text = String(errorText || "").toLowerCase();
return text.includes("server error")
function isTemporaryUnrestrictError(errorText: string): boolean {
const text = String(errorText || "").toLowerCase();
return text.includes("mega-web login ungültig oder session blockiert")
|| text.includes("mega-web login ungultig oder session blockiert")
|| text.includes("server error")
|| text.includes("internal server error")
|| text.includes("notdebrid")
|| text.includes("unable to generate link")
@@ -1739,6 +1741,21 @@ function retryDelayWithJitter(attempt: number, baseMs: number): number {
return Math.floor(jitter);
}
export function parseMegaDebridSessionBackoff(errorText: string): { delayMs: number; detail: string } | null {
const match = String(errorText || "").match(/mega_debrid_session_backoff:(\d+):(.*)$/is);
if (!match) {
return null;
}
const raw = Number(match[1]);
if (!Number.isFinite(raw) || raw <= 0) {
return null;
}
return {
delayMs: Math.max(1000, Math.min(5 * 60 * 1000, raw)),
detail: String(match[2] || "").trim()
};
}
function clearResumeRecoveryState(item: DownloadItem, includeHttp416Budget = true): void {
delete item.resumeLinkRenewalFailures;
delete item.resumeHardResetUsed;
@@ -2614,8 +2631,59 @@ export class DownloadManager extends EventEmitter {
}
return released;
}
public getSettings(): AppSettings {
public resetMegaDebridCooldowns(): number {
const cleared = clearAllMegaDebridAccountRuntimeCooldowns();
let clearedProviderFailures = 0;
for (const key of [...this.providerFailures.keys()]) {
if (isMegaDebridProviderKey(key)) {
this.providerFailures.delete(key);
clearedProviderFailures += 1;
}
}
const activeItemIds = new Set([...this.activeTasks.values()].map((task) => task.itemId));
const affectedPackageIds = new Set<string>();
let released = 0;
for (const item of Object.values(this.session.items)) {
if (activeItemIds.has(item.id) || (item.status !== "queued" && item.status !== "reconnect_wait")) {
continue;
}
const status = item.fullStatus || "";
const expectedProvider = String(item.provider || this.getExpectedProviderForItem(item) || "");
const isMegaWait = /Mega-Debrid (?:Cooldown|Session-Backoff|bis Tagesreset gesperrt)/i.test(status)
|| (/Provider-Cooldown/i.test(status) && isMegaDebridProviderKey(expectedProvider));
if (!isMegaWait) {
continue;
}
this.retryAfterByItem.delete(item.id);
this.retryStateByItem.delete(item.id);
item.status = "queued";
item.fullStatus = "Wartet";
item.lastError = "";
item.provider = null;
item.providerLabel = undefined;
item.providerAccountId = undefined;
item.providerAccountLabel = undefined;
item.updatedAt = nowMs();
affectedPackageIds.add(item.packageId);
released += 1;
}
for (const packageId of affectedPackageIds) {
const pkg = this.session.packages[packageId];
if (pkg) {
this.refreshPackageStatus(pkg);
}
}
logger.info(`Mega-Debrid-Cooldowns manuell zurückgesetzt: accounts=${cleared}, provider=${clearedProviderFailures}, queue=${released}`);
this.persistSoon();
this.emitState(true);
if (this.session.running) {
void this.ensureScheduler().catch((error) => logger.error(`Scheduler nach Cooldown-Reset fehlgeschlagen: ${compactErrorText(error)}`));
}
return cleared;
}
public getSettings(): AppSettings {
return this.settings;
}
@@ -9542,7 +9610,8 @@ export class DownloadManager extends EventEmitter {
if (active.abortController.signal.aborted) {
throw new Error(`aborted:${active.abortReason}`);
}
const cooldownProvider = this.getProviderFailureKeyForItem(item);
active.validationProvider = null;
const cooldownProvider = this.getProviderFailureKeyForItem(item);
const cooldownMs = this.getProviderCooldownRemaining(cooldownProvider);
let preferredLeadProvider: DebridProvider | null = null;
if (cooldownMs > 0) {
@@ -9634,21 +9703,22 @@ export class DownloadManager extends EventEmitter {
packageId: item.packageId
}
);
} catch (unrestrictError) {
if (!active.abortController.signal.aborted && unrestrictTimeoutSignal.aborted) {
this.recordProviderFailure(cooldownProvider);
} catch (unrestrictError) {
const failedProvider = this.getProviderFailureKeyForItem(item, active.validationProvider || cooldownProvider);
if (!active.abortController.signal.aborted && unrestrictTimeoutSignal.aborted) {
this.recordProviderFailure(failedProvider);
throw new Error(`Unrestrict Timeout nach ${Math.ceil(unrestrictTimeoutMs / 1000)}s`);
}
const errText = compactErrorText(unrestrictError);
if (isUnrestrictFailure(errText) && !isHosterUnavailableError(errText)) {
this.recordProviderFailure(cooldownProvider);
if (isProviderBusyUnrestrictError(errText) || isTemporaryUnrestrictError(errText)) {
const busyCooldownMs = isTemporaryUnrestrictError(errText)
? Math.min(180000, 20000 + Number(active.unrestrictRetries || 0) * 10000)
: Math.min(60000, 12000 + Number(active.unrestrictRetries || 0) * 3000);
this.applyProviderBusyBackoff(cooldownProvider, busyCooldownMs);
}
}
}
const errText = compactErrorText(unrestrictError);
if (isUnrestrictFailure(errText) && !isHosterUnavailableError(errText)) {
this.recordProviderFailure(failedProvider);
if (isProviderBusyUnrestrictError(errText) || isTemporaryUnrestrictError(errText)) {
const busyCooldownMs = isTemporaryUnrestrictError(errText)
? Math.min(180000, 20000 + Number(active.unrestrictRetries || 0) * 10000)
: Math.min(60000, 12000 + Number(active.unrestrictRetries || 0) * 3000);
this.applyProviderBusyBackoff(failedProvider, busyCooldownMs);
}
}
throw unrestrictError;
}
if (active.abortController.signal.aborted) {
@@ -10287,8 +10357,23 @@ export class DownloadManager extends EventEmitter {
return;
}
const megaRawError = error instanceof Error ? String(error.message || "") : String(error || "");
const megaSlowLinkRetry = parseMegaDebridSlowLinkRetry(megaRawError);
const megaRawError = error instanceof Error ? String(error.message || "") : String(error || "");
const megaSessionBackoff = parseMegaDebridSessionBackoff(megaRawError);
if (megaSessionBackoff && active.unrestrictRetries < maxUnrestrictRetries) {
active.unrestrictRetries += 1;
item.retries += 1;
this.queueRetry(
item,
active,
megaSessionBackoff.delayMs,
`Mega-Debrid Session-Backoff, neuer Versuch in ${Math.ceil(megaSessionBackoff.delayMs / 1000)}s`
);
item.lastError = megaSessionBackoff.detail || errorText;
this.persistSoon();
this.emitState();
return;
}
const megaSlowLinkRetry = parseMegaDebridSlowLinkRetry(megaRawError);
if (megaSlowLinkRetry && active.unrestrictRetries < maxUnrestrictRetries) {
active.unrestrictRetries += 1;
item.retries += 1;
@@ -10393,7 +10478,7 @@ export class DownloadManager extends EventEmitter {
active.unrestrictRetries += 1;
item.retries += 1;
const failureProvider = this.getProviderFailureKeyForItem(item);
const failureProvider = this.getProviderFailureKeyForItem(item, active.validationProvider || item.provider);
const isDebridLinkError = /debrid-link|debrid_link/i.test(errorText) || failureProvider === "debridlink";
if (!isDebridLinkError) {
this.recordProviderFailure(failureProvider);
+2 -1
View File
@@ -411,13 +411,14 @@ function registerIpcHandlers(): void {
}
return createRendererSettings(controller.resetProviderDailyUsage(validatedProvider));
});
handleTrusted(IPC_CHANNELS.RESET_DEBRID_LINK_API_KEY_DAILY_USAGE, (_event: IpcMainInvokeEvent, keyId: string) => {
handleTrusted(IPC_CHANNELS.RESET_DEBRID_LINK_API_KEY_DAILY_USAGE, (_event: IpcMainInvokeEvent, keyId: string) => {
const validatedKeyId = validateString(keyId, "keyId").trim();
if (!validatedKeyId) {
throw new Error("keyId ist ungültig");
}
return createRendererSettings(controller.resetDebridLinkApiKeyDailyUsage(validatedKeyId));
});
handleTrusted(IPC_CHANNELS.RESET_MEGA_DEBRID_COOLDOWNS, () => controller.resetMegaDebridCooldowns());
handleTrusted(IPC_CHANNELS.CREATE_ACCOUNT, (_event: IpcMainInvokeEvent, rawCommand: unknown) => {
const command = validateAccountCommand(rawCommand);
+3
View File
@@ -64,6 +64,9 @@ export function validateRendererSettingsUpdate(value: unknown, current: AppSetti
const input = value as Record<string, unknown>;
const output: Record<string, unknown> = {};
for (const [key, entry] of Object.entries(input)) {
if (entry === undefined) {
continue;
}
if (DERIVED_KEYS.has(key)) {
continue;
}
+1
View File
@@ -39,6 +39,7 @@ const api: ElectronApi = {
updateSettings: (settings: RendererSettingsUpdate): Promise<RendererSettings> => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_SETTINGS, settings),
resetProviderDailyUsage: (provider: DebridProvider): Promise<RendererSettings> => ipcRenderer.invoke(IPC_CHANNELS.RESET_PROVIDER_DAILY_USAGE, provider),
resetDebridLinkApiKeyDailyUsage: (keyId: string): Promise<RendererSettings> => ipcRenderer.invoke(IPC_CHANNELS.RESET_DEBRID_LINK_API_KEY_DAILY_USAGE, keyId),
resetMegaDebridCooldowns: (): Promise<{ cleared: number }> => ipcRenderer.invoke(IPC_CHANNELS.RESET_MEGA_DEBRID_COOLDOWNS),
createAccount: (command: AccountCreateCommand): Promise<AccountCommandResult> => ipcRenderer.invoke(IPC_CHANNELS.CREATE_ACCOUNT, command),
replaceAccount: (command: AccountReplaceCommand): Promise<AccountCommandResult> => ipcRenderer.invoke(IPC_CHANNELS.REPLACE_ACCOUNT, command),
updateAccountSecret: (command: AccountUpdateSecretCommand): Promise<AccountCommandResult> => ipcRenderer.invoke(IPC_CHANNELS.UPDATE_ACCOUNT_SECRET, command),
+13 -2
View File
@@ -2914,7 +2914,7 @@ export function App(): ReactElement {
});
};
const onResetDebridLinkApiKeyDailyUsage = async (entry: ConfiguredAccountEntry, keyId: string, keyLabel: string): Promise<void> => {
const onResetDebridLinkApiKeyDailyUsage = async (entry: ConfiguredAccountEntry, keyId: string, keyLabel: string): Promise<void> => {
await performQuickAction(async () => {
const result = await window.rd.resetDebridLinkApiKeyDailyUsage(keyId);
syncLiveProviderUsageSettings(result);
@@ -3014,6 +3014,16 @@ export function App(): ReactElement {
});
};
const onResetMegaDebridCooldowns = async (): Promise<void> => {
await performQuickAction(async () => {
const result = await window.rd.resetMegaDebridCooldowns();
await reconcileAuthoritativeSnapshot();
showToast(result.cleared > 0 ? "Mega-Debrid-Cooldowns zurückgesetzt" : "Keine aktiven Mega-Debrid-Cooldowns.", 2400);
}, (error) => {
showToast(`Cooldown-Reset fehlgeschlagen: ${String(error)}`, 3200);
});
};
const getAccountToggleTarget = (row: AccountTableRow): AccountToggleTarget | null => {
if (row.toggleKind === "mega" && row.accountId) {
return {
@@ -5155,7 +5165,8 @@ export function App(): ReactElement {
: hosterId;
if (!resolvedHosterId || settingsDraft.hosterRouting?.[resolvedHosterId] || !configuredProviders[0]) return;
setHosterRouting({ ...(settingsDraft.hosterRouting || {}), [resolvedHosterId]: configuredProviders[0] });
}
},
onResetMegaDebridCooldowns: () => { void onResetMegaDebridCooldowns(); }
};
const settingsFormActions: SettingsViewActions["form"] = {
+1 -1
View File
@@ -49,7 +49,7 @@ const pairs = [
["Fertige Downloads und Konflikte", "Completed downloads and conflicts"], ["Fertige Downloads aus der Liste", "Completed downloads in the list"], ["Bei gleichnamigen Dateien", "When files have the same name"],
["Hinzufügen", "Add"], ["Aktualisieren", "Refresh"], [" Hinzufügen", " Add"], [" Entfernen", " Remove"], ["↻ Aktualisieren", "↻ Refresh"], ["Provider-Reihenfolge", "Provider order"], ["Lege fest, in welcher Reihenfolge verfügbare Provider verwendet werden.", "Choose the order in which available providers are used."],
["Automatischer Fallback", "Automatic fallback"], ["Zugangsdaten lokal speichern", "Store credentials locally"], ["Hoster-Routing", "Hoster routing"], ["Eigene Zuordnungen überschreiben für den jeweiligen Hoster die Standardreihenfolge.", "Custom assignments override the default order for each hoster."],
["Rotations-Verlauf", "Rotation history"], ["Sammlung", "Collection"], ["URL oder Rohzeile", "URL or raw line"], ["Zeile", "Line"], ["Paket / Datei", "Package / file"], ["Größe", "Size"], ["Gestartet", "Started"], ["Beendet", "Finished"],
["Rotations-Verlauf", "Rotation history"], ["Cooldowns zurücksetzen", "Reset cooldowns"], ["Keine aktiven Mega-Debrid-Cooldowns.", "No active Mega-Debrid cooldowns."], ["Mega-Debrid-Cooldowns zurückgesetzt", "Mega-Debrid cooldowns reset"], ["Cooldown-Reset fehlgeschlagen: ", "Cooldown reset failed: "], ["Sammlung", "Collection"], ["URL oder Rohzeile", "URL or raw line"], ["Zeile", "Line"], ["Paket / Datei", "Package / file"], ["Größe", "Size"], ["Gestartet", "Started"], ["Beendet", "Finished"],
["Alle Einträge", "All entries"], ["Heute", "Today"], ["Letzte 7 Tage", "Last 7 days"], ["Älter", "Older"], ["Gelöscht", "Deleted"], ["Fehlgeschlagen", "Failed"],
["Dauer", "Duration"], ["Durchschnitt", "Average"], ["Zielordner", "Destination folder"], ["Verlaufsfilter", "History filters"], ["Verlauf leeren", "Clear history"], ["Verlaufsaktionen", "History actions"],
["Einträge", "Entries"], ["Erneut hinzufügen", "Add again"], ["Im Ordner zeigen", "Show in folder"], ["Auswahl löschen", "Clear selection"], ["Gesamtverlauf löschen", "Delete all history"], ["Verlauf durchsuchen", "Search history"], ["Name, Pfad, Hoster oder Provider", "Name, path, hoster or provider"],
@@ -86,7 +86,8 @@ export interface AccountWorkspaceActions {
onToggleRememberCredentials?: (enabled: boolean) => void;
onRoutingProviderChange?: (hosterId: string, provider: string) => void;
onRoutingRemove?: (hosterId: string) => void;
onRoutingAdd?: (hosterId: string) => void;
onRoutingAdd?: (hosterId: string) => void;
onResetMegaDebridCooldowns?: () => void;
}
export interface AccountWorkspaceProps {
@@ -412,9 +413,17 @@ function AccountRules({ model, actions }: AccountWorkspaceProps): ReactElement {
</ul>
)}
</section>
{model.rules.rotationEvents ? (
<section className="settings-rule-section">
<h3>Rotations-Verlauf</h3>
{model.rules.rotationEvents ? (
<section className="settings-rule-section">
<div className="settings-rule-section-heading">
<h3>Rotations-Verlauf</h3>
<button
className="settings-button settings-button-secondary"
disabled={model.busy || !actions.onResetMegaDebridCooldowns}
onClick={actions.onResetMegaDebridCooldowns}
type="button"
>Cooldowns zurücksetzen</button>
</div>
{model.rules.rotationEvents.length === 0 ? (
<span className="settings-rule-empty">Noch keine Rotations-Ereignisse.</span>
) : model.rules.rotationEvents.map((event) => (
+7
View File
@@ -782,6 +782,13 @@
border-bottom: 1px solid var(--ui-border);
}
.settings-rule-section-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.settings-provider-order,
.settings-routing-list {
display: grid;
+3 -2
View File
@@ -10,8 +10,9 @@ export const IPC_CHANNELS = {
REPLACE_ACCOUNT: "app:replace-account",
UPDATE_ACCOUNT_SECRET: "app:update-account-secret",
DELETE_ACCOUNT: "app:delete-account",
RESET_PROVIDER_DAILY_USAGE: "app:reset-provider-daily-usage",
RESET_DEBRID_LINK_API_KEY_DAILY_USAGE: "app:reset-debrid-link-api-key-daily-usage",
RESET_PROVIDER_DAILY_USAGE: "app:reset-provider-daily-usage",
RESET_DEBRID_LINK_API_KEY_DAILY_USAGE: "app:reset-debrid-link-api-key-daily-usage",
RESET_MEGA_DEBRID_COOLDOWNS: "app:reset-mega-debrid-cooldowns",
ADD_LINKS: "queue:add-links",
ADD_CONTAINERS: "queue:add-containers",
GET_START_CONFLICTS: "queue:get-start-conflicts",
+1
View File
@@ -39,6 +39,7 @@ export interface ElectronApi {
updateSettings: (settings: RendererSettingsUpdate) => Promise<RendererSettings>;
resetProviderDailyUsage: (provider: DebridProvider) => Promise<RendererSettings>;
resetDebridLinkApiKeyDailyUsage: (keyId: string) => Promise<RendererSettings>;
resetMegaDebridCooldowns: () => Promise<{ cleared: number }>;
createAccount: (command: AccountCreateCommand) => Promise<AccountCommandResult>;
replaceAccount: (command: AccountReplaceCommand) => Promise<AccountCommandResult>;
updateAccountSecret: (command: AccountUpdateSecretCommand) => Promise<AccountCommandResult>;