uuhm
This commit is contained in:
@@ -43,12 +43,88 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Queue subtitle with statistics */
|
||||
.queue-subtitle {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 5px;
|
||||
font-size: 0.8rem;
|
||||
color: #b3b3b3;
|
||||
}
|
||||
|
||||
.queue-stat {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.queue-stat-active {
|
||||
color: #4a90e2;
|
||||
background-color: rgba(74, 144, 226, 0.1);
|
||||
}
|
||||
|
||||
.queue-stat-completed {
|
||||
color: #1DB954;
|
||||
background-color: rgba(29, 185, 84, 0.1);
|
||||
}
|
||||
|
||||
.queue-stat-error {
|
||||
color: #ff5555;
|
||||
background-color: rgba(255, 85, 85, 0.1);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Refresh queue button */
|
||||
#refreshQueueBtn {
|
||||
background: #2a2a2a;
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s ease, transform 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#refreshQueueBtn:hover {
|
||||
background: #333;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
#refreshQueueBtn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
#refreshQueueBtn.refreshing {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Artist queue message */
|
||||
.queue-artist-message {
|
||||
background: #2a2a2a;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-left: 4px solid #4a90e2;
|
||||
animation: pulse 1.5s infinite;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { opacity: 0.8; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
/* Cancel all button styling */
|
||||
#cancelAllBtn {
|
||||
background: #8b0000; /* Dark blood red */
|
||||
|
||||
@@ -20,12 +20,14 @@ class DownloadQueue {
|
||||
this.MAX_RETRIES = 3; // Default max retries
|
||||
this.RETRY_DELAY = 5; // Default retry delay in seconds
|
||||
this.RETRY_DELAY_INCREASE = 5; // Default retry delay increase in seconds
|
||||
this.MAX_SSE_CONNECTIONS = 5; // Maximum number of active SSE connections
|
||||
|
||||
this.downloadQueue = {}; // keyed by unique queueId
|
||||
this.currentConfig = {}; // Cache for current config
|
||||
|
||||
// EventSource connections for SSE tracking
|
||||
this.sseConnections = {}; // keyed by prgFile/task_id
|
||||
this.pendingForSSE = []; // Queue of entries waiting for SSE connections
|
||||
|
||||
// Load the saved visible count (or default to 10)
|
||||
const storedVisibleCount = localStorage.getItem("downloadQueueVisibleCount");
|
||||
@@ -34,6 +36,9 @@ class DownloadQueue {
|
||||
// Load the cached status info (object keyed by prgFile)
|
||||
this.queueCache = JSON.parse(localStorage.getItem("downloadQueueCache") || "{}");
|
||||
|
||||
// Add a throttled update method to reduce UI updates
|
||||
this.throttledUpdateQueue = this.throttle(this.updateQueueOrder.bind(this), 500);
|
||||
|
||||
// Wait for initDOM to complete before setting up event listeners and loading existing PRG files.
|
||||
this.initDOM().then(() => {
|
||||
this.initEventListeners();
|
||||
@@ -41,6 +46,25 @@ class DownloadQueue {
|
||||
});
|
||||
}
|
||||
|
||||
/* Utility method to throttle frequent function calls */
|
||||
throttle(func, delay) {
|
||||
let lastCall = 0;
|
||||
let timeout;
|
||||
return function(...args) {
|
||||
const now = Date.now();
|
||||
if (now - lastCall < delay) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
lastCall = now;
|
||||
func(...args);
|
||||
}, delay);
|
||||
} else {
|
||||
lastCall = now;
|
||||
func(...args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/* DOM Management */
|
||||
async initDOM() {
|
||||
// New HTML structure for the download queue.
|
||||
@@ -53,6 +77,14 @@ class DownloadQueue {
|
||||
<img src="https://www.svgrepo.com/show/488384/skull-head.svg" alt="Skull" class="skull-icon">
|
||||
Cancel all
|
||||
</button>
|
||||
<button id="refreshQueueBtn" aria-label="Refresh queue" title="Refresh queue">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.91 15.51H15.38V20.04" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.09 8.49H8.62V3.96" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8.62 8.49C8.62 8.49 5.19 12.57 4.09 15.51C2.99 18.45 4.09 20.04 4.09 20.04" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15.38 15.51C15.38 15.51 18.81 11.43 19.91 8.49C21.01 5.55 19.91 3.96 19.91 3.96" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="queueItems" aria-live="polite"></div>
|
||||
@@ -129,6 +161,24 @@ class DownloadQueue {
|
||||
});
|
||||
}
|
||||
|
||||
// "Refresh queue" button
|
||||
const refreshQueueBtn = document.getElementById('refreshQueueBtn');
|
||||
if (refreshQueueBtn) {
|
||||
refreshQueueBtn.addEventListener('click', async () => {
|
||||
try {
|
||||
refreshQueueBtn.disabled = true;
|
||||
refreshQueueBtn.classList.add('refreshing');
|
||||
await this.loadExistingPrgFiles();
|
||||
console.log('Queue refreshed');
|
||||
} catch (error) {
|
||||
console.error('Error refreshing queue:', error);
|
||||
} finally {
|
||||
refreshQueueBtn.disabled = false;
|
||||
refreshQueueBtn.classList.remove('refreshing');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close all SSE connections when the page is about to unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
this.closeAllSSEConnections();
|
||||
@@ -509,6 +559,8 @@ class DownloadQueue {
|
||||
updateQueueOrder() {
|
||||
const container = document.getElementById('queueItems');
|
||||
const footer = document.getElementById('queueFooter');
|
||||
if (!container || !footer) return;
|
||||
|
||||
const entries = Object.values(this.downloadQueue);
|
||||
|
||||
// Sorting: errors/canceled first (group 0), ongoing next (group 1), queued last (group 2, sorted by position).
|
||||
@@ -536,58 +588,90 @@ class DownloadQueue {
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('queueTotalCount').textContent = entries.length;
|
||||
// Calculate statistics to display in the header
|
||||
const totalEntries = entries.length;
|
||||
const completedEntries = entries.filter(e => e.hasEnded && e.lastStatus && e.lastStatus.status === 'complete').length;
|
||||
const errorEntries = entries.filter(e => e.hasEnded && e.lastStatus && e.lastStatus.status === 'error').length;
|
||||
const activeEntries = entries.filter(e => !e.hasEnded).length;
|
||||
|
||||
// Only recreate the container content if really needed
|
||||
const visibleEntries = entries.slice(0, this.visibleCount);
|
||||
// Update the header with detailed count
|
||||
const countEl = document.getElementById('queueTotalCount');
|
||||
if (countEl) {
|
||||
countEl.textContent = totalEntries;
|
||||
}
|
||||
|
||||
// Handle empty state
|
||||
if (entries.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="queue-empty">
|
||||
<img src="/static/images/queue-empty.svg" alt="Empty queue" onerror="this.src='/static/images/queue.svg'">
|
||||
<p>Your download queue is empty</p>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// Get currently visible items
|
||||
const visibleItems = Array.from(container.children).filter(el => el.classList.contains('queue-item'));
|
||||
// Update subtitle with detailed stats if we have entries
|
||||
if (totalEntries > 0) {
|
||||
let statsHtml = '';
|
||||
if (activeEntries > 0) {
|
||||
statsHtml += `<span class="queue-stat queue-stat-active">${activeEntries} active</span>`;
|
||||
}
|
||||
if (completedEntries > 0) {
|
||||
statsHtml += `<span class="queue-stat queue-stat-completed">${completedEntries} completed</span>`;
|
||||
}
|
||||
if (errorEntries > 0) {
|
||||
statsHtml += `<span class="queue-stat queue-stat-error">${errorEntries} failed</span>`;
|
||||
}
|
||||
|
||||
// Update container more efficiently
|
||||
if (visibleItems.length === 0) {
|
||||
// No items in container, append all visible entries
|
||||
container.innerHTML = ''; // Clear any empty state
|
||||
visibleEntries.forEach(entry => {
|
||||
// We no longer automatically start monitoring here
|
||||
// Monitoring is now explicitly started by the methods that create downloads
|
||||
container.appendChild(entry.element);
|
||||
});
|
||||
} else {
|
||||
// Container already has items, update more efficiently
|
||||
|
||||
// Create a map of current DOM elements by queue ID
|
||||
const existingElementMap = {};
|
||||
visibleItems.forEach(el => {
|
||||
const queueId = el.querySelector('.cancel-btn')?.dataset.queueid;
|
||||
if (queueId) existingElementMap[queueId] = el;
|
||||
});
|
||||
|
||||
// Clear container to re-add in correct order
|
||||
container.innerHTML = '';
|
||||
|
||||
// Add visible entries in correct order
|
||||
visibleEntries.forEach(entry => {
|
||||
// We no longer automatically start monitoring here
|
||||
container.appendChild(entry.element);
|
||||
|
||||
// Mark the entry as not new anymore
|
||||
entry.isNew = false;
|
||||
});
|
||||
// Only add the subtitle if we have stats to show
|
||||
if (statsHtml) {
|
||||
const subtitleEl = document.getElementById('queueSubtitle');
|
||||
if (subtitleEl) {
|
||||
subtitleEl.innerHTML = statsHtml;
|
||||
} else {
|
||||
// Create the subtitle if it doesn't exist
|
||||
const headerEl = document.querySelector('.sidebar-header h2');
|
||||
if (headerEl) {
|
||||
headerEl.insertAdjacentHTML('afterend', `<div id="queueSubtitle" class="queue-subtitle">${statsHtml}</div>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Remove subtitle if no entries
|
||||
const subtitleEl = document.getElementById('queueSubtitle');
|
||||
if (subtitleEl) {
|
||||
subtitleEl.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// We no longer start or stop monitoring based on visibility changes here
|
||||
// This allows the explicit monitoring control from the download methods
|
||||
// Use DocumentFragment for better performance when updating the DOM
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
// Handle empty state
|
||||
if (entries.length === 0) {
|
||||
const emptyDiv = document.createElement('div');
|
||||
emptyDiv.className = 'queue-empty';
|
||||
emptyDiv.innerHTML = `
|
||||
<img src="/static/images/queue-empty.svg" alt="Empty queue" onerror="this.src='/static/images/queue.svg'">
|
||||
<p>Your download queue is empty</p>
|
||||
`;
|
||||
container.innerHTML = '';
|
||||
container.appendChild(emptyDiv);
|
||||
} else {
|
||||
// Get the visible entries slice
|
||||
const visibleEntries = entries.slice(0, this.visibleCount);
|
||||
|
||||
// Create a map of current DOM elements by queue ID
|
||||
const existingElements = container.querySelectorAll('.queue-item');
|
||||
const existingElementMap = {};
|
||||
Array.from(existingElements).forEach(el => {
|
||||
const cancelBtn = el.querySelector('.cancel-btn');
|
||||
if (cancelBtn) {
|
||||
const queueId = cancelBtn.dataset.queueid;
|
||||
if (queueId) existingElementMap[queueId] = el;
|
||||
}
|
||||
});
|
||||
|
||||
// Add visible entries to the fragment in the correct order
|
||||
visibleEntries.forEach(entry => {
|
||||
fragment.appendChild(entry.element);
|
||||
entry.isNew = false;
|
||||
});
|
||||
|
||||
// Clear container and append the fragment
|
||||
container.innerHTML = '';
|
||||
container.appendChild(fragment);
|
||||
}
|
||||
|
||||
// Update footer
|
||||
footer.innerHTML = '';
|
||||
@@ -951,8 +1035,14 @@ class DownloadQueue {
|
||||
// Close any existing SSE connection
|
||||
this.closeSSEConnection(queueId);
|
||||
|
||||
// For album tasks created from artist downloads, we need to ensure
|
||||
// we're using the album URL, not the original artist URL
|
||||
let retryUrl = entry.requestUrl;
|
||||
|
||||
console.log(`Retrying download for ${entry.type} with URL: ${retryUrl}`);
|
||||
|
||||
// Use the stored original request URL to create a new download
|
||||
const retryResponse = await fetch(entry.requestUrl);
|
||||
const retryResponse = await fetch(retryUrl);
|
||||
if (!retryResponse.ok) {
|
||||
throw new Error(`Server returned ${retryResponse.status}`);
|
||||
}
|
||||
@@ -1052,29 +1142,63 @@ class DownloadQueue {
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Handle artist downloads which return multiple album_prg_files
|
||||
if (type === 'artist' && data.album_prg_files && Array.isArray(data.album_prg_files)) {
|
||||
// Add each album to the download queue separately
|
||||
const queueIds = [];
|
||||
data.album_prg_files.forEach(prgFile => {
|
||||
const queueId = this.addDownload(item, 'album', prgFile, apiUrl, false);
|
||||
queueIds.push({queueId, prgFile});
|
||||
});
|
||||
|
||||
// Wait a short time before setting up SSE connections
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Set up SSE connections for each entry
|
||||
for (const {queueId, prgFile} of queueIds) {
|
||||
const entry = this.downloadQueue[queueId];
|
||||
if (entry && !entry.hasEnded) {
|
||||
this.setupSSEConnection(queueId);
|
||||
// Handle artist downloads which return multiple album tasks
|
||||
if (type === 'artist') {
|
||||
// Check for new API response format
|
||||
if (data.task_ids && Array.isArray(data.task_ids)) {
|
||||
// For artist discographies, we get individual task IDs for each album
|
||||
console.log(`Queued artist discography with ${data.task_ids.length} albums`);
|
||||
|
||||
// Make queue visible to show progress
|
||||
this.toggleVisibility(true);
|
||||
|
||||
// Show a temporary message about the artist download
|
||||
const artistMessage = document.createElement('div');
|
||||
artistMessage.className = 'queue-artist-message';
|
||||
artistMessage.textContent = `Queued ${data.task_ids.length} albums for ${item.name || 'artist'}. Loading...`;
|
||||
document.getElementById('queueItems').prepend(artistMessage);
|
||||
|
||||
// Wait a moment to ensure backend has processed the tasks
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
// Remove the temporary message
|
||||
artistMessage.remove();
|
||||
|
||||
// Fetch the latest tasks to show all newly created album downloads
|
||||
await this.loadExistingPrgFiles();
|
||||
|
||||
return data.task_ids;
|
||||
}
|
||||
// Check for older API response format
|
||||
else if (data.album_prg_files && Array.isArray(data.album_prg_files)) {
|
||||
console.log(`Queued artist discography with ${data.album_prg_files.length} albums (old format)`);
|
||||
// Add each album to the download queue separately
|
||||
const queueIds = [];
|
||||
data.album_prg_files.forEach(prgFile => {
|
||||
const queueId = this.addDownload(item, 'album', prgFile, apiUrl, false);
|
||||
queueIds.push({queueId, prgFile});
|
||||
});
|
||||
|
||||
// Make queue visible to show progress
|
||||
this.toggleVisibility(true);
|
||||
|
||||
// Wait a short time before setting up SSE connections
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Set up SSE connections for each entry
|
||||
for (const {queueId, prgFile} of queueIds) {
|
||||
const entry = this.downloadQueue[queueId];
|
||||
if (entry && !entry.hasEnded) {
|
||||
this.setupSSEConnection(queueId);
|
||||
}
|
||||
}
|
||||
|
||||
return queueIds.map(({queueId}) => queueId);
|
||||
}
|
||||
|
||||
return queueIds.map(({queueId}) => queueId);
|
||||
} else if (data.prg_file) {
|
||||
// Handle single-file downloads (tracks, albums, playlists)
|
||||
}
|
||||
|
||||
// Handle single-file downloads (tracks, albums, playlists)
|
||||
if (data.prg_file) {
|
||||
const queueId = this.addDownload(item, type, data.prg_file, apiUrl, false);
|
||||
|
||||
// Wait a short time before setting up SSE connection
|
||||
@@ -1101,6 +1225,16 @@ class DownloadQueue {
|
||||
*/
|
||||
async loadExistingPrgFiles() {
|
||||
try {
|
||||
// Clear existing queue entries first to avoid duplicates when refreshing
|
||||
for (const queueId in this.downloadQueue) {
|
||||
const entry = this.downloadQueue[queueId];
|
||||
// Close any active connections
|
||||
this.closeSSEConnection(queueId);
|
||||
|
||||
// Don't remove the entry from DOM - we'll rebuild it entirely
|
||||
delete this.downloadQueue[queueId];
|
||||
}
|
||||
|
||||
const response = await fetch('/api/prgs/list');
|
||||
const prgFiles = await response.json();
|
||||
|
||||
@@ -1284,6 +1418,17 @@ class DownloadQueue {
|
||||
// Close any existing connection
|
||||
this.closeSSEConnection(queueId);
|
||||
|
||||
// Check if we're at the connection limit
|
||||
const activeConnectionCount = Object.keys(this.sseConnections).length;
|
||||
if (activeConnectionCount >= this.MAX_SSE_CONNECTIONS) {
|
||||
// Add to pending queue instead of creating connection now
|
||||
if (!this.pendingForSSE.includes(queueId)) {
|
||||
this.pendingForSSE.push(queueId);
|
||||
console.log(`Queued SSE connection for ${queueId} (max connections reached)`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new EventSource connection
|
||||
try {
|
||||
const sse = new EventSource(`/api/prgs/stream/${entry.prgFile}`);
|
||||
@@ -1321,96 +1466,44 @@ class DownloadQueue {
|
||||
entry.status = data.status;
|
||||
});
|
||||
|
||||
sse.addEventListener('update', (event) => {
|
||||
// Combined handler for all update-style events
|
||||
const updateHandler = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('SSE update event:', data);
|
||||
this.handleSSEUpdate(queueId, data);
|
||||
});
|
||||
|
||||
sse.addEventListener('progress', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('SSE progress event:', data);
|
||||
this.handleSSEUpdate(queueId, data);
|
||||
});
|
||||
|
||||
// Add specific handler for track_complete events
|
||||
sse.addEventListener('track_complete', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('SSE track_complete event:', data);
|
||||
console.log(`Current entry type: ${entry.type}`);
|
||||
const eventType = event.type;
|
||||
|
||||
// Mark this status as a track completion
|
||||
data.status = 'track_complete';
|
||||
|
||||
// Only update the log message without changing status colors
|
||||
const logElement = document.getElementById(`log-${entry.uniqueId}-${entry.prgFile}`);
|
||||
if (logElement) {
|
||||
let message = `Completed track: ${data.title || data.track || 'Unknown'}`;
|
||||
if (data.artist) message += ` by ${data.artist}`;
|
||||
logElement.textContent = message;
|
||||
}
|
||||
|
||||
// For single track downloads, track_complete is a terminal state
|
||||
if (entry.type === 'track') {
|
||||
console.log('Single track download completed - terminating');
|
||||
// Mark the track as ended
|
||||
entry.hasEnded = true;
|
||||
if (eventType === 'track_complete') {
|
||||
// Special handling for track completions
|
||||
console.log('SSE track_complete event:', data);
|
||||
|
||||
// Handle as a terminal state
|
||||
setTimeout(() => {
|
||||
this.closeSSEConnection(queueId);
|
||||
this.cleanupEntry(queueId);
|
||||
}, 5000);
|
||||
} else {
|
||||
console.log(`Album/playlist track completed - continuing download (type: ${entry.type})`);
|
||||
// For albums/playlists, just update entry data without changing status
|
||||
entry.lastStatus = data;
|
||||
entry.lastUpdated = Date.now();
|
||||
// Mark this status as a track completion
|
||||
data.status = 'track_complete';
|
||||
|
||||
// Save to cache
|
||||
this.queueCache[entry.prgFile] = data;
|
||||
localStorage.setItem("downloadQueueCache", JSON.stringify(this.queueCache));
|
||||
}
|
||||
});
|
||||
|
||||
// Also handle 'done' events which can come for individual tracks
|
||||
sse.addEventListener('done', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('SSE done event (individual track):', data);
|
||||
console.log(`Current entry type: ${entry.type}`);
|
||||
|
||||
// Only update the log message without changing status colors for album tracks
|
||||
const logElement = document.getElementById(`log-${entry.uniqueId}-${entry.prgFile}`);
|
||||
if (logElement) {
|
||||
let message = `Completed track: ${data.song || data.title || data.track || 'Unknown'}`;
|
||||
if (data.artist) message += ` by ${data.artist}`;
|
||||
logElement.textContent = message;
|
||||
}
|
||||
|
||||
// For single track downloads, done is a terminal state
|
||||
if (entry.type === 'track') {
|
||||
console.log('Single track download completed (done) - terminating');
|
||||
// Mark the track as ended
|
||||
entry.hasEnded = true;
|
||||
// Only update the log message without changing status colors
|
||||
const logElement = document.getElementById(`log-${entry.uniqueId}-${entry.prgFile}`);
|
||||
if (logElement) {
|
||||
let message = `Completed track: ${data.title || data.track || 'Unknown'}`;
|
||||
if (data.artist) message += ` by ${data.artist}`;
|
||||
logElement.textContent = message;
|
||||
}
|
||||
|
||||
// Handle as a terminal state
|
||||
setTimeout(() => {
|
||||
this.closeSSEConnection(queueId);
|
||||
this.cleanupEntry(queueId);
|
||||
}, 5000);
|
||||
} else if (data.song) {
|
||||
console.log(`Album/playlist individual track done - continuing download (type: ${entry.type})`);
|
||||
// For albums/playlists, just update entry data without changing status
|
||||
data._isIndividualTrack = true; // Mark it for special handling in update logic
|
||||
entry.lastStatus = data;
|
||||
entry.lastUpdated = Date.now();
|
||||
// For single track downloads, track_complete is a terminal state
|
||||
if (entry.type === 'track') {
|
||||
entry.hasEnded = true;
|
||||
setTimeout(() => {
|
||||
this.closeSSEConnection(queueId);
|
||||
this.cleanupEntry(queueId);
|
||||
}, 5000);
|
||||
} else {
|
||||
// For albums/playlists, just update entry data without changing status
|
||||
entry.lastStatus = data;
|
||||
entry.lastUpdated = Date.now();
|
||||
this.queueCache[entry.prgFile] = data;
|
||||
localStorage.setItem("downloadQueueCache", JSON.stringify(this.queueCache));
|
||||
}
|
||||
} else if (eventType === 'complete' || eventType === 'done') {
|
||||
// Terminal state handling
|
||||
console.log(`SSE ${eventType} event:`, data);
|
||||
|
||||
// Save to cache
|
||||
this.queueCache[entry.prgFile] = data;
|
||||
localStorage.setItem("downloadQueueCache", JSON.stringify(this.queueCache));
|
||||
} else {
|
||||
// This is a real done event for the entire album/playlist
|
||||
console.log(`Entire ${entry.type} completed - finalizing`);
|
||||
this.handleSSEUpdate(queueId, data);
|
||||
entry.hasEnded = true;
|
||||
|
||||
@@ -1418,91 +1511,38 @@ class DownloadQueue {
|
||||
this.closeSSEConnection(queueId);
|
||||
this.cleanupEntry(queueId);
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
sse.addEventListener('complete', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('SSE complete event:', data);
|
||||
console.log(`Current entry type: ${entry.type}`);
|
||||
|
||||
// Skip terminal processing for track_complete status in albums/playlists
|
||||
// Also skip for "done" status when it's for an individual track in an album/playlist
|
||||
if ((data.status === 'track_complete' && entry.type !== 'track') ||
|
||||
(data.status === 'done' && data.song && entry.type !== 'track')) {
|
||||
console.log(`Track ${data.status} in ${entry.type} download - continuing`);
|
||||
// Don't process individual track completion events here
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the status is set to 'complete' for UI purposes
|
||||
if (!data.status || data.status === '') {
|
||||
data.status = 'complete';
|
||||
}
|
||||
|
||||
// For track downloads, make sure we have a proper name
|
||||
if (entry.type === 'track' && !data.name && entry.lastStatus) {
|
||||
data.name = entry.lastStatus.name || '';
|
||||
data.artist = entry.lastStatus.artist || '';
|
||||
}
|
||||
|
||||
this.handleSSEUpdate(queueId, data);
|
||||
|
||||
// Always mark as terminal state for 'complete' events (except individual track completions in albums)
|
||||
entry.hasEnded = true;
|
||||
|
||||
// Close the connection after a short delay
|
||||
setTimeout(() => {
|
||||
} else if (eventType === 'error') {
|
||||
// Error state handling
|
||||
console.log('SSE error event:', data);
|
||||
this.handleSSEUpdate(queueId, data);
|
||||
entry.hasEnded = true;
|
||||
this.closeSSEConnection(queueId);
|
||||
this.cleanupEntry(queueId);
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
sse.addEventListener('error', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('SSE error event:', data);
|
||||
this.handleSSEUpdate(queueId, data);
|
||||
|
||||
// Mark the download as ended with error
|
||||
entry.hasEnded = true;
|
||||
|
||||
// Close the connection, but don't automatically clean up the entry
|
||||
// to allow for potential retry
|
||||
this.closeSSEConnection(queueId);
|
||||
});
|
||||
|
||||
sse.addEventListener('end', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('SSE end event:', data);
|
||||
|
||||
// For track downloads, ensure we have the proper fields for UI display
|
||||
if (entry.type === 'track') {
|
||||
// If the end event doesn't have a name/artist, copy from lastStatus
|
||||
if ((!data.name || !data.artist) && entry.lastStatus) {
|
||||
data.name = data.name || entry.lastStatus.name || '';
|
||||
data.artist = data.artist || entry.lastStatus.artist || '';
|
||||
}
|
||||
} else if (eventType === 'end') {
|
||||
// End event handling
|
||||
console.log('SSE end event:', data);
|
||||
|
||||
// Force status to 'complete' if not provided
|
||||
if (!data.status || data.status === '') {
|
||||
data.status = 'complete';
|
||||
// Update with final status
|
||||
this.handleSSEUpdate(queueId, data);
|
||||
entry.hasEnded = true;
|
||||
this.closeSSEConnection(queueId);
|
||||
|
||||
if (data.status === 'complete' || data.status === 'done') {
|
||||
setTimeout(() => this.cleanupEntry(queueId), 5000);
|
||||
}
|
||||
} else {
|
||||
// Standard update handling
|
||||
this.handleSSEUpdate(queueId, data);
|
||||
}
|
||||
|
||||
// Update with final status
|
||||
this.handleSSEUpdate(queueId, data);
|
||||
|
||||
// Mark the download as ended
|
||||
entry.hasEnded = true;
|
||||
|
||||
// Close the connection
|
||||
this.closeSSEConnection(queueId);
|
||||
|
||||
// Clean up the entry after a delay if it's a success
|
||||
if (data.status === 'complete' || data.status === 'done') {
|
||||
setTimeout(() => this.cleanupEntry(queueId), 5000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Set up shared handler for all events
|
||||
sse.addEventListener('update', updateHandler);
|
||||
sse.addEventListener('progress', updateHandler);
|
||||
sse.addEventListener('track_complete', updateHandler);
|
||||
sse.addEventListener('complete', updateHandler);
|
||||
sse.addEventListener('done', updateHandler);
|
||||
sse.addEventListener('error', updateHandler);
|
||||
sse.addEventListener('end', updateHandler);
|
||||
|
||||
// Handle connection error
|
||||
sse.onerror = (error) => {
|
||||
@@ -1537,6 +1577,13 @@ class DownloadQueue {
|
||||
console.error('Error closing SSE connection:', error);
|
||||
}
|
||||
delete this.sseConnections[queueId];
|
||||
|
||||
// Now that we've freed a slot, check if any entries are waiting for an SSE connection
|
||||
if (this.pendingForSSE.length > 0) {
|
||||
const nextQueueId = this.pendingForSSE.shift();
|
||||
console.log(`Starting SSE connection for queued entry ${nextQueueId}`);
|
||||
this.setupSSEConnection(nextQueueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1552,8 +1599,6 @@ class DownloadQueue {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`handleSSEUpdate for ${queueId} with type ${entry.type} and status ${data.status}`);
|
||||
|
||||
// Track completion is special - don't change visible status ONLY for albums/playlists
|
||||
// Check for both 'track_complete' and 'done' statuses for individual tracks in albums
|
||||
const isTrackCompletion = data.status === 'track_complete' ||
|
||||
@@ -1574,29 +1619,46 @@ class DownloadQueue {
|
||||
entry.status = data.status;
|
||||
}
|
||||
|
||||
// Update status message in the UI
|
||||
const logElement = document.getElementById(`log-${entry.uniqueId}-${entry.prgFile}`);
|
||||
if (logElement) {
|
||||
const statusMessage = this.getStatusMessage(data);
|
||||
logElement.textContent = statusMessage;
|
||||
}
|
||||
// Update status message in the UI - use a more efficient approach
|
||||
this.updateEntryStatusUI(entry, data, skipStatusChange);
|
||||
|
||||
// Apply appropriate CSS classes based on status only if not skipping status change
|
||||
if (!skipStatusChange) {
|
||||
this.applyStatusClasses(entry, data);
|
||||
}
|
||||
|
||||
// Save updated status to cache
|
||||
this.queueCache[entry.prgFile] = data;
|
||||
localStorage.setItem("downloadQueueCache", JSON.stringify(this.queueCache));
|
||||
// Save updated status to cache - debounce these writes to reduce storage operations
|
||||
clearTimeout(entry.cacheWriteTimeout);
|
||||
entry.cacheWriteTimeout = setTimeout(() => {
|
||||
this.queueCache[entry.prgFile] = data;
|
||||
localStorage.setItem("downloadQueueCache", JSON.stringify(this.queueCache));
|
||||
}, 500);
|
||||
|
||||
// Special handling for error status
|
||||
if (data.status === 'error') {
|
||||
this.handleTerminalState(entry, queueId, data);
|
||||
}
|
||||
|
||||
// Update the queue order
|
||||
this.updateQueueOrder();
|
||||
// Throttle UI updates to improve performance with multiple downloads
|
||||
this.throttledUpdateQueue();
|
||||
}
|
||||
|
||||
// Optimized method to update the entry status in the UI
|
||||
updateEntryStatusUI(entry, data, skipStatusChange) {
|
||||
// First, update the log message text if the element exists
|
||||
const logElement = document.getElementById(`log-${entry.uniqueId}-${entry.prgFile}`);
|
||||
if (logElement) {
|
||||
// Only modify the text content if it doesn't already have child elements
|
||||
// (which would be the case for error states with retry buttons)
|
||||
if (!logElement.querySelector('.error-message')) {
|
||||
const statusMessage = this.getStatusMessage(data);
|
||||
|
||||
// Only update DOM if the text has changed
|
||||
if (logElement.textContent !== statusMessage) {
|
||||
logElement.textContent = statusMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply CSS classes for status indication only if we're not skipping status changes
|
||||
if (!skipStatusChange) {
|
||||
this.applyStatusClasses(entry, data);
|
||||
}
|
||||
}
|
||||
|
||||
/* Close all active SSE connections */
|
||||
|
||||
Reference in New Issue
Block a user