Compare commits

..

5 Commits

Author SHA1 Message Date
Sucukdeluxe
bc50e4285d Release v1.7.217 2026-06-17 05:58:59 +02:00
Sucukdeluxe
cbb84f29d8 docs(tasks): Runde-3-Audit dokumentiert (3 Fixes v1.7.217, 5 charakterisiert/vorgelegt) 2026-06-17 05:56:06 +02:00
Sucukdeluxe
1e04b7bdcb Fix: Bearbeiten eines Mega-Debrid-Accounts ändert nicht mehr heimlich die API/Web-Bevorzugung
Sind beide Mega-Debrid-Modi (API + Web) aktiv, entscheidet megaDebridPreferApi,
welcher Modus zuerst versucht wird. Beim Speichern des Account-Dialogs hat
applyAccountDialogToSettings diese Einstellung jedoch fest überschrieben: ein
Bearbeiten der API-Zeile erzwang megaDebridPreferApi=true, ein Bearbeiten der
Web-Zeile erzwang false. Wer also bewusst Web bevorzugt (preferApi=false) hatte
und nur einen zweiten Account hinzufügen oder das Tageslimit ändern wollte und
dafür die API-Zeile bearbeitete, dem wurde die Reihenfolge ungefragt auf
"API zuerst" umgestellt — was die Provider-Auflösung für alle künftigen
Mega-Links ändert.

Fix: Die festen megaDebridPreferApi-Werte aus beiden Dialog-Zweigen entfernt.
Der bestehende Wert wird über den ...settings-Spread durchgereicht. Erst-Anlage
bleibt korrekt, weil der Default ohnehin true ist (constants). Bei nur EINEM
aktiven Modus ist preferApi ohnehin irrelevant (die single-mode-Zweige
kurzschließen davor).

Test: bei beiden Modi aktiv + preferApi=false bleibt nach API-Edit false; bei
preferApi=true bleibt nach Web-Edit true. Mit der alten Festverdrahtung kippt der
Wert (rot bewiesen).
2026-06-17 05:54:42 +02:00
Sucukdeluxe
dfd19260f6 Fix: Download-Queue läuft nach fehlgeschlagenem Update weiter (kein Stillstand bis Neustart)
Beim Klick auf "Update" stoppt das Tool zuerst die laufende Session
(stop({parkForRestart:true}) → running=false, alle Items auf "Wartet") und
startet dann die Installation. Schlug die Installation fehl — und das ist auf
realistischen Wegen möglich (git.24-music.de-Aussetzer, Netz weg über alle
Versuche, oder ein Release mit fehlendem/falschem Digest → Integritätsprüfung
schlägt fehl) — kam KEIN App-Neustart (nur bei Erfolg wird app.quit geplant) und
auch kein In-Process-Resume. Folge: die komplette Queue stand still (alle Items
"Wartet"), bis ein Mensch "Start" klickte oder das Programm neu startete — auf
einem unbeaufsichtigten Server mit ~1 TB/h ein echter Stillstand.

Fix: Der Stop-Installieren-Resume-Ablauf liegt jetzt in
runInstallWithResume(): War vorher eine Session aktiv und schlägt die
Installation fehl, wird die Session wieder gestartet (manager.start(),
idempotent — die Items sind bereits "queued", der Scheduler nimmt sie normal
auf). Der Resume greift auf BEIDEN Fehlerpfaden: started:false UND geworfene
Ausnahme (try/catch). Bei Erfolg (started:true) wird NICHT resumed (die App
beendet sich gleich), und war keine Session aktiv, passiert nichts.

Test: started:false → resume; install wirft → resume + rethrow; started:true →
kein resume; nicht-laufend → kein stop/resume. Logik in eine reine, testbare
Funktion ausgelagert (kein schwergewichtiger AppController-Konstruktor nötig).
2026-06-17 05:50:55 +02:00
Sucukdeluxe
76b3f99476 Fix: Mega-Debrid-Account sperrt sich nicht mehr dauerhaft selbst (Self-Cooldown bis Neustart)
Ein gesunder Mega-Debrid-Account konnte sich dauerhaft "bis Neustart" sperren,
ohne sich von selbst zu erholen — genau das vom Nutzer beobachtete Verhalten
("das Tool setzt sich selbst Cooldowns"). Zwei Ursachen, beide behoben:

1) Falsche Einstufung eines HOSTER-Problems als ACCOUNT-Limit:
   classifyAccountFailure hat den hoster-spezifischen Fehler "Kein Server fuer
   diesen Hoster" (MEGA_DEBRID_NO_SERVER_RE — Mega-Debrid hat fuer EINEN Hoster,
   z.B. Rapidgator, gerade keinen Server) mit limitSignal=true markiert. Dieses
   Signal speist den "bis Neustart"-Streak (3x -> Dauer-Park). Bei einer
   Warteschlange voller Links desselben (kurzzeitig nicht bedienbaren) Hosters
   kletterte der Streak hoch und parkte einen kerngesunden Account — der sich
   dann ueber die Rotation auf ALLE Accounts ausbreitete. Fix: limitSignal aus
   dem No-Server-Zweig entfernt (der 120s-Cooldown bleibt). Hoster-Verfuegbarkeit
   ist kein Account-Gesundheitssignal. Der echte Tageslimit-Pfad (leere Antwort)
   bleibt unveraendert.

2) "Bis Neustart"-Park hatte keine Ablaufzeit (until = MAX_SAFE_INTEGER) und
   konnte daher NIE von selbst aufgehen — auch nicht, nachdem Mega-Debrid wieder
   Server hatte oder das Tageslimit um Mitternacht zuruecksetzt. Nur ein Neustart
   half. Fix: Der Park laeuft jetzt zum naechsten Tagesreset (lokale Mitternacht,
   mind. 120s) ab und heilt sich damit garantiert innerhalb von <=24h selbst —
   egal welcher Zweig ihn ausgeloest hat. Texte entsprechend von "bis Neustart
   gesperrt" auf "bis zum Tagesreset gesperrt" angepasst (Main + Renderer
   konsistent).

Tests: (a) ein geparkter Account ist nach dem Tagesreset wieder frei statt
dauerhaft gesperrt (rot ohne Fix: MAX_SAFE_INTEGER laeuft nie ab); (b) ein
"no server"-Fehler liefert kein limitSignal mehr (rot ohne Fix), waehrend die
echte leere Antwort weiterhin als Limit zaehlt. 86 Debrid-Tests gruen.
2026-06-17 05:47:52 +02:00
9 changed files with 213 additions and 30 deletions

View File

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

View File

@ -38,6 +38,7 @@ import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session
import { MegaWebFallback } from "./mega-web-fallback"; import { MegaWebFallback } from "./mega-web-fallback";
import { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistory, loadHistoryForRetention, loadSession, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, resetHistoryForRetention, saveHistory, saveSession, saveSettings } from "./storage"; import { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistory, loadHistoryForRetention, loadSession, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, resetHistoryForRetention, saveHistory, saveSession, saveSettings } from "./storage";
import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update"; import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
import { runInstallWithResume } from "./update-install-flow";
import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server"; import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server";
import { encryptBackup, decryptBackup } from "./backup-crypto"; import { encryptBackup, decryptBackup } from "./backup-crypto";
import { buildBackupPayload, planBackupImport } from "./backup-payload"; import { buildBackupPayload, planBackupImport } from "./backup-payload";
@ -459,16 +460,14 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
} }
public async installUpdate(onProgress?: (progress: UpdateInstallProgress) => void): Promise<UpdateInstallResult> { public async installUpdate(onProgress?: (progress: UpdateInstallProgress) => void): Promise<UpdateInstallResult> {
if (this.manager.isSessionRunning()) {
this.manager.stop({ parkForRestart: true });
}
this.manager.persistNowSync();
const cacheAgeMs = Date.now() - this.lastUpdateCheckAt; const cacheAgeMs = Date.now() - this.lastUpdateCheckAt;
const cached = this.lastUpdateCheck && !this.lastUpdateCheck.error && cacheAgeMs <= 10 * 60 * 1000 const cached = this.lastUpdateCheck && !this.lastUpdateCheck.error && cacheAgeMs <= 10 * 60 * 1000
? this.lastUpdateCheck ? this.lastUpdateCheck
: undefined; : undefined;
const result = await installLatestUpdate(this.settings.updateRepo, cached, onProgress); const result = await runInstallWithResume(
this.manager,
() => installLatestUpdate(this.settings.updateRepo, cached, onProgress)
);
if (result.started) { if (result.started) {
this.lastUpdateCheck = null; this.lastUpdateCheck = null;
this.lastUpdateCheckAt = 0; this.lastUpdateCheckAt = 0;

View File

@ -349,14 +349,26 @@ export function primeMegaDebridRuntimeCooldownForTests(accountId: string, cooldo
setMegaDebridAccountCooldownState(accountId, cooldownMs, message, "temporary"); setMegaDebridAccountCooldownState(accountId, cooldownMs, message, "temporary");
} }
export function primeMegaDebridUntilRestartForTests(accountId: string, message = "Tageslimit (Test) — bis Neustart gesperrt"): void { export function primeMegaDebridUntilRestartForTests(accountId: string, message = "Tageslimit (Test) — bis zum Tagesreset gesperrt"): void {
setMegaDebridAccountCooldownState(accountId, 0, message, "quota", true); setMegaDebridAccountCooldownState(accountId, 0, message, "quota", true);
} }
export function classifyMegaDebridAccountFailureForTests(
error: unknown
): { fatal: boolean; cooldownMs: number; message: string; category: MegaDebridCooldownCategory; limitSignal?: boolean } {
return MegaDebridClient.classifyAccountFailure(error);
}
function clearMegaDebridAccountCooldownState(accountId: string): void { function clearMegaDebridAccountCooldownState(accountId: string): void {
megaDebridAccountCooldowns.delete(accountId); megaDebridAccountCooldowns.delete(accountId);
} }
function megaDebridDailyParkExpiry(now: number): number {
const midnight = new Date(now);
midnight.setHours(24, 0, 0, 0);
return Math.max(midnight.getTime(), now + MEGA_DEBRID_ACCOUNT_COOLDOWN_MS);
}
function setMegaDebridAccountCooldownState( function setMegaDebridAccountCooldownState(
accountId: string, accountId: string,
cooldownMs: number, cooldownMs: number,
@ -366,7 +378,7 @@ function setMegaDebridAccountCooldownState(
): void { ): void {
if (untilRestart) { if (untilRestart) {
megaDebridAccountCooldowns.set(accountId, { megaDebridAccountCooldowns.set(accountId, {
until: Number.MAX_SAFE_INTEGER, until: megaDebridDailyParkExpiry(Date.now()),
message, message,
category, category,
untilRestart: true untilRestart: true
@ -1987,7 +1999,7 @@ class MegaDebridClient {
? "Neustart" ? "Neustart"
: new Date(accountCooldownState.until).toLocaleTimeString(); : new Date(accountCooldownState.until).toLocaleTimeString();
const reasonText = accountCooldownState.untilRestart const reasonText = accountCooldownState.untilRestart
? "Tageslimit erreicht — bis Neustart gesperrt" ? "Tageslimit erreicht — bis zum Tagesreset gesperrt"
: `Cooldown bis ${untilStr}`; : `Cooldown bis ${untilStr}`;
logger.info(`Mega-Debrid${accountLabel}: uebersprungen (${reasonText}), pruefe naechsten Account`); logger.info(`Mega-Debrid${accountLabel}: uebersprungen (${reasonText}), pruefe naechsten Account`);
logAccountRotation("INFO", providerName, rotationLabel, "SKIP_COOLDOWN", { logAccountRotation("INFO", providerName, rotationLabel, "SKIP_COOLDOWN", {
@ -2086,7 +2098,7 @@ class MegaDebridClient {
const streak = recordMegaDebridEmptyResponseStreak(cooldownKey); const streak = recordMegaDebridEmptyResponseStreak(cooldownKey);
if (streak >= MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART) { if (streak >= MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART) {
parkUntilRestart = true; parkUntilRestart = true;
parkMessage = `Tageslimit erreicht (${streak}x kein Server/leere Antwort) — bis Neustart gesperrt`; parkMessage = `Tageslimit erreicht (${streak}x kein Server/leere Antwort) — bis zum Tagesreset gesperrt`;
} }
} else { } else {
clearMegaDebridEmptyResponseStreak(cooldownKey); clearMegaDebridEmptyResponseStreak(cooldownKey);
@ -2109,7 +2121,7 @@ class MegaDebridClient {
throw new Error(`Mega-Debrid${accountLabel}: ${failure.message}`); throw new Error(`Mega-Debrid${accountLabel}: ${failure.message}`);
} }
const cooldownInfo = parkUntilRestart const cooldownInfo = parkUntilRestart
? ", bis Neustart gesperrt" ? ", bis zum Tagesreset gesperrt"
: failure.cooldownMs > 0 : failure.cooldownMs > 0
? `, Cooldown ${Math.ceil(failure.cooldownMs / 1000)}s` ? `, Cooldown ${Math.ceil(failure.cooldownMs / 1000)}s`
: ""; : "";
@ -2146,14 +2158,14 @@ class MegaDebridClient {
throw new Error(`mega_debrid_cooldown:${retryMs}:${cooldownFailures.join(" | ")}`); throw new Error(`mega_debrid_cooldown:${retryMs}:${cooldownFailures.join(" | ")}`);
} }
if (parkedUntilRestartSeen) { if (parkedUntilRestartSeen) {
throw new Error(`Mega-Debrid: Alle Accounts am Tageslimit (bis Neustart gesperrt)${cooldownFailures.length > 0 ? ` | ${cooldownFailures.join(" | ")}` : ""}`); throw new Error(`Mega-Debrid: Alle Accounts am Tageslimit (bis zum Tagesreset gesperrt)${cooldownFailures.length > 0 ? ` | ${cooldownFailures.join(" | ")}` : ""}`);
} }
throw new Error("Mega-Debrid: Kein aktiver Account verfuegbar"); throw new Error("Mega-Debrid: Kein aktiver Account verfuegbar");
} }
throw new Error(failures.join(" | ") || "Mega-Debrid: Kein aktiver Account verfuegbar"); throw new Error(failures.join(" | ") || "Mega-Debrid: Kein aktiver Account verfuegbar");
} }
private static classifyAccountFailure( static classifyAccountFailure(
error: unknown error: unknown
): { fatal: boolean; cooldownMs: number; message: string; category: MegaDebridCooldownCategory; limitSignal?: boolean } { ): { fatal: boolean; cooldownMs: number; message: string; category: MegaDebridCooldownCategory; limitSignal?: boolean } {
const errorText = compactErrorText(error).replace(/^Error:\s*/i, ""); const errorText = compactErrorText(error).replace(/^Error:\s*/i, "");
@ -2206,8 +2218,7 @@ class MegaDebridClient {
fatal: false, fatal: false,
cooldownMs: MEGA_DEBRID_ACCOUNT_COOLDOWN_MS, cooldownMs: MEGA_DEBRID_ACCOUNT_COOLDOWN_MS,
message: "Kein Server fuer diesen Hoster (Tageslimit/Hoster nicht verfuegbar)", message: "Kein Server fuer diesen Hoster (Tageslimit/Hoster nicht verfuegbar)",
category: "quota", category: "quota"
limitSignal: true
}; };
} }

View File

@ -0,0 +1,34 @@
export interface InstallResumeManager {
isSessionRunning(): boolean;
stop(options: { parkForRestart: boolean }): void;
persistNowSync(): void;
start(): Promise<void> | void;
}
export async function runInstallWithResume<T extends { started: boolean }>(
manager: InstallResumeManager,
doInstall: () => Promise<T>
): Promise<T> {
const wasRunning = manager.isSessionRunning();
if (wasRunning) {
manager.stop({ parkForRestart: true });
}
manager.persistNowSync();
const resumeIfParked = async (): Promise<void> => {
if (wasRunning && !manager.isSessionRunning()) {
await manager.start();
}
};
try {
const result = await doInstall();
if (!result.started) {
await resumeIfParked();
}
return result;
} catch (error) {
await resumeIfParked();
throw error;
}
}

View File

@ -109,7 +109,7 @@ interface MegaDialogAccount {
password: string; password: string;
} }
interface AccountDialogState { export interface AccountDialogState {
mode: "create" | "edit"; mode: "create" | "edit";
kind: AccountKind | null; kind: AccountKind | null;
token: string; token: string;
@ -647,7 +647,7 @@ function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | n
} }
} }
function applyAccountDialogToSettings(settings: AppSettings, dialog: AccountDialogState): AppSettings { export function applyAccountDialogToSettings(settings: AppSettings, dialog: AccountDialogState): AppSettings {
if (!dialog.kind) { if (!dialog.kind) {
return settings; return settings;
} }
@ -682,7 +682,7 @@ function applyAccountDialogToSettings(settings: AppSettings, dialog: AccountDial
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)); const validIds = new Set(megaParsed.map((a) => a.id));
const megaDebridDisabledAccountIds = (dialog.megaDisabledIds || []).filter((id) => validIds.has(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 }; return { ...settings, megaCredentials: megaSerialized, megaLogin: firstLogin, megaPassword: firstPassword, megaDebridApiEnabled: true, megaDebridDisabledAccountIds, providerDailyLimitBytes: nextProviderDailyLimitBytes };
} }
case "megadebrid-web": { case "megadebrid-web": {
const megaSerialized = serializeMegaDebridAccounts(dialog.megaAccounts); const megaSerialized = serializeMegaDebridAccounts(dialog.megaAccounts);
@ -691,7 +691,7 @@ function applyAccountDialogToSettings(settings: AppSettings, dialog: AccountDial
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)); const validIds = new Set(megaParsed.map((a) => a.id));
const megaDebridDisabledAccountIds = (dialog.megaDisabledIds || []).filter((id) => validIds.has(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 }; return { ...settings, megaCredentials: megaSerialized, megaLogin: firstLogin, megaPassword: firstPassword, megaDebridWebEnabled: true, 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 };
@ -1159,13 +1159,13 @@ function formatCheckedAgo(checkedAt: number): string {
} }
function rotationEventText(ev: { event: string; cooldownSec?: number; next?: string; reason?: string }): string { function rotationEventText(ev: { event: string; cooldownSec?: number; next?: string; reason?: string }): string {
const untilRestart = /bis Neustart gesperrt/i.test(ev.reason || ""); const untilRestart = /bis zum Tagesreset gesperrt/i.test(ev.reason || "");
switch (ev.event) { switch (ev.event) {
case "OK": return "erfolgreich"; case "OK": return "erfolgreich";
case "FAILED": { case "FAILED": {
if (untilRestart) { if (untilRestart) {
const nx = ev.next && ev.next !== "ENDE" ? `${ev.next}` : ""; const nx = ev.next && ev.next !== "ENDE" ? `${ev.next}` : "";
return `Tageslimit erreicht, bis Neustart gesperrt${nx}`; return `Tageslimit erreicht, bis zum Tagesreset gesperrt${nx}`;
} }
const cd = ev.cooldownSec ? `, Cooldown ${ev.cooldownSec}s` : ""; const cd = ev.cooldownSec ? `, Cooldown ${ev.cooldownSec}s` : "";
const nx = ev.next && ev.next !== "ENDE" ? `${ev.next}` : ""; const nx = ev.next && ev.next !== "ENDE" ? `${ev.next}` : "";
@ -1176,7 +1176,7 @@ function rotationEventText(ev: { event: string; cooldownSec?: number; next?: str
const cd = ev.cooldownSec ? `, Cooldown ${ev.cooldownSec}s` : ""; const cd = ev.cooldownSec ? `, Cooldown ${ev.cooldownSec}s` : "";
return `Timeout/Abbruch${cd} → nächster Account beim Retry`; return `Timeout/Abbruch${cd} → nächster Account beim Retry`;
} }
case "SKIP_COOLDOWN": return untilRestart ? "übersprungen (bis Neustart gesperrt)" : "übersprungen (Cooldown aktiv)"; case "SKIP_COOLDOWN": return untilRestart ? "übersprungen (bis zum Tagesreset gesperrt)" : "übersprungen (Cooldown aktiv)";
case "SKIP_DISABLED": return "übersprungen (deaktiviert)"; case "SKIP_DISABLED": return "übersprungen (deaktiviert)";
case "SKIP_DAILY_LIMIT": return "übersprungen (Tageslimit erreicht)"; case "SKIP_DAILY_LIMIT": return "übersprungen (Tageslimit erreicht)";
case "SKIP_HOST_COOLDOWN": return "übersprungen (Host-Cooldown)"; case "SKIP_HOST_COOLDOWN": return "übersprungen (Host-Cooldown)";

View File

@ -76,6 +76,36 @@ daily-limit aggregate early-exit.
braucht workMs-Threading → groesserer Eingriff, nicht LOW-billig. braucht workMs-Threading → groesserer Eingriff, nicht LOW-billig.
- Vor Runde 4-5: SYNTHESE-Pass — ist Retry/Cooldown/Rotation END-TO-END kohaerent selbstheilend? - Vor Runde 4-5: SYNTHESE-Pass — ist Retry/Cooldown/Rotation END-TO-END kohaerent selbstheilend?
## Runde 3 (Failover/Reconnect/Cooldown-Lifecycle/Scheduler/IPC-Toggle/Updater) — Workflow wcwztx7e9
7 confirmed / 1 refuted. provider-failover-Finder crashte (Socket) → diese Dimension via MEINER
unabhaengigen Code-Verifikation abgedeckt (60s-Timeout kappt Failover, debrid.ts 3845 + dl-mgr 8814).
### Batch 3 → v1.7.217 (GEFIXT, je rot-bewiesener Test)
- [x] #R3-1 HIGH (3/3) Self-Cooldown bis Neustart — DER vom Nutzer gemeldete „Tool sperrt sich selbst".
(a) limitSignal aus MEGA_DEBRID_NO_SERVER_RE-Zweig entfernt (Hoster-Problem != Account-Limit),
(b) until-restart-Park laeuft jetzt zum Tagesreset (lokale Mitternacht) ab statt MAX_SAFE_INTEGER →
heilt <=24h selbst. Texte „bis Neustart"→„bis zum Tagesreset". Commit 76b3f99.
- [x] #R3-5 HIGH (3/3) Fehlgeschlagenes Update → Queue-Stillstand bis Neustart. runInstallWithResume()
(neue reine Funktion) resumt bei started:false UND throw. Commit dfd1926.
- [x] #R3-3 MED (3/3) Account-Edit ueberschreibt megaDebridPreferApi. Hardcode entfernt → ...settings
reicht Nutzerwahl durch. Commit 1e04b7b.
### Dokumentiert / NICHT autonom gefixt (Advisor-Disziplin)
- #R3-2 HIGH (2/3, UMSTRITTEN) Mega API/Web-Account-Zeilen teilen EINE login-only Enable-Flag →
Toggle spiegelt sich (= Nutzer-Report „API aus → Web an"). KEIN Auto-Fix: Daten-Modell-Fix braucht
Settings-Migration (kann deaktivierte Accounts re-aktivieren), UI-Collapse = Layout-Redesign (Nutzer
UI-Geschmack-sensibel). → DEM NUTZER vorlegen: gemeinsamer Schalter vs. unabhaengige pro-Modus-Flags.
- 60s-Failover-Kappung (HIGH, mein Fund, Finder gecrasht) — debrid.ts 3845 wertet JEDEN combined-signal-
Abort (cancel ODER 60s-Timeout) als kein-Failover; langsamer Provider1 hungert Provider2 aus, auch ueber
Retries. Post-214 (API-first) groesstenteils latent. → eigene Runde: gecrashten Finder ERST neu laufen
lassen (unabhaengige Verifikation fehlt), dann per-Provider-Timeout-Design mit Advisor. NICHT in 217.
- #R3-6 MED (3/3) Update-Mirror-Failover feuert nie (nur Gitea). NICHT fixen: aendert den Update-Fetch-Pfad
= der Kanal, ueber den jeder Fix den Nutzer erreicht; faellt heute sicher aus (App behaelt alte Version).
- #R3-4 LOW (2/3) providerPrimary kann auf disabled Mega normalisieren — self-heilt zur Laufzeit. Belassen
(Refuter: Fix riskanter als Bug — schreibt persistierte Absicht um).
- #R3-7 LOW (3/3) Update-Integritaet hash-only, kein Authenticode — ehrliche Grenze, faellt sicher aus.
- REFUTIERT (0/3): all-accounts-parked wirft plain error ohne cooldown-retry-Token.
## Runde 2 (Download-Ausfuehrung: stream/resume/disk/integrity/extract/persist) — Workflow whspc8ddv ## Runde 2 (Download-Ausfuehrung: stream/resume/disk/integrity/extract/persist) — Workflow whspc8ddv
14 confirmed / 7 refuted (>=2/3 adversarisch). Alle HIGH/MED unten unabhaengig am echten Code 14 confirmed / 7 refuted (>=2/3 adversarisch). Alle HIGH/MED unten unabhaengig am echten Code
verifiziert (Zeilen zitiert) bevor gefixt. Jeder Fix mit rot-bewiesenem Test, tsc bleibt 6. verifiziert (Zeilen zitiert) bevor gefixt. Jeder Fix mit rot-bewiesenem Test, tsc bleibt 6.

View File

@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { applyAccountDialogToSettings, AccountDialogState } from "../src/renderer/App";
import { defaultSettings } from "../src/main/constants";
function megaDialog(kind: "megadebrid-api" | "megadebrid-web"): AccountDialogState {
return {
mode: "edit",
kind,
token: "",
login: "",
password: "",
dailyLimitGb: "",
keyDailyLimitGbById: {},
megaAccounts: [{ login: "user@x", password: "pw" }],
megaNewLogin: "",
megaNewPassword: "",
megaDisabledIds: []
};
}
describe("applyAccountDialogToSettings — keeps the user's Mega preferApi choice", () => {
it("does not flip megaDebridPreferApi to true when editing the API account", () => {
const settings = { ...defaultSettings(), megaDebridApiEnabled: true, megaDebridWebEnabled: true, megaDebridPreferApi: false };
const next = applyAccountDialogToSettings(settings, megaDialog("megadebrid-api"));
expect(next.megaDebridApiEnabled).toBe(true);
expect(next.megaDebridPreferApi).toBe(false);
});
it("does not flip megaDebridPreferApi to false when editing the Web account", () => {
const settings = { ...defaultSettings(), megaDebridApiEnabled: true, megaDebridWebEnabled: true, megaDebridPreferApi: true };
const next = applyAccountDialogToSettings(settings, megaDialog("megadebrid-web"));
expect(next.megaDebridWebEnabled).toBe(true);
expect(next.megaDebridPreferApi).toBe(true);
});
});

View File

@ -3,7 +3,7 @@ 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 { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits"; import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
import { clearMegaDebridEmptyResponseStreak, DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyRuntimeStateForTests, getMegaDebridAccountCooldownState, MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART, MEGA_DEBRID_STICKY_LINKS, normalizeResolvedFilename, primeMegaDebridUntilRestartForTests, recordMegaDebridEmptyResponseStreak, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid"; import { classifyMegaDebridAccountFailureForTests, clearMegaDebridEmptyResponseStreak, DebridService, extractRapidgatorFilenameFromHtml, fetchAllDebridHostInfo, fetchDebridLinkHostLimits, filenameFromRapidgatorUrlPath, getDebridLinkKeyRuntimeStateForTests, getMegaDebridAccountCooldownState, MEGA_DEBRID_EMPTY_STREAK_UNTIL_RESTART, MEGA_DEBRID_STICKY_LINKS, normalizeResolvedFilename, primeMegaDebridUntilRestartForTests, recordMegaDebridEmptyResponseStreak, resetDebridLinkRuntimeStateForTests, resetMegaDebridRuntimeStateForTests } from "../src/main/debrid";
const originalFetch = globalThis.fetch; const originalFetch = globalThis.fetch;
@ -1918,13 +1918,24 @@ describe("debrid service", () => {
expect(recordMegaDebridEmptyResponseStreak(key)).toBe(1); expect(recordMegaDebridEmptyResponseStreak(key)).toBe(1);
}); });
it("keeps an 'until restart' park active forever (never expires until process restart)", () => { it("self-heals a daily-limit park after the daily reset instead of staying locked until process restart", () => {
const key = `${getMegaDebridAccountId("user1")}:api`; const key = `${getMegaDebridAccountId("user1")}:api`;
primeMegaDebridUntilRestartForTests(key); primeMegaDebridUntilRestartForTests(key);
const now = getMegaDebridAccountCooldownState(key); const active = getMegaDebridAccountCooldownState(key);
expect(now?.untilRestart).toBe(true); expect(active?.untilRestart).toBe(true);
const farFuture = Date.now() + 100 * 24 * 60 * 60 * 1000; expect(getMegaDebridAccountCooldownState(key, Date.now() + 60_000)?.untilRestart).toBe(true);
expect(getMegaDebridAccountCooldownState(key, farFuture)?.untilRestart).toBe(true); const afterReset = Date.now() + 25 * 60 * 60 * 1000;
expect(getMegaDebridAccountCooldownState(key, afterReset)).toBeNull();
});
it("does NOT treat a per-hoster 'no server' failure as an account daily-limit signal (no until-restart park)", () => {
const noServer = classifyMegaDebridAccountFailureForTests(new Error("no server available for this host"));
expect(noServer.limitSignal).toBeFalsy();
expect(noServer.category).toBe("quota");
expect(noServer.cooldownMs).toBeGreaterThan(0);
const genuineEmpty = classifyMegaDebridAccountFailureForTests(new Error("Antwort leer"));
expect(genuineEmpty.limitSignal).toBe(true);
}); });
it("skips a Mega-Debrid account parked until restart and rotates to the next, without re-testing it", async () => { it("skips a Mega-Debrid account parked until restart and rotates to the next, without re-testing it", async () => {
@ -1990,7 +2001,7 @@ describe("debrid service", () => {
const megaWeb = vi.fn(async () => ({ fileName: "x.rar", directUrl: "https://mega-web.example/x.rar", fileSize: null, retriesUsed: 0 })); const megaWeb = vi.fn(async () => ({ fileName: "x.rar", directUrl: "https://mega-web.example/x.rar", fileSize: null, retriesUsed: 0 }));
const service = new DebridService(settings, { megaWebUnrestrict: megaWeb }); const service = new DebridService(settings, { megaWebUnrestrict: megaWeb });
await expect(service.unrestrictLink("https://rapidgator.net/file/all-parked-test")).rejects.toThrow(/bis Neustart gesperrt/i); await expect(service.unrestrictLink("https://rapidgator.net/file/all-parked-test")).rejects.toThrow(/bis zum Tagesreset gesperrt/i);
expect(megaWeb).not.toHaveBeenCalled(); expect(megaWeb).not.toHaveBeenCalled();
}, 20000); }, 20000);

View File

@ -0,0 +1,63 @@
import { describe, expect, it } from "vitest";
import { runInstallWithResume, InstallResumeManager } from "../src/main/update-install-flow";
function makeManager(running: boolean): InstallResumeManager & { startCalls: number; stopCalls: number; persistCalls: number; sessionRunning: boolean } {
return {
sessionRunning: running,
startCalls: 0,
stopCalls: 0,
persistCalls: 0,
isSessionRunning() {
return this.sessionRunning;
},
stop() {
this.stopCalls += 1;
this.sessionRunning = false;
},
persistNowSync() {
this.persistCalls += 1;
},
async start() {
this.startCalls += 1;
this.sessionRunning = true;
}
};
}
describe("runInstallWithResume", () => {
it("resumes a running session when the install returns started:false", async () => {
const m = makeManager(true);
const result = await runInstallWithResume(m, async () => ({ started: false }));
expect(result.started).toBe(false);
expect(m.stopCalls).toBe(1);
expect(m.startCalls).toBe(1);
expect(m.isSessionRunning()).toBe(true);
});
it("resumes a running session when the install THROWS, then rethrows", async () => {
const m = makeManager(true);
await expect(
runInstallWithResume(m, async () => {
throw new Error("network down");
})
).rejects.toThrow("network down");
expect(m.stopCalls).toBe(1);
expect(m.startCalls).toBe(1);
expect(m.isSessionRunning()).toBe(true);
});
it("does NOT resume when the install succeeds (started:true) — the app is about to quit", async () => {
const m = makeManager(true);
const result = await runInstallWithResume(m, async () => ({ started: true }));
expect(result.started).toBe(true);
expect(m.startCalls).toBe(0);
expect(m.isSessionRunning()).toBe(false);
});
it("does NOT resume when no session was running before the install", async () => {
const m = makeManager(false);
await runInstallWithResume(m, async () => ({ started: false }));
expect(m.stopCalls).toBe(0);
expect(m.startCalls).toBe(0);
});
});