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:
@@ -22,6 +22,7 @@ import {
|
||||
StartConflictResolutionResult,
|
||||
UiSnapshot, DebridAccountStatus } from "../shared/types";
|
||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||
import { isMegaDebridTransientResolveFailure, germanMegaDebridResolveReason } from "../shared/mega-debrid-errors";
|
||||
import {
|
||||
addDebridLinkApiKeyDailyUsageBytes,
|
||||
addDebridLinkApiKeyTotalUsageBytes,
|
||||
@@ -51,7 +52,7 @@ function releaseTlsSkip(): void {
|
||||
}
|
||||
import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
|
||||
import { planDownloadCompletion, validateDownloadedFileCompletion } from "./download-completion";
|
||||
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState } from "./debrid";
|
||||
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, 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";
|
||||
@@ -701,6 +702,12 @@ function isTemporaryUnrestrictError(errorText: string): boolean {
|
||||
|| text.includes("worker error");
|
||||
}
|
||||
|
||||
export function transientResolveRetryDelayMs(retryCount: number): number {
|
||||
const steps = [3000, 6000, 10000];
|
||||
const n = Math.max(1, Math.floor(Number(retryCount) || 1));
|
||||
return steps[Math.min(n - 1, steps.length - 1)];
|
||||
}
|
||||
|
||||
function isFinishedStatus(status: DownloadStatus): boolean {
|
||||
return status === "completed" || status === "failed" || status === "cancelled";
|
||||
}
|
||||
@@ -7974,7 +7981,7 @@ export class DownloadManager extends EventEmitter {
|
||||
|
||||
private getSerializedValidatingLimit(provider: DebridProvider | null): number {
|
||||
if (provider === "megadebrid-web") {
|
||||
return 1;
|
||||
return Math.max(1, getAvailableMegaDebridAccounts(this.settings).length);
|
||||
}
|
||||
return Number.MAX_SAFE_INTEGER;
|
||||
}
|
||||
@@ -9297,6 +9304,35 @@ export class DownloadManager extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMegaDebridTransientResolveFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) {
|
||||
active.unrestrictRetries += 1;
|
||||
item.retries += 1;
|
||||
const transientDelayMs = transientResolveRetryDelayMs(active.unrestrictRetries);
|
||||
const transientReason = germanMegaDebridResolveReason(errorText);
|
||||
logger.warn(`Transienter Mega-Debrid-Resolve-Fehler: item=${item.fileName || item.id}, retry=${active.unrestrictRetries}/${retryDisplayLimit}, delay=${transientDelayMs}ms, error=${errorText}, link=${item.url.slice(0, 80)}`);
|
||||
if (item.downloadedBytes > 0) {
|
||||
const targetFile = this.claimedTargetPathByItem.get(item.id) || "";
|
||||
if (targetFile) {
|
||||
try { fs.rmSync(targetFile, { force: true }); } catch { }
|
||||
}
|
||||
this.releaseTargetPath(item.id);
|
||||
item.downloadedBytes = 0;
|
||||
item.progressPercent = 0;
|
||||
item.totalBytes = null;
|
||||
this.dropItemContribution(item.id);
|
||||
}
|
||||
this.queueRetry(
|
||||
item,
|
||||
active,
|
||||
transientDelayMs,
|
||||
`${transientReason} — neuer Versuch ${active.unrestrictRetries}/${retryDisplayLimit} (${Math.ceil(transientDelayMs / 1000)}s)`
|
||||
);
|
||||
item.lastError = transientReason;
|
||||
this.persistSoon();
|
||||
this.emitState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isUnrestrictFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) {
|
||||
const debridLinkCooldown = parseDebridLinkCooldownRetry(errorText);
|
||||
if (debridLinkCooldown) {
|
||||
|
||||
+11
-4
@@ -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
|
||||
|
||||
@@ -7,9 +7,17 @@ export function isMegaDebridResolveFailure(errorText: string): boolean {
|
||||
|| text.includes("fichier inexistant");
|
||||
}
|
||||
|
||||
export function isMegaDebridTransientResolveFailure(errorText: string): boolean {
|
||||
const text = String(errorText || "").toLowerCase();
|
||||
return isMegaDebridResolveFailure(text)
|
||||
|| text.includes("datei beim hoster gerade nicht abrufbar")
|
||||
|| text.includes("datei beim hoster nicht gefunden");
|
||||
}
|
||||
|
||||
export function germanMegaDebridResolveReason(errorText: string): string {
|
||||
const text = String(errorText || "").toLowerCase();
|
||||
if (text.includes("introuvable") || text.includes("fichier inexistant") || text.includes("n'existe plus") || text.includes("n existe plus")) {
|
||||
if (text.includes("datei beim hoster nicht gefunden")
|
||||
|| text.includes("introuvable") || text.includes("fichier inexistant") || text.includes("n'existe plus") || text.includes("n existe plus")) {
|
||||
return "Datei beim Hoster nicht gefunden";
|
||||
}
|
||||
return "Datei beim Hoster gerade nicht abrufbar";
|
||||
|
||||
Reference in New Issue
Block a user