⚡️ perf: improve extraction status, stuck detection, and retry logic
Some checks are pending
Build and Release / build (push) Waiting to run
Some checks are pending
Build and Release / build (push) Waiting to run
- Extraction status: "Entpackt - Done" / "Entpacken - Ausstehend" - Per-item extraction progress (no cross-contamination) - Validating-stuck watchdog: abort items stuck >45s in "Link wird umgewandelt" - Global stall timeout reduced 90s → 60s, unrestrict timeout 120s → 60s - Unrestrict retry: longer backoff (5/10/15s), reset partial downloads - Stall retry: reset partial downloads for fresh link - Mega-Web generate: max 30 polls (was 60), 45s overall timeout - Mega-Web session refresh: 10min (was 20min) - Comprehensive logging on all retry/failure paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0e55c28142
commit
1825e8ba04
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.73",
|
"version": "1.4.74",
|
||||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -43,13 +43,13 @@ const DEFAULT_DOWNLOAD_STALL_TIMEOUT_MS = 30000;
|
|||||||
|
|
||||||
const DEFAULT_DOWNLOAD_CONNECT_TIMEOUT_MS = 25000;
|
const DEFAULT_DOWNLOAD_CONNECT_TIMEOUT_MS = 25000;
|
||||||
|
|
||||||
const DEFAULT_GLOBAL_STALL_WATCHDOG_TIMEOUT_MS = 90000;
|
const DEFAULT_GLOBAL_STALL_WATCHDOG_TIMEOUT_MS = 60000;
|
||||||
|
|
||||||
const DEFAULT_POST_EXTRACT_TIMEOUT_MS = 4 * 60 * 60 * 1000;
|
const DEFAULT_POST_EXTRACT_TIMEOUT_MS = 4 * 60 * 60 * 1000;
|
||||||
|
|
||||||
const EXTRACT_PROGRESS_EMIT_INTERVAL_MS = 260;
|
const EXTRACT_PROGRESS_EMIT_INTERVAL_MS = 260;
|
||||||
|
|
||||||
const DEFAULT_UNRESTRICT_TIMEOUT_MS = 120000;
|
const DEFAULT_UNRESTRICT_TIMEOUT_MS = 60000;
|
||||||
|
|
||||||
const DEFAULT_LOW_THROUGHPUT_TIMEOUT_MS = 120000;
|
const DEFAULT_LOW_THROUGHPUT_TIMEOUT_MS = 120000;
|
||||||
|
|
||||||
@ -2847,7 +2847,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
pkg.updatedAt = nowMs();
|
pkg.updatedAt = nowMs();
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.status === "completed" && !isExtractedLabel(item.fullStatus)) {
|
if (item.status === "completed" && !isExtractedLabel(item.fullStatus)) {
|
||||||
item.fullStatus = "Entpacken ausstehend";
|
item.fullStatus = "Entpacken - Ausstehend";
|
||||||
item.updatedAt = nowMs();
|
item.updatedAt = nowMs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2911,7 +2911,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
pkg.updatedAt = nowMs();
|
pkg.updatedAt = nowMs();
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item.status === "completed" && !isExtractedLabel(item.fullStatus)) {
|
if (item.status === "completed" && !isExtractedLabel(item.fullStatus)) {
|
||||||
item.fullStatus = "Entpacken ausstehend";
|
item.fullStatus = "Entpacken - Ausstehend";
|
||||||
item.updatedAt = nowMs();
|
item.updatedAt = nowMs();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3026,6 +3026,24 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-item validating watchdog: abort items stuck in "validating" for >45s
|
||||||
|
const VALIDATING_STUCK_MS = 45000;
|
||||||
|
for (const active of this.activeTasks.values()) {
|
||||||
|
if (active.abortController.signal.aborted) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const item = this.session.items[active.itemId];
|
||||||
|
if (!item || item.status !== "validating") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ageMs = item.updatedAt > 0 ? now - item.updatedAt : 0;
|
||||||
|
if (ageMs > VALIDATING_STUCK_MS) {
|
||||||
|
logger.warn(`Validating-Stuck erkannt: item=${item.fileName || active.itemId}, ${Math.floor(ageMs / 1000)}s ohne Fortschritt`);
|
||||||
|
active.abortReason = "stall";
|
||||||
|
active.abortController.abort("stall");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.session.totalDownloadedBytes !== this.lastGlobalProgressBytes) {
|
if (this.session.totalDownloadedBytes !== this.lastGlobalProgressBytes) {
|
||||||
this.lastGlobalProgressBytes = this.session.totalDownloadedBytes;
|
this.lastGlobalProgressBytes = this.session.totalDownloadedBytes;
|
||||||
this.lastGlobalProgressAt = now;
|
this.lastGlobalProgressAt = now;
|
||||||
@ -3042,7 +3060,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const item = this.session.items[active.itemId];
|
const item = this.session.items[active.itemId];
|
||||||
if (item && item.status === "downloading") {
|
if (item && (item.status === "downloading" || item.status === "validating")) {
|
||||||
stalledCount += 1;
|
stalledCount += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3057,7 +3075,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const item = this.session.items[active.itemId];
|
const item = this.session.items[active.itemId];
|
||||||
if (item && item.status === "downloading") {
|
if (item && (item.status === "downloading" || item.status === "validating")) {
|
||||||
active.abortReason = "stall";
|
active.abortReason = "stall";
|
||||||
active.abortController.abort("stall");
|
active.abortController.abort("stall");
|
||||||
}
|
}
|
||||||
@ -3250,6 +3268,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
item.status = "validating";
|
item.status = "validating";
|
||||||
item.fullStatus = "Link wird umgewandelt";
|
item.fullStatus = "Link wird umgewandelt";
|
||||||
|
item.speedBps = 0;
|
||||||
|
// Reset stale progress so UI doesn't show old % while re-validating
|
||||||
|
if (item.downloadedBytes === 0) {
|
||||||
|
item.progressPercent = 0;
|
||||||
|
}
|
||||||
item.updatedAt = nowMs();
|
item.updatedAt = nowMs();
|
||||||
pkg.status = "downloading";
|
pkg.status = "downloading";
|
||||||
pkg.updatedAt = nowMs();
|
pkg.updatedAt = nowMs();
|
||||||
@ -3433,7 +3456,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
item.status = "completed";
|
item.status = "completed";
|
||||||
item.fullStatus = `Fertig (${humanSize(item.downloadedBytes)})`;
|
item.fullStatus = this.settings.autoExtract
|
||||||
|
? "Entpacken - Ausstehend"
|
||||||
|
: `Fertig (${humanSize(item.downloadedBytes)})`;
|
||||||
item.progressPercent = 100;
|
item.progressPercent = 100;
|
||||||
item.speedBps = 0;
|
item.speedBps = 0;
|
||||||
item.updatedAt = nowMs();
|
item.updatedAt = nowMs();
|
||||||
@ -3503,20 +3528,37 @@ export class DownloadManager extends EventEmitter {
|
|||||||
} else if (reason === "stall") {
|
} else if (reason === "stall") {
|
||||||
const stallErrorText = compactErrorText(error);
|
const stallErrorText = compactErrorText(error);
|
||||||
const isSlowThroughput = stallErrorText.includes("slow_throughput");
|
const isSlowThroughput = stallErrorText.includes("slow_throughput");
|
||||||
|
const wasValidating = item.status === "validating";
|
||||||
active.stallRetries += 1;
|
active.stallRetries += 1;
|
||||||
|
const stallDelayMs = retryDelayWithJitter(active.stallRetries, 500);
|
||||||
|
logger.warn(`Stall erkannt: item=${item.fileName || item.id}, phase=${wasValidating ? "validating" : "downloading"}, retry=${active.stallRetries}/${retryDisplayLimit}, bytes=${item.downloadedBytes}, error=${stallErrorText || "none"}, provider=${item.provider || "?"}`);
|
||||||
if (active.stallRetries <= maxStallRetries) {
|
if (active.stallRetries <= maxStallRetries) {
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
const retryText = isSlowThroughput
|
// Reset partial download so next attempt uses a fresh link
|
||||||
? `Zu wenig Datenfluss, Retry ${active.stallRetries}/${retryDisplayLimit}`
|
if (item.downloadedBytes > 0) {
|
||||||
: `Keine Daten empfangen, Retry ${active.stallRetries}/${retryDisplayLimit}`;
|
const targetFile = this.claimedTargetPathByItem.get(item.id) || "";
|
||||||
this.queueRetry(item, active, 350 * active.stallRetries, retryText);
|
if (targetFile) {
|
||||||
|
try { fs.rmSync(targetFile, { force: true }); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
this.releaseTargetPath(item.id);
|
||||||
|
item.downloadedBytes = 0;
|
||||||
|
item.progressPercent = 0;
|
||||||
|
item.totalBytes = null;
|
||||||
|
this.dropItemContribution(item.id);
|
||||||
|
}
|
||||||
|
const retryText = wasValidating
|
||||||
|
? `Link-Umwandlung hing, Retry ${active.stallRetries}/${retryDisplayLimit}`
|
||||||
|
: isSlowThroughput
|
||||||
|
? `Zu wenig Datenfluss, Retry ${active.stallRetries}/${retryDisplayLimit}`
|
||||||
|
: `Keine Daten empfangen, Retry ${active.stallRetries}/${retryDisplayLimit}`;
|
||||||
|
this.queueRetry(item, active, stallDelayMs, retryText);
|
||||||
item.lastError = "";
|
item.lastError = "";
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
this.emitState();
|
this.emitState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
item.status = "failed";
|
item.status = "failed";
|
||||||
item.lastError = "Download hing wiederholt";
|
item.lastError = wasValidating ? "Link-Umwandlung hing wiederholt" : "Download hing wiederholt";
|
||||||
item.fullStatus = `Fehler: ${item.lastError}`;
|
item.fullStatus = `Fehler: ${item.lastError}`;
|
||||||
this.recordRunOutcome(item.id, "failed");
|
this.recordRunOutcome(item.id, "failed");
|
||||||
this.retryStateByItem.delete(item.id);
|
this.retryStateByItem.delete(item.id);
|
||||||
@ -3549,6 +3591,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (shouldFreshRetry) {
|
if (shouldFreshRetry) {
|
||||||
active.freshRetryUsed = true;
|
active.freshRetryUsed = true;
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
|
logger.warn(`Netzwerkfehler: item=${item.fileName || item.id}, fresh retry, error=${errorText}, provider=${item.provider || "?"}`);
|
||||||
try {
|
try {
|
||||||
fs.rmSync(item.targetPath, { force: true });
|
fs.rmSync(item.targetPath, { force: true });
|
||||||
} catch {
|
} catch {
|
||||||
@ -3568,7 +3611,22 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (isUnrestrictFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) {
|
if (isUnrestrictFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) {
|
||||||
active.unrestrictRetries += 1;
|
active.unrestrictRetries += 1;
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
this.queueRetry(item, active, Math.min(8000, 2000 * active.unrestrictRetries), `Unrestrict-Fehler, Retry ${active.unrestrictRetries}/${retryDisplayLimit}`);
|
// Longer backoff for unrestrict: 5s, 10s, 15s (capped at 15s) to let API cache expire
|
||||||
|
const unrestrictDelayMs = Math.min(15000, 5000 * active.unrestrictRetries);
|
||||||
|
logger.warn(`Unrestrict-Fehler: item=${item.fileName || item.id}, retry=${active.unrestrictRetries}/${retryDisplayLimit}, delay=${unrestrictDelayMs}ms, error=${errorText}, link=${item.url.slice(0, 80)}`);
|
||||||
|
// Reset partial download so next attempt starts fresh
|
||||||
|
if (item.downloadedBytes > 0) {
|
||||||
|
const targetFile = this.claimedTargetPathByItem.get(item.id) || "";
|
||||||
|
if (targetFile) {
|
||||||
|
try { fs.rmSync(targetFile, { force: true }); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
this.releaseTargetPath(item.id);
|
||||||
|
item.downloadedBytes = 0;
|
||||||
|
item.progressPercent = 0;
|
||||||
|
item.totalBytes = null;
|
||||||
|
this.dropItemContribution(item.id);
|
||||||
|
}
|
||||||
|
this.queueRetry(item, active, unrestrictDelayMs, `Unrestrict-Fehler, Retry ${active.unrestrictRetries}/${retryDisplayLimit} (${Math.ceil(unrestrictDelayMs / 1000)}s)`);
|
||||||
item.lastError = errorText;
|
item.lastError = errorText;
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
this.emitState();
|
this.emitState();
|
||||||
@ -3578,7 +3636,9 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (active.genericErrorRetries < maxGenericErrorRetries) {
|
if (active.genericErrorRetries < maxGenericErrorRetries) {
|
||||||
active.genericErrorRetries += 1;
|
active.genericErrorRetries += 1;
|
||||||
item.retries += 1;
|
item.retries += 1;
|
||||||
this.queueRetry(item, active, Math.min(1200, 300 * active.genericErrorRetries), `Fehler erkannt, Auto-Retry ${active.genericErrorRetries}/${retryDisplayLimit}`);
|
const genericDelayMs = retryDelayWithJitter(active.genericErrorRetries, 400);
|
||||||
|
logger.warn(`Generic-Fehler: item=${item.fileName || item.id}, retry=${active.genericErrorRetries}/${retryDisplayLimit}, error=${errorText}, provider=${item.provider || "?"}`);
|
||||||
|
this.queueRetry(item, active, genericDelayMs, `Fehler erkannt, Auto-Retry ${active.genericErrorRetries}/${retryDisplayLimit}`);
|
||||||
item.lastError = errorText;
|
item.lastError = errorText;
|
||||||
this.persistSoon();
|
this.persistSoon();
|
||||||
this.emitState();
|
this.emitState();
|
||||||
@ -3589,6 +3649,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.recordRunOutcome(item.id, "failed");
|
this.recordRunOutcome(item.id, "failed");
|
||||||
item.lastError = errorText;
|
item.lastError = errorText;
|
||||||
item.fullStatus = `Fehler: ${item.lastError}`;
|
item.fullStatus = `Fehler: ${item.lastError}`;
|
||||||
|
logger.error(`Item endgültig fehlgeschlagen: item=${item.fileName || item.id}, error=${errorText}, provider=${item.provider || "?"}, stallRetries=${active.stallRetries}, unrestrictRetries=${active.unrestrictRetries}, genericRetries=${active.genericErrorRetries}`);
|
||||||
this.retryStateByItem.delete(item.id);
|
this.retryStateByItem.delete(item.id);
|
||||||
}
|
}
|
||||||
item.speedBps = 0;
|
item.speedBps = 0;
|
||||||
@ -4490,7 +4551,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const resolveArchiveItems = (archiveName: string): DownloadItem[] =>
|
const resolveArchiveItems = (archiveName: string): DownloadItem[] =>
|
||||||
resolveArchiveItemsFromList(archiveName, hybridItems);
|
resolveArchiveItemsFromList(archiveName, hybridItems);
|
||||||
|
|
||||||
let currentArchiveItems: DownloadItem[] = hybridItems;
|
// Only update the items currently being extracted, not all hybrid items at once
|
||||||
|
let currentArchiveItems: DownloadItem[] = [];
|
||||||
const updateExtractingStatus = (text: string): void => {
|
const updateExtractingStatus = (text: string): void => {
|
||||||
const normalized = String(text || "");
|
const normalized = String(text || "");
|
||||||
if (hybridLastStatusText === normalized) {
|
if (hybridLastStatusText === normalized) {
|
||||||
@ -4523,7 +4585,14 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.emitState();
|
this.emitState();
|
||||||
};
|
};
|
||||||
|
|
||||||
emitHybridStatus("Entpacken (hybrid) 0%", true);
|
// Mark items not yet being extracted as pending
|
||||||
|
for (const entry of hybridItems) {
|
||||||
|
if (!isExtractedLabel(entry.fullStatus)) {
|
||||||
|
entry.fullStatus = "Entpacken - Ausstehend";
|
||||||
|
entry.updatedAt = nowMs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.emitState();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await extractPackageArchives({
|
const result = await extractPackageArchives({
|
||||||
@ -4542,20 +4611,20 @@ export class DownloadManager extends EventEmitter {
|
|||||||
if (progress.phase === "done") {
|
if (progress.phase === "done") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// When a new archive starts, mark the previous archive's items as "Entpackt"
|
// When a new archive starts, mark the previous archive's items as done
|
||||||
if (progress.archiveName && progress.archiveName !== lastHybridArchiveName) {
|
if (progress.archiveName && progress.archiveName !== lastHybridArchiveName) {
|
||||||
if (lastHybridArchiveName && currentArchiveItems !== hybridItems) {
|
if (lastHybridArchiveName && currentArchiveItems.length > 0) {
|
||||||
const doneAt = nowMs();
|
const doneAt = nowMs();
|
||||||
for (const entry of currentArchiveItems) {
|
for (const entry of currentArchiveItems) {
|
||||||
if (!isExtractedLabel(entry.fullStatus)) {
|
if (!isExtractedLabel(entry.fullStatus)) {
|
||||||
entry.fullStatus = "Entpackt";
|
entry.fullStatus = "Entpackt - Done";
|
||||||
entry.updatedAt = doneAt;
|
entry.updatedAt = doneAt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastHybridArchiveName = progress.archiveName;
|
lastHybridArchiveName = progress.archiveName;
|
||||||
const resolved = resolveArchiveItems(progress.archiveName);
|
const resolved = resolveArchiveItems(progress.archiveName);
|
||||||
currentArchiveItems = resolved.length > 0 ? resolved : hybridItems;
|
currentArchiveItems = resolved;
|
||||||
}
|
}
|
||||||
const archive = progress.archiveName ? ` · ${progress.archiveName}` : "";
|
const archive = progress.archiveName ? ` · ${progress.archiveName}` : "";
|
||||||
const elapsed = progress.elapsedMs && progress.elapsedMs >= 1000
|
const elapsed = progress.elapsedMs && progress.elapsedMs >= 1000
|
||||||
@ -4563,7 +4632,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
: "";
|
: "";
|
||||||
const activeArchive = Number(progress.archivePercent ?? 0) > 0 ? 1 : 0;
|
const activeArchive = Number(progress.archivePercent ?? 0) > 0 ? 1 : 0;
|
||||||
const currentDisplay = Math.max(0, Math.min(progress.total, progress.current + activeArchive));
|
const currentDisplay = Math.max(0, Math.min(progress.total, progress.current + activeArchive));
|
||||||
const label = `Entpacken (hybrid) ${progress.percent}% (${currentDisplay}/${progress.total})${archive}${elapsed}`;
|
const label = `Entpacken ${progress.percent}% (${currentDisplay}/${progress.total})${archive}${elapsed}`;
|
||||||
emitHybridStatus(label);
|
emitHybridStatus(label);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -4576,18 +4645,17 @@ export class DownloadManager extends EventEmitter {
|
|||||||
logger.warn(`Hybrid-Extract: ${result.failed} Archive fehlgeschlagen, wird beim finalen Durchlauf erneut versucht`);
|
logger.warn(`Hybrid-Extract: ${result.failed} Archive fehlgeschlagen, wird beim finalen Durchlauf erneut versucht`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark all hybrid items with final status.
|
// Mark hybrid items with final status
|
||||||
// Use completedItems (not just hybridItems) so that items not matched to any archive
|
|
||||||
// also get marked — this prevents the final full extraction from re-running.
|
|
||||||
const updatedAt = nowMs();
|
const updatedAt = nowMs();
|
||||||
const targetItems = result.extracted > 0 && result.failed === 0 ? completedItems : hybridItems;
|
const targetItems = result.extracted > 0 && result.failed === 0 ? completedItems : hybridItems;
|
||||||
for (const entry of targetItems) {
|
for (const entry of targetItems) {
|
||||||
if (isExtractedLabel(entry.fullStatus)) {
|
if (isExtractedLabel(entry.fullStatus)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (/^Entpacken \(hybrid\)/i.test(entry.fullStatus || "") || /^Fertig\b/i.test(entry.fullStatus || "")) {
|
const status = entry.fullStatus || "";
|
||||||
|
if (/^Entpacken\b/i.test(status) || /^Fertig\b/i.test(status)) {
|
||||||
if (result.extracted > 0 && result.failed === 0) {
|
if (result.extracted > 0 && result.failed === 0) {
|
||||||
entry.fullStatus = "Entpackt";
|
entry.fullStatus = "Entpackt - Done";
|
||||||
} else {
|
} else {
|
||||||
entry.fullStatus = `Fertig (${humanSize(entry.downloadedBytes)})`;
|
entry.fullStatus = `Fertig (${humanSize(entry.downloadedBytes)})`;
|
||||||
}
|
}
|
||||||
@ -4649,7 +4717,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
const resolveArchiveItems = (archiveName: string): DownloadItem[] =>
|
const resolveArchiveItems = (archiveName: string): DownloadItem[] =>
|
||||||
resolveArchiveItemsFromList(archiveName, completedItems);
|
resolveArchiveItemsFromList(archiveName, completedItems);
|
||||||
|
|
||||||
let currentArchiveItems: DownloadItem[] = completedItems;
|
// Only update items of the currently extracting archive, not all items
|
||||||
|
let currentArchiveItems: DownloadItem[] = [];
|
||||||
const updateExtractingStatus = (text: string): void => {
|
const updateExtractingStatus = (text: string): void => {
|
||||||
const normalized = String(text || "");
|
const normalized = String(text || "");
|
||||||
if (lastExtractStatusText === normalized) {
|
if (lastExtractStatusText === normalized) {
|
||||||
@ -4682,7 +4751,14 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.emitState();
|
this.emitState();
|
||||||
};
|
};
|
||||||
|
|
||||||
emitExtractStatus("Entpacken 0%", true);
|
// Mark all items as pending before extraction starts
|
||||||
|
for (const entry of completedItems) {
|
||||||
|
if (!isExtractedLabel(entry.fullStatus)) {
|
||||||
|
entry.fullStatus = "Entpacken - Ausstehend";
|
||||||
|
entry.updatedAt = nowMs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.emitState();
|
||||||
|
|
||||||
const extractTimeoutMs = getPostExtractTimeoutMs();
|
const extractTimeoutMs = getPostExtractTimeoutMs();
|
||||||
const extractAbortController = new AbortController();
|
const extractAbortController = new AbortController();
|
||||||
@ -4722,20 +4798,19 @@ export class DownloadManager extends EventEmitter {
|
|||||||
signal: extractAbortController.signal,
|
signal: extractAbortController.signal,
|
||||||
packageId,
|
packageId,
|
||||||
onProgress: (progress) => {
|
onProgress: (progress) => {
|
||||||
// When a new archive starts, mark the previous archive's items as "Entpackt"
|
// When a new archive starts, mark the previous archive's items as done
|
||||||
if (progress.archiveName && progress.archiveName !== lastExtractArchiveName) {
|
if (progress.archiveName && progress.archiveName !== lastExtractArchiveName) {
|
||||||
if (lastExtractArchiveName && currentArchiveItems !== completedItems) {
|
if (lastExtractArchiveName && currentArchiveItems.length > 0) {
|
||||||
const doneAt = nowMs();
|
const doneAt = nowMs();
|
||||||
for (const entry of currentArchiveItems) {
|
for (const entry of currentArchiveItems) {
|
||||||
if (!isExtractedLabel(entry.fullStatus)) {
|
if (!isExtractedLabel(entry.fullStatus)) {
|
||||||
entry.fullStatus = "Entpackt";
|
entry.fullStatus = "Entpackt - Done";
|
||||||
entry.updatedAt = doneAt;
|
entry.updatedAt = doneAt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastExtractArchiveName = progress.archiveName;
|
lastExtractArchiveName = progress.archiveName;
|
||||||
const resolved = resolveArchiveItems(progress.archiveName);
|
currentArchiveItems = resolveArchiveItems(progress.archiveName);
|
||||||
currentArchiveItems = resolved.length > 0 ? resolved : completedItems;
|
|
||||||
}
|
}
|
||||||
const label = progress.phase === "done"
|
const label = progress.phase === "done"
|
||||||
? "Entpacken 100%"
|
? "Entpacken 100%"
|
||||||
@ -4768,7 +4843,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
let finalStatusText = "";
|
let finalStatusText = "";
|
||||||
|
|
||||||
if (result.extracted > 0 || hasExtractedOutput) {
|
if (result.extracted > 0 || hasExtractedOutput) {
|
||||||
finalStatusText = "Entpackt";
|
finalStatusText = "Entpackt - Done";
|
||||||
} else if (!sourceExists) {
|
} else if (!sourceExists) {
|
||||||
finalStatusText = "Entpackt (Quelle fehlt)";
|
finalStatusText = "Entpackt (Quelle fehlt)";
|
||||||
logger.warn(`Post-Processing ohne Quellordner: pkg=${pkg.name}, outputDir fehlt`);
|
logger.warn(`Post-Processing ohne Quellordner: pkg=${pkg.name}, outputDir fehlt`);
|
||||||
|
|||||||
@ -196,7 +196,7 @@ export class MegaWebFallback {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.cookie || Date.now() - this.cookieSetAt > 20 * 60 * 1000) {
|
if (!this.cookie || Date.now() - this.cookieSetAt > 10 * 60 * 1000) {
|
||||||
await this.login(creds.login, creds.password, signal);
|
await this.login(creds.login, creds.password, signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,6 +278,8 @@ export class MegaWebFallback {
|
|||||||
|
|
||||||
private async generate(link: string, signal?: AbortSignal): Promise<{ directUrl: string; fileName: string } | null> {
|
private async generate(link: string, signal?: AbortSignal): Promise<{ directUrl: string; fileName: string } | null> {
|
||||||
throwIfAborted(signal);
|
throwIfAborted(signal);
|
||||||
|
// Overall timeout for the entire generate operation (45s)
|
||||||
|
const generateSignal = withTimeoutSignal(signal, 45000);
|
||||||
const page = await fetch(DEBRID_URL, {
|
const page = await fetch(DEBRID_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -291,7 +293,7 @@ export class MegaWebFallback {
|
|||||||
password: "",
|
password: "",
|
||||||
showLinks: "1"
|
showLinks: "1"
|
||||||
}),
|
}),
|
||||||
signal: withTimeoutSignal(signal, 30000)
|
signal: withTimeoutSignal(generateSignal, 20000)
|
||||||
});
|
});
|
||||||
|
|
||||||
const html = await page.text();
|
const html = await page.text();
|
||||||
@ -300,8 +302,10 @@ export class MegaWebFallback {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= 60; attempt += 1) {
|
let reloadCount = 0;
|
||||||
throwIfAborted(signal);
|
let hosterRetryCount = 0;
|
||||||
|
for (let attempt = 1; attempt <= 30; attempt += 1) {
|
||||||
|
throwIfAborted(generateSignal);
|
||||||
const res = await fetch(DEBRID_AJAX_URL, {
|
const res = await fetch(DEBRID_AJAX_URL, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
@ -314,12 +318,14 @@ export class MegaWebFallback {
|
|||||||
code,
|
code,
|
||||||
autodl: "0"
|
autodl: "0"
|
||||||
}),
|
}),
|
||||||
signal: withTimeoutSignal(signal, 15000)
|
signal: withTimeoutSignal(generateSignal, 12000)
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = (await res.text()).trim();
|
const text = (await res.text()).trim();
|
||||||
if (text === "reload") {
|
if (text === "reload") {
|
||||||
await sleepWithSignal(650, signal);
|
reloadCount += 1;
|
||||||
|
// Back off progressively: 500ms, 700ms, 900ms...
|
||||||
|
await sleepWithSignal(Math.min(2000, 500 + reloadCount * 200), generateSignal);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (text === "false") {
|
if (text === "false") {
|
||||||
@ -333,7 +339,11 @@ export class MegaWebFallback {
|
|||||||
|
|
||||||
if (!parsed.link) {
|
if (!parsed.link) {
|
||||||
if (/hoster does not respond correctly|could not be done for this moment/i.test(parsed.text || "")) {
|
if (/hoster does not respond correctly|could not be done for this moment/i.test(parsed.text || "")) {
|
||||||
await sleepWithSignal(1200, signal);
|
hosterRetryCount += 1;
|
||||||
|
if (hosterRetryCount > 5) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
await sleepWithSignal(Math.min(3000, 800 + hosterRetryCount * 400), generateSignal);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user