updated queue and playlist fallback

This commit is contained in:
coolgitternotin
2025-03-24 12:20:16 -06:00
parent e916684092
commit 5701dd8b04
3 changed files with 138 additions and 45 deletions

View File

@@ -1041,7 +1041,11 @@ def download_playlist(self, **task_data):
custom_dir_format=custom_dir_format,
custom_track_format=custom_track_format,
pad_tracks=pad_tracks,
progress_callback=self.progress_callback
initial_retry_delay=initial_retry_delay,
retry_delay_increase=retry_delay_increase,
max_retries=max_retries,
progress_callback=self.progress_callback,
spotify_quality=fall_quality
)
return {"status": "success", "message": "Playlist download completed"}

View File

@@ -95,7 +95,8 @@ def download_playlist(
pad_tracks=pad_tracks,
initial_retry_delay=initial_retry_delay,
retry_delay_increase=retry_delay_increase,
max_retries=max_retries
max_retries=max_retries,
spotify_quality=fall_quality
)
print(f"DEBUG: Playlist download completed successfully using Deezer credentials")
except Exception as e:

View File

@@ -126,7 +126,7 @@ class DownloadQueue {
entry.hasEnded = true;
// Close SSE connection
this.closeSSEConnection(queueId);
this.clearPollingInterval(queueId);
if (entry.intervalId) {
clearInterval(entry.intervalId);
@@ -143,7 +143,7 @@ class DownloadQueue {
// Close all SSE connections when the page is about to unload
window.addEventListener('beforeunload', () => {
this.closeAllSSEConnections();
this.clearAllPollingIntervals();
});
}
@@ -222,7 +222,7 @@ class DownloadQueue {
// Only start monitoring if explicitly requested
if (startMonitoring && this.isEntryVisible(queueId)) {
this.startEntryMonitoring(queueId);
this.startDownloadStatusMonitoring(queueId);
}
this.dispatchEvent('downloadAdded', { queueId, item, type });
@@ -230,7 +230,7 @@ class DownloadQueue {
}
/* Start processing the entry only if it is visible. */
async startEntryMonitoring(queueId) {
async startDownloadStatusMonitoring(queueId) {
const entry = this.queueEntries[queueId];
if (!entry || entry.hasEnded) return;
@@ -295,7 +295,7 @@ class DownloadQueue {
// If the entry is already in a terminal state, don't set up SSE
if (['error', 'complete', 'cancel', 'cancelled', 'done'].includes(data.last_line.status)) {
entry.hasEnded = true;
this.handleTerminalState(entry, queueId, data.last_line);
this.handleDownloadCompletion(entry, queueId, data.last_line);
return;
}
}
@@ -305,7 +305,7 @@ class DownloadQueue {
}
// Set up SSE connection for real-time updates
this.setupSSEConnection(queueId);
this.setupPollingInterval(queueId);
}
/* Helper Methods */
@@ -415,7 +415,8 @@ class DownloadQueue {
const defaultMessage = (type === 'playlist') ? 'Reading track list' : 'Initializing download...';
// Use display values if available, or fall back to standard fields
const displayTitle = item.name || 'Unknown';
// Support both 'name' and 'music' fields which may be used by the backend
const displayTitle = item.name || item.music || item.song || 'Unknown';
const displayType = type.charAt(0).toUpperCase() + type.slice(1);
const div = document.createElement('article');
@@ -497,7 +498,7 @@ class DownloadQueue {
entry.hasEnded = true;
// Close any active connections
this.closeSSEConnection(queueid);
this.clearPollingInterval(queueid);
if (entry.intervalId) {
clearInterval(entry.intervalId);
@@ -668,7 +669,7 @@ class DownloadQueue {
const entry = this.queueEntries[queueId];
if (entry) {
// Close any SSE connection
this.closeSSEConnection(queueId);
this.clearPollingInterval(queueId);
// Clean up any intervals
if (entry.intervalId) {
@@ -723,12 +724,16 @@ class DownloadQueue {
function pluralize(word) {
return word.endsWith('s') ? word : word + 's';
}
// Extract the track name - check 'music' field first (from backend), then 'song', then 'name'
const trackName = data.music || data.song || data.name || 'Unknown';
switch (data.status) {
case 'queued':
if (data.type === 'album' || data.type === 'playlist') {
return `Queued ${data.type} "${data.name}"${data.position ? ` (position ${data.position})` : ''}`;
} else if (data.type === 'track') {
return `Queued track "${data.name}"${data.artist ? ` by ${data.artist}` : ''}`;
return `Queued track "${trackName}"${data.artist ? ` by ${data.artist}` : ''}`;
}
return `Queued ${data.type} "${data.name}"`;
@@ -746,7 +751,7 @@ class DownloadQueue {
case 'downloading':
if (data.type === 'track') {
return `Downloading track "${data.song}" by ${data.artist}...`;
return `Downloading track "${trackName}"${data.artist ? ` by ${data.artist}` : ''}...`;
}
return `Downloading ${data.type}...`;
@@ -792,7 +797,7 @@ class DownloadQueue {
case 'done':
if (data.type === 'track') {
return `Finished track "${data.song || data.name}" by ${data.artist}`;
return `Finished track "${trackName}"${data.artist ? ` by ${data.artist}` : ''}`;
} else if (data.type === 'playlist') {
return `Finished playlist "${data.name}" with ${data.total_tracks} tracks`;
} else if (data.type === 'album') {
@@ -804,7 +809,7 @@ class DownloadQueue {
case 'complete':
if (data.type === 'track') {
return `Finished track "${data.name || data.song}" by ${data.artist}`;
return `Finished track "${trackName}"${data.artist ? ` by ${data.artist}` : ''}`;
} else if (data.type === 'playlist') {
return `Finished playlist "${data.name}" with ${data.total_tracks || ''} tracks`;
} else if (data.type === 'album') {
@@ -832,14 +837,14 @@ class DownloadQueue {
return errorMsg;
case 'skipped':
return `Track "${data.song}" skipped, it already exists!`;
return `Track "${trackName}" skipped, it already exists!`;
case 'real_time': {
const totalMs = data.time_elapsed;
const minutes = Math.floor(totalMs / 60000);
const seconds = Math.floor((totalMs % 60000) / 1000);
const paddedSeconds = seconds < 10 ? '0' + seconds : seconds;
return `Real-time downloading track "${data.song}" by ${data.artist} (${(data.percentage * 100).toFixed(1)}%). Time elapsed: ${minutes}:${paddedSeconds}`;
return `Real-time downloading track "${trackName}"${data.artist ? ` by ${data.artist}` : ''} (${(data.percentage * 100).toFixed(1)}%). Time elapsed: ${minutes}:${paddedSeconds}`;
}
default:
@@ -848,7 +853,7 @@ class DownloadQueue {
}
/* New Methods to Handle Terminal State, Inactivity and Auto-Retry */
handleTerminalState(entry, queueId, progress) {
handleDownloadCompletion(entry, queueId, progress) {
// Mark the entry as ended
entry.hasEnded = true;
@@ -863,11 +868,10 @@ class DownloadQueue {
}
// Stop polling
this.closeSSEConnection(queueId);
this.clearPollingInterval(queueId);
// For error state, use longer timeout (30 seconds)
const isError = entry.lastStatus && entry.lastStatus.status === 'error';
const cleanupDelay = isError ? 30000 : 5000;
// Use 10 seconds cleanup delay for all states including errors
const cleanupDelay = 10000;
// Clean up after the appropriate delay
setTimeout(() => {
@@ -885,7 +889,7 @@ class DownloadQueue {
const now = Date.now();
if (now - entry.lastUpdated > 300000) {
const progress = { status: 'error', message: 'Inactivity timeout' };
this.handleTerminalState(entry, queueId, progress);
this.handleDownloadCompletion(entry, queueId, progress);
} else {
if (logElement) {
logElement.textContent = this.getStatusMessage(entry.lastStatus);
@@ -928,7 +932,7 @@ class DownloadQueue {
try {
// Close any existing SSE connection
this.closeSSEConnection(queueId);
this.clearPollingInterval(queueId);
console.log(`Retrying download for ${entry.type} with URL: ${retryUrl}`);
@@ -981,7 +985,7 @@ class DownloadQueue {
}
// Set up a new SSE connection for the retried download
this.setupSSEConnection(queueId);
this.setupPollingInterval(queueId);
} else {
logElement.textContent = 'Retry failed: invalid response from server';
}
@@ -999,7 +1003,7 @@ class DownloadQueue {
const entry = this.queueEntries[queueId];
// Only start monitoring if the entry is not in a terminal state and is visible
if (!entry.hasEnded && this.isEntryVisible(queueId) && !this.sseConnections[queueId]) {
this.setupSSEConnection(queueId);
this.setupPollingInterval(queueId);
}
}
}
@@ -1075,7 +1079,7 @@ class DownloadQueue {
const entry = this.queueEntries[queueId];
// Only start monitoring if the entry is not in a terminal state
if (!entry.hasEnded && !this.sseConnections[queueId]) {
this.setupSSEConnection(queueId);
this.setupPollingInterval(queueId);
}
}
@@ -1084,6 +1088,13 @@ class DownloadQueue {
// 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)`);
// Show a temporary message about the artist download
const artistMessage = document.createElement('div');
artistMessage.className = 'queue-artist-message';
artistMessage.textContent = `Queued ${data.album_prg_files.length} albums for ${item.name || 'artist'}. Loading...`;
document.getElementById('queueItems').prepend(artistMessage);
// Add each album to the download queue separately
const queueIds = [];
data.album_prg_files.forEach(prgFile => {
@@ -1095,18 +1106,56 @@ class DownloadQueue {
this.toggleVisibility(true);
// Wait a short time before setting up SSE connections
await new Promise(resolve => setTimeout(resolve, 1000));
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();
// Set up SSE connections for each entry
for (const {queueId, prgFile} of queueIds) {
for (const queueId in this.queueEntries) {
const entry = this.queueEntries[queueId];
if (entry && !entry.hasEnded) {
this.setupSSEConnection(queueId);
this.setupPollingInterval(queueId);
}
}
return queueIds.map(({queueId}) => queueId);
}
// Handle any other response format for artist downloads
else {
console.log(`Queued artist discography with unknown format:`, data);
// Show a temporary message
const artistMessage = document.createElement('div');
artistMessage.className = 'queue-artist-message';
artistMessage.textContent = `Queued albums for ${item.name || 'artist'}. Loading...`;
document.getElementById('queueItems').prepend(artistMessage);
// Make queue visible
this.toggleVisibility(true);
// Wait a moment for tasks to be created on the backend
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();
// Start monitoring all entries
for (const queueId in this.queueEntries) {
const entry = this.queueEntries[queueId];
if (entry && !entry.hasEnded) {
this.setupPollingInterval(queueId);
}
}
return data;
}
}
// Handle single-file downloads (tracks, albums, playlists)
@@ -1119,7 +1168,7 @@ class DownloadQueue {
// Set up SSE connection
const entry = this.queueEntries[queueId];
if (entry && !entry.hasEnded) {
this.setupSSEConnection(queueId);
this.setupPollingInterval(queueId);
}
return queueId;
@@ -1141,7 +1190,7 @@ class DownloadQueue {
for (const queueId in this.queueEntries) {
const entry = this.queueEntries[queueId];
// Close any active connections
this.closeSSEConnection(queueId);
this.clearPollingInterval(queueId);
// Don't remove the entry from DOM - we'll rebuild it entirely
delete this.queueEntries[queueId];
@@ -1323,7 +1372,7 @@ class DownloadQueue {
}
/* Sets up a Server-Sent Events connection for real-time status updates */
setupSSEConnection(queueId) {
setupPollingInterval(queueId) {
console.log(`Setting up polling for ${queueId}`);
const entry = this.queueEntries[queueId];
if (!entry || !entry.prgFile) {
@@ -1332,15 +1381,15 @@ class DownloadQueue {
}
// Close any existing connection
this.closeSSEConnection(queueId);
this.clearPollingInterval(queueId);
try {
// Immediately fetch initial data
this.fetchTaskStatus(queueId);
this.fetchDownloadStatus(queueId);
// Create a polling interval of 1 second
const intervalId = setInterval(() => {
this.fetchTaskStatus(queueId);
this.fetchDownloadStatus(queueId);
}, 1000);
// Store the interval ID for later cleanup
@@ -1355,7 +1404,7 @@ class DownloadQueue {
}
}
async fetchTaskStatus(queueId) {
async fetchDownloadStatus(queueId) {
const entry = this.queueEntries[queueId];
if (!entry || !entry.prgFile) {
console.warn(`No entry or prgFile for ${queueId}`);
@@ -1384,8 +1433,14 @@ class DownloadQueue {
}
}
// Filter the last_line if it doesn't match the entry's type
if (data.last_line && data.last_line.type && entry.type && data.last_line.type !== entry.type) {
console.log(`Skipping status update with type '${data.last_line.type}' for entry with type '${entry.type}'`);
return;
}
// Process the update
this.handleSSEUpdate(queueId, data);
this.handleStatusUpdate(queueId, data);
// Handle terminal states
if (data.last_line && ['complete', 'error', 'cancelled', 'done'].includes(data.last_line.status)) {
@@ -1393,7 +1448,7 @@ class DownloadQueue {
entry.hasEnded = true;
setTimeout(() => {
this.closeSSEConnection(queueId);
this.clearPollingInterval(queueId);
this.cleanupEntry(queueId);
}, 5000);
}
@@ -1409,7 +1464,7 @@ class DownloadQueue {
}
}
closeSSEConnection(queueId) {
clearPollingInterval(queueId) {
if (this.sseConnections[queueId]) {
console.log(`Stopping polling for ${queueId}`);
try {
@@ -1423,7 +1478,7 @@ class DownloadQueue {
}
/* Handle SSE update events */
handleSSEUpdate(queueId, data) {
handleStatusUpdate(queueId, data) {
const entry = this.queueEntries[queueId];
if (!entry) {
console.warn(`No entry for ${queueId}`);
@@ -1436,6 +1491,12 @@ class DownloadQueue {
// Extract the actual status data from the API response
const statusData = data.last_line || {};
// Skip updates where the type doesn't match the entry's type
if (statusData.type && entry.type && statusData.type !== entry.type) {
return;
}
status = statusData.status || data.event || 'unknown';
// For new polling API structure
@@ -1447,6 +1508,16 @@ class DownloadQueue {
message = `Status: ${status}`;
}
// Extract trackName from different possible fields
const trackName = statusData.music || statusData.song || statusData.name || 'Unknown';
if (trackName && trackName !== 'Unknown') {
// Update the title in the queue item if we have a track name
const titleEl = entry.element.querySelector('.title');
if (titleEl && trackName !== titleEl.textContent) {
titleEl.textContent = trackName;
}
}
// Track progress data
if (data.progress_percent) {
progress = data.progress_percent;
@@ -1538,6 +1609,13 @@ class DownloadQueue {
this.retryDownload(queueId, logElement);
});
// Set up automatic cleanup after 10 seconds
setTimeout(() => {
if (this.queueEntries[queueId] && this.queueEntries[queueId].hasEnded) {
this.cleanupEntry(queueId);
}
}, 10000);
} else {
// Cannot retry - just show error with close button
logElement.innerHTML = `
@@ -1550,6 +1628,13 @@ class DownloadQueue {
logElement.querySelector('.close-error-btn').addEventListener('click', () => {
this.cleanupEntry(queueId);
});
// Set up automatic cleanup after 10 seconds
setTimeout(() => {
if (this.queueEntries[queueId] && this.queueEntries[queueId].hasEnded) {
this.cleanupEntry(queueId);
}
}, 10000);
}
}
@@ -1558,12 +1643,15 @@ class DownloadQueue {
entry.element.classList.add('error');
// Close SSE connection
this.closeSSEConnection(queueId);
this.clearPollingInterval(queueId);
} else {
// For non-error states, update the log element with the latest message
const logElement = document.getElementById(`log-${entry.uniqueId}-${entry.prgFile}`);
if (logElement && message) {
logElement.textContent = message;
} else if (logElement) {
// Generate a message if none provided
logElement.textContent = this.getStatusMessage(statusData);
}
// Set the proper status classes on the list item
@@ -1597,14 +1685,14 @@ class DownloadQueue {
// Handle terminal states (except errors which we handle separately above)
if (['complete', 'cancelled', 'done'].includes(status)) {
this.handleTerminalState(entry, queueId, progress);
this.handleDownloadCompletion(entry, queueId, progress);
}
}
/* Close all active SSE connections */
closeAllSSEConnections() {
clearAllPollingIntervals() {
for (const queueId in this.sseConnections) {
this.closeSSEConnection(queueId);
this.clearPollingInterval(queueId);
}
}
}