diff --git a/static/js/queue.js b/static/js/queue.js index 6a5d17b..6f1288e 100644 --- a/static/js/queue.js +++ b/static/js/queue.js @@ -67,7 +67,7 @@ class DownloadQueue { if (!entry || entry.hasEnded) return; entry.intervalId = setInterval(async () => { - // Note: use the current prgFile value stored in the entry to build the log element id. + // Use the current prgFile value stored in the entry to build the log element id. const logElement = document.getElementById(`log-${entry.uniqueId}-${entry.prgFile}`); if (entry.hasEnded) { clearInterval(entry.intervalId); @@ -140,7 +140,9 @@ class DownloadQueue { lastUpdated: Date.now(), hasEnded: false, intervalId: null, - uniqueId: queueId + uniqueId: queueId, + retryCount: 0, // <== Initialize retry counter + autoRetryInterval: null // <== To store the countdown interval ID for auto retry }; } @@ -208,6 +210,9 @@ class DownloadQueue { const entry = this.downloadQueue[queueId]; if (entry) { clearInterval(entry.intervalId); + if (entry.autoRetryInterval) { + clearInterval(entry.autoRetryInterval); + } entry.element.remove(); delete this.downloadQueue[queueId]; fetch(`/api/prgs/delete/${encodeURIComponent(entry.prgFile)}`, { method: 'DELETE' }) @@ -297,7 +302,7 @@ class DownloadQueue { return `Finished ${data.type}`; case 'retrying': - return `Track "${data.song}" by ${data.artist}" failed, retrying (${data.retry_count}/10) in ${data.seconds_left}s`; + return `Track "${data.song}" by ${data.artist}" failed, retrying (${data.retry_count}/3) in ${data.seconds_left}s`; case 'error': return `Error: ${data.message || 'Unknown error'}`; @@ -321,7 +326,7 @@ class DownloadQueue { } } - /* New Methods to Handle Terminal State and Inactivity */ + /* New Methods to Handle Terminal State, Inactivity and Auto-Retry */ handleTerminalState(entry, queueId, progress) { // Mark the entry as ended and clear its monitoring interval. @@ -330,7 +335,6 @@ class DownloadQueue { const logElement = document.getElementById(`log-${entry.uniqueId}-${entry.prgFile}`); if (!logElement) return; - // If the terminal state is an error, hide the cancel button and add error buttons. if (progress.status === 'error') { // Hide the cancel button. const cancelBtn = entry.element.querySelector('.cancel-btn'); @@ -338,6 +342,7 @@ class DownloadQueue { cancelBtn.style.display = 'none'; } + // Display error message with retry buttons. logElement.innerHTML = `
`; - + // Close (X) button: immediately remove the queue entry. logElement.querySelector('.close-error-btn').addEventListener('click', () => { + // If an auto-retry countdown is running, clear it. + if (entry.autoRetryInterval) { + clearInterval(entry.autoRetryInterval); + entry.autoRetryInterval = null; + } this.cleanupEntry(queueId); }); - - // Retry button: re-send the original API request. + + // Manual Retry button: cancel the auto-retry timer (if running) and retry immediately. logElement.querySelector('.retry-btn').addEventListener('click', async () => { - logElement.textContent = 'Retrying download...'; - if (!entry.requestUrl) { - logElement.textContent = 'Retry not available: missing original request information.'; - return; - } - try { - const retryResponse = await fetch(entry.requestUrl); - const retryData = await retryResponse.json(); - if (retryData.prg_file) { - // Delete the failed prg file before updating to the new one. - const oldPrgFile = entry.prgFile; - await fetch(`/api/prgs/delete/${encodeURIComponent(oldPrgFile)}`, { method: 'DELETE' }); - - // Update the log element's id to reflect the new prg_file. - const logEl = entry.element.querySelector('.log'); - logEl.id = `log-${entry.uniqueId}-${retryData.prg_file}`; - - // Update the entry with the new prg_file and reset its state. - entry.prgFile = retryData.prg_file; - entry.lastStatus = null; - entry.hasEnded = false; - entry.lastUpdated = Date.now(); - logEl.textContent = 'Retry initiated...'; - - // Restart monitoring using the new prg_file. - this.startEntryMonitoring(queueId); - } else { - logElement.textContent = 'Retry failed: invalid response from server'; - } - } catch (error) { - logElement.textContent = 'Retry failed: ' + error.message; + if (entry.autoRetryInterval) { + clearInterval(entry.autoRetryInterval); + entry.autoRetryInterval = null; } + this.retryDownload(queueId, logElement); }); + + // --- Auto-Retry Logic --- + // Only auto-retry if we have a requestUrl. + if (entry.requestUrl) { + const maxRetries = 10; + if (entry.retryCount < maxRetries) { + const autoRetryDelay = 300; // seconds (5 minutes) + let secondsLeft = autoRetryDelay; + + // Start a countdown that updates the error message every second. + entry.autoRetryInterval = setInterval(() => { + secondsLeft--; + const errorMsgEl = logElement.querySelector('.error-message'); + if (errorMsgEl) { + errorMsgEl.textContent = `Error: ${progress.message || 'Unknown error'}. Retrying in ${secondsLeft} seconds... (attempt ${entry.retryCount + 1}/${maxRetries})`; + } + if (secondsLeft <= 0) { + clearInterval(entry.autoRetryInterval); + entry.autoRetryInterval = null; + this.retryDownload(queueId, logElement); + } + }, 1000); + } + } // Do not automatically clean up if an error occurred. return; } else { @@ -396,17 +403,59 @@ class DownloadQueue { } handleInactivity(entry, queueId, logElement) { - // If no update in 10 seconds, treat as an error. + // If no update in 5 minutes (300,000ms), treat as an error. const now = Date.now(); if (now - entry.lastUpdated > 300000) { const progress = { status: 'error', message: 'Inactivity timeout' }; this.handleTerminalState(entry, queueId, progress); } else { if (logElement) { - logElement.textContent = this.getStatusMessage(entry.lastStatus) + logElement.textContent = this.getStatusMessage(entry.lastStatus); } } } + + /** + * retryDownload() handles both manual and automatic retries. + */ + async retryDownload(queueId, logElement) { + const entry = this.downloadQueue[queueId]; + if (!entry) return; + + logElement.textContent = 'Retrying download...'; + if (!entry.requestUrl) { + logElement.textContent = 'Retry not available: missing original request information.'; + return; + } + try { + const retryResponse = await fetch(entry.requestUrl); + const retryData = await retryResponse.json(); + if (retryData.prg_file) { + // Delete the failed prg file before updating to the new one. + const oldPrgFile = entry.prgFile; + await fetch(`/api/prgs/delete/${encodeURIComponent(oldPrgFile)}`, { method: 'DELETE' }); + + // Update the log element's id to reflect the new prg_file. + const logEl = entry.element.querySelector('.log'); + logEl.id = `log-${entry.uniqueId}-${retryData.prg_file}`; + + // Update the entry with the new prg_file and reset its state. + entry.prgFile = retryData.prg_file; + entry.lastStatus = null; + entry.hasEnded = false; + entry.lastUpdated = Date.now(); + entry.retryCount = (entry.retryCount || 0) + 1; + logEl.textContent = 'Retry initiated...'; + + // Restart monitoring using the new prg_file. + this.startEntryMonitoring(queueId); + } else { + logElement.textContent = 'Retry failed: invalid response from server'; + } + } catch (error) { + logElement.textContent = 'Retry failed: ' + error.message; + } + } } // Singleton instance