Fix: Mega-Debrid-Umwandlung robust + parallel — schneller Retry, alle Accounts, sichtbarer Grund

R3 (Kern): Transiente Resolve-Fehler ("Datei beim Hoster gerade nicht
abrufbar", frz. "Fichier supprimé") werden jetzt mit kurzem Delay
(3–6–10s) neu versucht statt mit dem 5s..120s-Exponential. Ein solcher
Einzel-Fehler zählt nicht mehr zum Provider-Circuit-Breaker (der bei 20
Fehlern 30–300s Cooldown setzt und die Mega-Session invalidiert) und
löst keine Provider-Cooldown-Inflation aus — dadurch bremst er die
gesunden Links nicht mehr aus. Neuer Branch vor der generischen
Unrestrict-Retry-Logik; Detektor isMegaDebridTransientResolveFailure
matcht sowohl die rohe französische als auch die gerenderte deutsche
Meldung (am Download-Manager kommt die klassifizierte deutsche an).

R2 (parallel): getSerializedValidatingLimit für megadebrid-web ist nicht
mehr hart 1, sondern die Anzahl nutzbarer Accounts. Der harte Wert 1
stammte aus der Einzel-Account-Zeit (Commit a3e3d6f). Sicher, weil die
Web-Ebene ohnehin pro Account serialisiert (runExclusive, key=login) und
die Account-Rotation gleichzeitige Aufrufe atomar auf distinkte Accounts
verteilt (megaDebridInFlight wird synchron vor dem await erhöht). Ein
Account → Limit 1 (keine Änderung).

R4 (Sichtbarkeit): Der echte Grund steht inline im Status und als Tooltip
(item.lastError); kaputten "?"-Separator zu "(Rn)" gesäubert.

UI-Bugfixes: Account-Duplizierung beim Entfernen (React-Key-Kollision
API/Web → rowKey nach kind), Username-Spalte zeigt eingegebenen Login
statt Anbieter-Mail (Mail als Tooltip).

Tests: transientResolveRetryDelayMs, isMegaDebridTransientResolveFailure,
2 behaviorale Download-Manager-Tests (schneller Re-Try mit deutschem
Grund; 1 Web-Umwandlung pro Account parallel). 833/833 grün, tsc
unverändert.
This commit is contained in:
Sucukdeluxe
2026-06-17 02:07:25 +02:00
parent bec119583d
commit 6d4da02f92
6 changed files with 250 additions and 8 deletions
+11 -4
View File
@@ -2561,7 +2561,7 @@ export function App(): ReactElement {
const used = getMegaDebridAccountDailyUsageBytes(snapshot.settings, acc.id);
const limit = getMegaDebridAccountDailyLimitBytes(settingsDraft, acc.id);
rows.push({
rowKey: `mega-${acc.id}`,
rowKey: `mega-${entry.kind}-${acc.id}`,
entry,
hosterLabel: entry.serviceLabel,
modeLabel: entry.modeLabel,
@@ -2684,7 +2684,8 @@ export function App(): ReactElement {
else if (!st.isPremium) { statusCls = "free"; statusText = "Free Account"; }
else { statusCls = "ok"; statusText = st.message || "Premium Account"; }
const isProblem = statusCls === "invalid";
const username = st && st.email ? st.email : row.username;
const username = row.username;
const usernameTitle = st && st.email && st.email.toLowerCase() !== row.username.toLowerCase() ? `${row.username} (Mail: ${st.email})` : row.username;
const expiry = st && st.premiumUntilMs && st.premiumUntilMs > 0 ? new Date(st.premiumUntilMs).toLocaleDateString("de-DE") : "—";
const traffic = row.dailyLimitBytes > 0
? `${humanSize(row.dailyRemainingBytes)} von ${humanSize(row.dailyLimitBytes)} übrig`
@@ -2714,7 +2715,7 @@ export function App(): ReactElement {
? <span className="acct2-nostatus" title="Für diesen Anbieter gibt es keine Status-Prüfung"></span>
: <span className={`account-validity-badge ${statusCls}`}>{statusText}</span>}
</span>
<span className="acct2-user" title={username}>{username}</span>
<span className="acct2-user" title={usernameTitle}>{username}</span>
<span className="acct2-expiry">{expiry}</span>
<span className="acct2-c-actions">
{row.toggleKind === "single" && getAccountQuickActionMeta(row.entry.kind) && (
@@ -6784,7 +6785,13 @@ const ItemRow = memo(function ItemRow({ item, packageId, isSelected, sessionRunn
}, [packageId, item.id, onContextMenu]);
const formattedCreatedAt = useMemo(() => formatDateTime(item.createdAt), [item.createdAt]);
const displayStatus = useMemo(() => computeDisplayedItemStatus(item, sessionRunning), [item, sessionRunning]);
const statusTitle = displayStatus ? (item.retries > 0 ? `${displayStatus} ? R${item.retries}` : displayStatus) : "";
const retrySuffix = item.retries > 0 ? ` (R${item.retries})` : "";
const lastErrorText = String(item.lastError || "").trim();
const statusTitle = displayStatus
? (lastErrorText && lastErrorText !== displayStatus && !displayStatus.includes(lastErrorText)
? `${displayStatus}${retrySuffix}\n${lastErrorText}`
: `${displayStatus}${retrySuffix}`)
: lastErrorText;
return (
<div