fix: clamp ETA bounds, store stats interval, add activeDownloads finally-cleanup, prevent progress backward jump

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
xRangerDE 2026-03-21 15:04:59 +01:00
parent 4607ba9cc6
commit 6c47c63fa8
2 changed files with 86 additions and 79 deletions

View File

@ -2204,13 +2204,15 @@ function downloadVODPart(
if (speed > 0 && downloadedBytes > 0) { if (speed > 0 && downloadedBytes > 0) {
const itemStartTime = itemTracking.startTime; const itemStartTime = itemTracking.startTime;
const elapsedSec = (Date.now() - itemStartTime) / 1000; const elapsedSec = (Date.now() - itemStartTime) / 1000;
if (elapsedSec > 3) { if (elapsedSec > 5) { // Wait at least 5 seconds before showing ETA
const avgSpeed = downloadedBytes / elapsedSec; const avgSpeed = downloadedBytes / elapsedSec;
if (expectedDurationSeconds && expectedDurationSeconds > 0) { if (expectedDurationSeconds && expectedDurationSeconds > 0) {
const estimatedTotalBytes = avgSpeed * expectedDurationSeconds; const estimatedTotalBytes = avgSpeed * expectedDurationSeconds;
if (estimatedTotalBytes > downloadedBytes) { if (estimatedTotalBytes > downloadedBytes) {
const remainingSec = (estimatedTotalBytes - downloadedBytes) / avgSpeed; const remainingSec = (estimatedTotalBytes - downloadedBytes) / avgSpeed;
etaStr = formatETA(remainingSec); if (remainingSec > 0 && remainingSec < 86400) { // Between 0 and 24 hours
etaStr = formatETA(remainingSec);
}
} }
} }
} }
@ -2798,96 +2800,96 @@ async function processOneQueueItem(item: QueueItem): Promise<void> {
item.last_error = ''; item.last_error = '';
let finalResult: DownloadResult = { success: false, error: 'Unbekannter Fehler beim Download' }; try {
const maxAttempts = getRetryAttemptLimit(); let finalResult: DownloadResult = { success: false, error: 'Unbekannter Fehler beim Download' };
const maxAttempts = getRetryAttemptLimit();
for (let attempt = 1; attempt <= maxAttempts; attempt++) { for (let attempt = 1; attempt <= maxAttempts; attempt++) {
appendDebugLog('queue-item-attempt', { itemId: item.id, attempt, max: maxAttempts }); appendDebugLog('queue-item-attempt', { itemId: item.id, attempt, max: maxAttempts });
const result = item.mergeGroup const result = item.mergeGroup
? await processDownloadMergeGroup(item, (progress) => { ? await processDownloadMergeGroup(item, (progress) => {
mainWindow?.webContents.send('download-progress', progress); mainWindow?.webContents.send('download-progress', progress);
}) })
: await downloadVOD(item, (progress) => { : await downloadVOD(item, (progress) => {
mainWindow?.webContents.send('download-progress', progress); mainWindow?.webContents.send('download-progress', progress);
}); });
if (result.success) {
finalResult = result;
break;
}
if (result.success) {
finalResult = result; finalResult = result;
break;
if (!isDownloading || currentDownloadCancelled || cancelledItemIds.has(item.id) || pauseRequested) {
finalResult = { success: false, error: pauseRequested ? 'Download wurde pausiert.' : 'Download wurde abgebrochen.' };
break;
}
const errorClass = classifyDownloadError(result.error || '');
runtimeMetrics.lastErrorClass = errorClass;
if (errorClass === 'tooling' || errorClass === 'validation') {
appendDebugLog('queue-item-no-retry', {
itemId: item.id,
errorClass,
error: result.error || 'unknown'
});
break;
}
if (attempt < maxAttempts) {
const retryDelaySeconds = getRetryDelaySeconds(errorClass, attempt);
runtimeMetrics.retriesScheduled += 1;
runtimeMetrics.lastRetryDelaySeconds = retryDelaySeconds;
item.last_error = `Versuch ${attempt}/${maxAttempts} fehlgeschlagen (${errorClass}): ${result.error || 'Unbekannter Fehler'}`;
mainWindow?.webContents.send('download-progress', {
id: item.id,
progress: -1,
speed: '',
eta: '',
status: `Neuer Versuch in ${retryDelaySeconds}s (${errorClass})...`,
currentPart: item.currentPart,
totalParts: item.totalParts
} as DownloadProgress);
saveQueue(downloadQueue);
emitQueueUpdated();
await sleep(retryDelaySeconds * 1000);
} else {
runtimeMetrics.retriesExhausted += 1;
}
} }
finalResult = result; if (!hasQueueItemId(item.id)) {
appendDebugLog('queue-item-finished-removed', { itemId: item.id });
if (!isDownloading || currentDownloadCancelled || cancelledItemIds.has(item.id) || pauseRequested) { return;
finalResult = { success: false, error: pauseRequested ? 'Download wurde pausiert.' : 'Download wurde abgebrochen.' };
break;
} }
const errorClass = classifyDownloadError(result.error || ''); const wasPaused = pauseRequested || (finalResult.error || '').includes('pausiert');
runtimeMetrics.lastErrorClass = errorClass; item.status = finalResult.success ? 'completed' : (wasPaused ? 'paused' : 'error');
item.progress = finalResult.success ? 100 : item.progress;
item.last_error = finalResult.success || wasPaused ? '' : (finalResult.error || 'Unbekannter Fehler beim Download');
if (errorClass === 'tooling' || errorClass === 'validation') { if (finalResult.success) {
appendDebugLog('queue-item-no-retry', { runtimeMetrics.downloadsCompleted += 1;
itemId: item.id, } else if (!wasPaused) {
errorClass, runtimeMetrics.downloadsFailed += 1;
error: result.error || 'unknown'
});
break;
} }
if (attempt < maxAttempts) { appendDebugLog('queue-item-finished', {
const retryDelaySeconds = getRetryDelaySeconds(errorClass, attempt); itemId: item.id,
runtimeMetrics.retriesScheduled += 1; status: item.status,
runtimeMetrics.lastRetryDelaySeconds = retryDelaySeconds; error: item.last_error
});
item.last_error = `Versuch ${attempt}/${maxAttempts} fehlgeschlagen (${errorClass}): ${result.error || 'Unbekannter Fehler'}`; saveQueue(downloadQueue);
mainWindow?.webContents.send('download-progress', { emitQueueUpdated();
id: item.id, } finally {
progress: -1,
speed: '',
eta: '',
status: `Neuer Versuch in ${retryDelaySeconds}s (${errorClass})...`,
currentPart: item.currentPart,
totalParts: item.totalParts
} as DownloadProgress);
saveQueue(downloadQueue);
emitQueueUpdated();
await sleep(retryDelaySeconds * 1000);
} else {
runtimeMetrics.retriesExhausted += 1;
}
}
if (!hasQueueItemId(item.id)) {
appendDebugLog('queue-item-finished-removed', { itemId: item.id });
activeDownloads.delete(item.id); activeDownloads.delete(item.id);
cancelledItemIds.delete(item.id); cancelledItemIds.delete(item.id);
return;
} }
const wasPaused = pauseRequested || (finalResult.error || '').includes('pausiert');
item.status = finalResult.success ? 'completed' : (wasPaused ? 'paused' : 'error');
item.progress = finalResult.success ? 100 : item.progress;
item.last_error = finalResult.success || wasPaused ? '' : (finalResult.error || 'Unbekannter Fehler beim Download');
if (finalResult.success) {
runtimeMetrics.downloadsCompleted += 1;
} else if (!wasPaused) {
runtimeMetrics.downloadsFailed += 1;
}
activeDownloads.delete(item.id);
cancelledItemIds.delete(item.id);
appendDebugLog('queue-item-finished', {
itemId: item.id,
status: item.status,
error: item.last_error
});
saveQueue(downloadQueue);
emitQueueUpdated();
} }
async function processQueue(): Promise<void> { async function processQueue(): Promise<void> {

View File

@ -99,7 +99,7 @@ async function init(): Promise<void> {
// Update stats bar // Update stats bar
updateStatsBar(); updateStatsBar();
setInterval(updateStatsBar, 5000); const _statsInterval = setInterval(updateStatsBar, 5000);
if (config.client_id && config.client_secret) { if (config.client_id && config.client_secret) {
await connect(); await connect();
@ -275,9 +275,14 @@ function mergeQueueState(nextQueue: QueueItem[]): QueueItem[] {
return item; return item;
} }
// Keep the higher progress value to prevent backward jumps from stale data
const bestProgress = (prev.status === 'downloading' && prev.progress > item.progress)
? prev.progress
: (item.progress > 0 ? item.progress : prev.progress);
return { return {
...item, ...item,
progress: item.progress > 0 ? item.progress : prev.progress, progress: bestProgress,
speed: item.speed || prev.speed, speed: item.speed || prev.speed,
eta: item.eta || prev.eta, eta: item.eta || prev.eta,
currentPart: item.currentPart || prev.currentPart, currentPart: item.currentPart || prev.currentPart,