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:
parent
4607ba9cc6
commit
6c47c63fa8
156
src/main.ts
156
src/main.ts
@ -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> {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user