fix
This commit is contained in:
parent
f24eedd419
commit
b08090ebe1
2 changed files with 309 additions and 26 deletions
|
|
@ -312,22 +312,59 @@ class WebUIDownloadMonitor:
|
|||
if 'queued_start_time' not in task:
|
||||
task['queued_start_time'] = current_time
|
||||
return False
|
||||
elif current_time - task['queued_start_time'] > 90:
|
||||
print(f"⚠️ Task stuck in queue for 90+ seconds")
|
||||
return True
|
||||
else:
|
||||
queue_time = current_time - task['queued_start_time']
|
||||
if queue_time > 90:
|
||||
# Track retry attempts to prevent rapid loops
|
||||
retry_count = task.get('stuck_retry_count', 0)
|
||||
last_retry = task.get('last_retry_time', 0)
|
||||
|
||||
# Don't retry too frequently (wait at least 30 seconds between retries)
|
||||
if retry_count < 3 and (current_time - last_retry) > 30: # Max 3 retry attempts
|
||||
print(f"⚠️ Task stuck in queue for {queue_time:.1f}s (retry {retry_count + 1}/3)")
|
||||
task['stuck_retry_count'] = retry_count + 1
|
||||
task['last_retry_time'] = current_time
|
||||
return True
|
||||
elif retry_count < 3:
|
||||
# Wait longer before next retry
|
||||
return False
|
||||
else:
|
||||
# Too many retries, mark as failed
|
||||
print(f"❌ Task failed after 3 retry attempts (queue timeout)")
|
||||
task['status'] = 'failed'
|
||||
task['error_message'] = 'Failed after multiple queue timeout retries'
|
||||
return False
|
||||
|
||||
# Check for downloading at 0% timeout (90 seconds like GUI)
|
||||
elif 'InProgress' in state_str and progress < 1:
|
||||
if 'downloading_start_time' not in task:
|
||||
task['downloading_start_time'] = current_time
|
||||
return False
|
||||
elif current_time - task['downloading_start_time'] > 90:
|
||||
print(f"⚠️ Task stuck at 0% for 90+ seconds")
|
||||
return True
|
||||
else:
|
||||
download_time = current_time - task['downloading_start_time']
|
||||
if download_time > 90:
|
||||
retry_count = task.get('stuck_retry_count', 0)
|
||||
last_retry = task.get('last_retry_time', 0)
|
||||
|
||||
# Don't retry too frequently (wait at least 30 seconds between retries)
|
||||
if retry_count < 3 and (current_time - last_retry) > 30: # Max 3 retry attempts
|
||||
print(f"⚠️ Task stuck at 0% for {download_time:.1f}s (retry {retry_count + 1}/3)")
|
||||
task['stuck_retry_count'] = retry_count + 1
|
||||
task['last_retry_time'] = current_time
|
||||
return True
|
||||
elif retry_count < 3:
|
||||
# Wait longer before next retry
|
||||
return False
|
||||
else:
|
||||
print(f"❌ Task failed after 3 retry attempts (0% progress timeout)")
|
||||
task['status'] = 'failed'
|
||||
task['error_message'] = 'Failed after multiple 0% progress retries'
|
||||
return False
|
||||
else:
|
||||
# Progress being made, reset timers
|
||||
# Progress being made, reset timers and retry counts
|
||||
task.pop('queued_start_time', None)
|
||||
task.pop('downloading_start_time', None)
|
||||
task.pop('stuck_retry_count', None)
|
||||
|
||||
return False
|
||||
|
||||
|
|
@ -335,7 +372,7 @@ class WebUIDownloadMonitor:
|
|||
"""Trigger retry for a stuck/failed task"""
|
||||
try:
|
||||
# Cancel the stuck download first (like GUI)
|
||||
self._cancel_download_before_retry(task)
|
||||
self._cancel_download_before_retry(task, task_id)
|
||||
|
||||
# Update task for retry
|
||||
with tasks_lock:
|
||||
|
|
@ -358,7 +395,7 @@ class WebUIDownloadMonitor:
|
|||
except Exception as e:
|
||||
print(f"❌ Error triggering retry for task {task_id}: {e}")
|
||||
|
||||
def _cancel_download_before_retry(self, task):
|
||||
def _cancel_download_before_retry(self, task, task_id):
|
||||
"""Cancel current download before retry (matches GUI cancel_download_before_retry)"""
|
||||
try:
|
||||
download_id = task.get('download_id')
|
||||
|
|
@ -368,17 +405,20 @@ class WebUIDownloadMonitor:
|
|||
# Only attempt cancellation if we have what looks like a proper download ID
|
||||
# (not a filename fallback which would be much longer)
|
||||
if download_id and username and len(download_id) < 100:
|
||||
print(f"🚫 Attempting to cancel stuck download: {os.path.basename(download_id)} from {username}")
|
||||
print(f"🚫 Attempting to cancel stuck download: {os.path.basename(download_id)} from {username} (task: {task_id[:8]}...)")
|
||||
try:
|
||||
success = asyncio.run(soulseek_client.cancel_download(download_id, username, remove=False))
|
||||
if success:
|
||||
print(f"✅ Successfully cancelled download")
|
||||
print(f"✅ Successfully cancelled download {download_id[:8]}... from {username}")
|
||||
# Clear any stored download info to prevent status conflicts
|
||||
task.pop('soulseek_download_id', None)
|
||||
task.pop('soulseek_username', None)
|
||||
else:
|
||||
print(f"⚠️ Cancel request failed, proceeding with retry anyway")
|
||||
print(f"⚠️ Cancel request failed for {download_id[:8]}... - all API endpoints returned errors, proceeding with retry anyway")
|
||||
except Exception as e:
|
||||
print(f"⚠️ Cancel error: {str(e)[:100]}, proceeding with retry anyway")
|
||||
print(f"⚠️ Cancel exception for {download_id[:8]}...: {str(e)[:150]}, proceeding with retry anyway")
|
||||
else:
|
||||
print(f"⚠️ No valid download ID for cancellation, proceeding with retry")
|
||||
print(f"⚠️ Invalid download ID for cancellation (len={len(download_id) if download_id else 0}, username={bool(username)}), proceeding with retry")
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error in cancellation logic, proceeding with retry: {e}")
|
||||
|
|
|
|||
|
|
@ -3000,10 +3000,38 @@ function closeDownloadMissingModal(playlistId) {
|
|||
updateYouTubeCardPhase(urlHash, 'discovered');
|
||||
}
|
||||
|
||||
// Reset Tidal playlist phase to 'discovered' when modal is closed after completion
|
||||
// Enhanced Tidal playlist state management (based on GUI sync.py patterns)
|
||||
if (playlistId.startsWith('tidal_')) {
|
||||
const tidalPlaylistId = playlistId.replace('tidal_', '');
|
||||
|
||||
// Clear download-specific state but preserve discovery results (like GUI closeEvent)
|
||||
if (tidalPlaylistStates[tidalPlaylistId]) {
|
||||
// Preserve discovery data for future use (like GUI modal behavior)
|
||||
const preservedData = {
|
||||
playlist: tidalPlaylistStates[tidalPlaylistId].playlist,
|
||||
discovery_results: tidalPlaylistStates[tidalPlaylistId].discovery_results,
|
||||
spotify_matches: tidalPlaylistStates[tidalPlaylistId].spotify_matches,
|
||||
discovery_progress: tidalPlaylistStates[tidalPlaylistId].discovery_progress,
|
||||
convertedSpotifyPlaylistId: tidalPlaylistStates[tidalPlaylistId].convertedSpotifyPlaylistId
|
||||
};
|
||||
|
||||
// Clear download-specific state
|
||||
delete tidalPlaylistStates[tidalPlaylistId].download_process_id;
|
||||
delete tidalPlaylistStates[tidalPlaylistId].phase;
|
||||
|
||||
// Restore preserved data and set to discovered phase
|
||||
Object.assign(tidalPlaylistStates[tidalPlaylistId], preservedData);
|
||||
tidalPlaylistStates[tidalPlaylistId].phase = 'discovered';
|
||||
|
||||
// ALTERNATIVE: Reset to fresh state for new discovery (uncomment if user prefers this)
|
||||
// tidalPlaylistStates[tidalPlaylistId].phase = 'fresh';
|
||||
|
||||
console.log(`🧹 [Modal Close] Reset Tidal playlist ${tidalPlaylistId} - cleared download state, preserved discovery data`);
|
||||
}
|
||||
|
||||
updateTidalCardPhase(tidalPlaylistId, 'discovered');
|
||||
console.log(`🔄 [Modal Close] Reset Tidal playlist ${tidalPlaylistId} to discovered phase`);
|
||||
console.log(`📝 [Modal Close] Expected button text for discovered phase: "${getActionButtonText('discovered')}"`);
|
||||
}
|
||||
|
||||
// Clear wishlist modal state when modal is fully closed
|
||||
|
|
@ -3299,6 +3327,8 @@ function startModalDownloadPolling(playlistId) {
|
|||
if (!process || !process.batchId) return;
|
||||
if (process.poller) clearInterval(process.poller);
|
||||
|
||||
console.log(`🔄 [Polling] Starting status polling for playlistId: ${playlistId}, batchId: ${process.batchId}`);
|
||||
|
||||
process.poller = setInterval(async () => {
|
||||
if (!activeDownloadProcesses[playlistId]) {
|
||||
clearInterval(process.poller);
|
||||
|
|
@ -3306,9 +3336,15 @@ function startModalDownloadPolling(playlistId) {
|
|||
}
|
||||
try {
|
||||
const response = await fetch(`/api/playlists/${process.batchId}/download_status`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.error) throw new Error(data.error);
|
||||
|
||||
console.debug(`📊 [Polling] Status update for ${playlistId}: phase=${data.phase}, tasks=${(data.tasks || []).length}`);
|
||||
|
||||
// Auto-show wishlist modal during active auto-processing
|
||||
const isWishlist = (playlistId === 'wishlist');
|
||||
const isAutoInitiated = data.auto_initiated || false;
|
||||
|
|
@ -3335,6 +3371,8 @@ function startModalDownloadPolling(playlistId) {
|
|||
document.getElementById(`stat-missing-${playlistId}`).textContent = missingCount;
|
||||
}
|
||||
} else if (data.phase === 'downloading' || data.phase === 'complete' || data.phase === 'error') {
|
||||
console.debug(`📊 [Status Update] Processing ${data.phase} phase for playlistId: ${playlistId}, tasks: ${(data.tasks || []).length}`);
|
||||
|
||||
if (document.getElementById(`analysis-progress-fill-${playlistId}`).style.width !== '100%') {
|
||||
document.getElementById(`analysis-progress-fill-${playlistId}`).style.width = '100%';
|
||||
document.getElementById(`analysis-progress-text-${playlistId}`).textContent = 'Analysis complete!';
|
||||
|
|
@ -3350,10 +3388,20 @@ function startModalDownloadPolling(playlistId) {
|
|||
const missingCount = missingTracks.length;
|
||||
let completedCount = 0;
|
||||
let failedOrCancelledCount = 0;
|
||||
|
||||
// Verify modal exists before processing tasks
|
||||
const modal = document.getElementById(`download-missing-modal-${playlistId}`);
|
||||
if (!modal) {
|
||||
console.error(`❌ [Status Update] Modal not found: download-missing-modal-${playlistId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
(data.tasks || []).forEach(task => {
|
||||
const row = document.querySelector(`#download-missing-modal-${playlistId} tr[data-track-index="${task.track_index}"]`);
|
||||
if (!row) return;
|
||||
if (!row) {
|
||||
console.debug(`❌ [Status Update] Row not found for playlistId: ${playlistId}, track_index: ${task.track_index}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Stronger protection: Don't override locally cancelled tracks with any backend updates
|
||||
if (row.dataset.locallyCancelled === 'true') {
|
||||
|
|
@ -3374,7 +3422,12 @@ function startModalDownloadPolling(playlistId) {
|
|||
case 'cancelled': statusText = '🚫 Cancelled'; failedOrCancelledCount++; break;
|
||||
default: statusText = `⚪ ${task.status}`; break;
|
||||
}
|
||||
if(statusEl) statusEl.textContent = statusText;
|
||||
if(statusEl) {
|
||||
statusEl.textContent = statusText;
|
||||
console.debug(`✅ [Status Update] Updated track ${task.track_index} to: ${statusText}`);
|
||||
} else {
|
||||
console.warn(`❌ [Status Update] Status element not found: download-${playlistId}-${task.track_index}`);
|
||||
}
|
||||
if (actionsEl && !['completed', 'failed', 'cancelled'].includes(task.status) && actionsEl.innerHTML === '-') {
|
||||
actionsEl.innerHTML = `<button class="cancel-track-btn" title="Cancel this download" onclick="cancelTrackDownload('${playlistId}', ${task.track_index})">×</button>`;
|
||||
}
|
||||
|
|
@ -3440,8 +3493,10 @@ function startModalDownloadPolling(playlistId) {
|
|||
const tidalPlaylistId = playlistId.replace('tidal_', '');
|
||||
if (tidalPlaylistStates[tidalPlaylistId]) {
|
||||
tidalPlaylistStates[tidalPlaylistId].phase = 'download_complete';
|
||||
// Store the download process ID for potential modal rehydration
|
||||
tidalPlaylistStates[tidalPlaylistId].download_process_id = process.batchId;
|
||||
updateTidalCardPhase(tidalPlaylistId, 'download_complete');
|
||||
console.log(`✅ Updated Tidal playlist ${tidalPlaylistId} to download_complete phase`);
|
||||
console.log(`✅ [Status Complete] Updated Tidal playlist ${tidalPlaylistId} to download_complete phase`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3488,7 +3543,14 @@ function startModalDownloadPolling(playlistId) {
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Polling error for ${playlistId}:`, error);
|
||||
console.error(`❌ [Polling] Error for ${playlistId} (batch: ${process.batchId}):`, error);
|
||||
|
||||
// Don't stop polling for temporary errors, but stop if the batch is no longer valid
|
||||
if (error.message.includes('404') || error.message.includes('Batch not found')) {
|
||||
console.warn(`🛑 [Polling] Stopping polling for ${playlistId} - batch no longer exists`);
|
||||
clearInterval(process.poller);
|
||||
process.poller = null;
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
|
@ -6445,10 +6507,28 @@ function createTidalCard(playlist) {
|
|||
}
|
||||
|
||||
async function handleTidalCardClick(playlistId) {
|
||||
// Robust state validation
|
||||
const state = tidalPlaylistStates[playlistId];
|
||||
if (!state) return;
|
||||
if (!state) {
|
||||
console.error(`❌ [Card Click] No state found for Tidal playlist: ${playlistId}`);
|
||||
showToast('Playlist state not found - try refreshing the page', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`🎵 Tidal card clicked: ${playlistId}, Phase: ${state.phase}`);
|
||||
// Validate required state data
|
||||
if (!state.playlist) {
|
||||
console.error(`❌ [Card Click] No playlist data found for Tidal playlist: ${playlistId}`);
|
||||
showToast('Playlist data missing - try refreshing the page', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate phase
|
||||
if (!state.phase) {
|
||||
console.warn(`⚠️ [Card Click] No phase set for Tidal playlist ${playlistId} - defaulting to 'fresh'`);
|
||||
state.phase = 'fresh';
|
||||
}
|
||||
|
||||
console.log(`🎵 [Card Click] Tidal card clicked: ${playlistId}, Phase: ${state.phase}`);
|
||||
|
||||
if (state.phase === 'fresh') {
|
||||
// No need to fetch data - we already have all tracks from initial load (like sync.py)
|
||||
|
|
@ -6459,32 +6539,77 @@ async function handleTidalCardClick(playlistId) {
|
|||
openTidalDiscoveryModal(playlistId, state.playlist);
|
||||
|
||||
} else if (state.phase === 'discovering' || state.phase === 'discovered' || state.phase === 'syncing' || state.phase === 'sync_complete') {
|
||||
// Reopen existing modal (like sync.py)
|
||||
// Reopen existing modal with preserved discovery results (like GUI sync.py)
|
||||
console.log(`🎵 [Card Click] Opening Tidal discovery modal for ${state.phase} phase`);
|
||||
|
||||
// Validate that we have discovery results to show
|
||||
if (state.phase === 'discovered' && (!state.discovery_results || state.discovery_results.length === 0)) {
|
||||
console.warn(`⚠️ [Card Click] Discovered phase but no discovery results found - attempting to reload from backend`);
|
||||
|
||||
// Try to fetch from backend as fallback
|
||||
try {
|
||||
const stateResponse = await fetch(`/api/tidal/state/${playlistId}`);
|
||||
if (stateResponse.ok) {
|
||||
const fullState = await stateResponse.json();
|
||||
if (fullState.discovery_results) {
|
||||
// Merge backend state with current state
|
||||
state.discovery_results = fullState.discovery_results;
|
||||
state.spotify_matches = fullState.spotify_matches || state.spotify_matches;
|
||||
state.discovery_progress = fullState.discovery_progress || state.discovery_progress;
|
||||
tidalPlaylistStates[playlistId] = {...tidalPlaylistStates[playlistId], ...state};
|
||||
console.log(`✅ [Card Click] Restored ${fullState.discovery_results.length} discovery results from backend`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ [Card Click] Failed to fetch discovery results from backend: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
openTidalDiscoveryModal(playlistId, state.playlist);
|
||||
} else if (state.phase === 'downloading' || state.phase === 'download_complete') {
|
||||
// Open download modal if we have the converted playlist ID
|
||||
if (state.convertedSpotifyPlaylistId) {
|
||||
console.log(`🔍 Opening download modal for Tidal playlist: ${state.playlist.name}`);
|
||||
console.log(`🔍 [Card Click] Opening download modal for Tidal playlist: ${state.playlist.name} (phase: ${state.phase})`);
|
||||
// Check if modal already exists, if not create it
|
||||
if (activeDownloadProcesses[state.convertedSpotifyPlaylistId]) {
|
||||
const process = activeDownloadProcesses[state.convertedSpotifyPlaylistId];
|
||||
if (process.modalElement) {
|
||||
console.log(`📱 [Card Click] Showing existing download modal for ${state.phase} phase`);
|
||||
process.modalElement.style.display = 'flex';
|
||||
} else {
|
||||
console.warn(`⚠️ [Card Click] Download process exists but modal element missing - rehydrating`);
|
||||
await rehydrateTidalDownloadModal(playlistId, state);
|
||||
}
|
||||
} else {
|
||||
// Need to create the download modal - fetch the discovery results
|
||||
console.log(`🔧 [Card Click] Rehydrating Tidal download modal for ${state.phase} phase`);
|
||||
await rehydrateTidalDownloadModal(playlistId, state);
|
||||
}
|
||||
} else {
|
||||
console.error('❌ No converted Spotify playlist ID found for Tidal download modal');
|
||||
showToast('Unable to open download modal - missing playlist data', 'error');
|
||||
console.error('❌ [Card Click] No converted Spotify playlist ID found for Tidal download modal');
|
||||
console.log('📊 [Card Click] Available state data:', Object.keys(state));
|
||||
|
||||
// Fallback: try to open discovery modal if we have discovery results
|
||||
if (state.discovery_results && state.discovery_results.length > 0) {
|
||||
console.log(`🔄 [Card Click] Fallback: Opening discovery modal with ${state.discovery_results.length} results`);
|
||||
openTidalDiscoveryModal(playlistId, state.playlist);
|
||||
} else {
|
||||
showToast('Unable to open download modal - missing playlist data', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function rehydrateTidalDownloadModal(playlistId, state) {
|
||||
try {
|
||||
console.log(`💧 Rehydrating Tidal download modal for: ${state.playlist.name}`);
|
||||
// Robust state validation for rehydration
|
||||
if (!state || !state.playlist) {
|
||||
console.error(`❌ [Rehydration] Invalid state data for Tidal playlist: ${playlistId}`);
|
||||
showToast('Cannot open download modal - invalid playlist data', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`💧 [Rehydration] Rehydrating Tidal download modal for: ${state.playlist.name}`);
|
||||
|
||||
// Get discovery results from backend if not already loaded
|
||||
if (!state.discovery_results) {
|
||||
|
|
@ -6545,6 +6670,27 @@ async function rehydrateTidalDownloadModal(playlistId, state) {
|
|||
if (beginBtn) beginBtn.style.display = 'none';
|
||||
if (cancelBtn) cancelBtn.style.display = 'none';
|
||||
console.log(`✅ Showing completed Tidal download results: ${state.download_process_id}`);
|
||||
|
||||
// For completed downloads, fetch the final results once to populate the modal
|
||||
try {
|
||||
const response = await fetch(`/api/playlists/${state.download_process_id}/download_status`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.phase === 'complete' && data.tasks) {
|
||||
console.log(`📊 [Rehydration] Loading ${data.tasks.length} completed tasks for modal display`);
|
||||
// Process the completed tasks to update modal display
|
||||
updateCompletedModalResults(virtualPlaylistId, data);
|
||||
} else {
|
||||
console.warn(`⚠️ [Rehydration] Unexpected data from download_status: phase=${data.phase}, tasks=${data.tasks?.length || 0}`);
|
||||
}
|
||||
} else {
|
||||
console.error(`❌ [Rehydration] Failed to fetch download status: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ [Rehydration] Error fetching final results for completed download: ${error}`);
|
||||
// Show a user-friendly message but still allow modal to open
|
||||
showToast('Could not load download results - modal may show incomplete data', 'warning', 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6557,6 +6703,86 @@ async function rehydrateTidalDownloadModal(playlistId, state) {
|
|||
}
|
||||
}
|
||||
|
||||
function updateCompletedModalResults(playlistId, downloadData) {
|
||||
/**
|
||||
* Update a completed download modal with final results
|
||||
* This reuses the existing status polling logic but applies it once for completed state
|
||||
*/
|
||||
console.log(`📊 [Completed Results] Updating modal ${playlistId} with final download results`);
|
||||
|
||||
// Validate input data
|
||||
if (!downloadData || !downloadData.tasks) {
|
||||
console.error(`❌ [Completed Results] Invalid download data for playlist ${playlistId}:`, downloadData);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Update analysis progress to 100%
|
||||
const analysisProgressFill = document.getElementById(`analysis-progress-fill-${playlistId}`);
|
||||
const analysisProgressText = document.getElementById(`analysis-progress-text-${playlistId}`);
|
||||
if (analysisProgressFill) analysisProgressFill.style.width = '100%';
|
||||
if (analysisProgressText) analysisProgressText.textContent = 'Analysis complete!';
|
||||
|
||||
// Update analysis results and stats
|
||||
if (downloadData.analysis_results) {
|
||||
updateTrackAnalysisResults(playlistId, downloadData.analysis_results);
|
||||
const foundCount = downloadData.analysis_results.filter(r => r.found).length;
|
||||
const missingCount = downloadData.analysis_results.filter(r => !r.found).length;
|
||||
|
||||
const statFound = document.getElementById(`stat-found-${playlistId}`);
|
||||
const statMissing = document.getElementById(`stat-missing-${playlistId}`);
|
||||
if (statFound) statFound.textContent = foundCount;
|
||||
if (statMissing) statMissing.textContent = missingCount;
|
||||
}
|
||||
|
||||
// Process completed tasks to update individual track statuses
|
||||
const missingTracks = (downloadData.analysis_results || []).filter(r => !r.found);
|
||||
let completedCount = 0;
|
||||
let failedOrCancelledCount = 0;
|
||||
|
||||
(downloadData.tasks || []).forEach(task => {
|
||||
const row = document.querySelector(`#download-missing-modal-${playlistId} tr[data-track-index="${task.track_index}"]`);
|
||||
if (!row) return;
|
||||
|
||||
row.dataset.taskId = task.task_id;
|
||||
const statusEl = document.getElementById(`download-${playlistId}-${task.track_index}`);
|
||||
const actionsEl = document.getElementById(`actions-${playlistId}-${task.track_index}`);
|
||||
|
||||
let statusText = '';
|
||||
switch (task.status) {
|
||||
case 'pending': statusText = '⏸️ Pending'; break;
|
||||
case 'searching': statusText = '🔍 Searching...'; break;
|
||||
case 'downloading': statusText = `⏬ Downloading... ${Math.round(task.progress || 0)}%`; break;
|
||||
case 'completed': statusText = '✅ Completed'; completedCount++; break;
|
||||
case 'failed': statusText = '❌ Failed'; failedOrCancelledCount++; break;
|
||||
case 'cancelled': statusText = '🚫 Cancelled'; failedOrCancelledCount++; break;
|
||||
default: statusText = `⚪ ${task.status}`; break;
|
||||
}
|
||||
|
||||
if (statusEl) statusEl.textContent = statusText;
|
||||
if (actionsEl) actionsEl.innerHTML = '-'; // Remove action buttons for completed tasks
|
||||
});
|
||||
|
||||
// Update download progress to final state
|
||||
const totalFinished = completedCount + failedOrCancelledCount;
|
||||
const missingCount = missingTracks.length;
|
||||
const progressPercent = missingCount > 0 ? (totalFinished / missingCount) * 100 : 100;
|
||||
|
||||
const downloadProgressFill = document.getElementById(`download-progress-fill-${playlistId}`);
|
||||
const downloadProgressText = document.getElementById(`download-progress-text-${playlistId}`);
|
||||
const statDownloaded = document.getElementById(`stat-downloaded-${playlistId}`);
|
||||
|
||||
if (downloadProgressFill) downloadProgressFill.style.width = `${progressPercent}%`;
|
||||
if (downloadProgressText) downloadProgressText.textContent = `${completedCount}/${missingCount} completed (${progressPercent.toFixed(0)}%)`;
|
||||
if (statDownloaded) statDownloaded.textContent = completedCount;
|
||||
|
||||
console.log(`✅ [Completed Results] Updated modal with ${completedCount} completed, ${failedOrCancelledCount} failed tasks`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ [Completed Results] Error updating completed modal results:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateTidalCardPhase(playlistId, phase) {
|
||||
const state = tidalPlaylistStates[playlistId];
|
||||
if (!state) return;
|
||||
|
|
@ -6566,13 +6792,30 @@ function updateTidalCardPhase(playlistId, phase) {
|
|||
// Re-render the card with new phase
|
||||
const card = document.getElementById(`tidal-card-${playlistId}`);
|
||||
if (card) {
|
||||
const oldButtonText = card.querySelector('.playlist-card-action-btn')?.textContent || 'unknown';
|
||||
const newCardHtml = createTidalCard(state.playlist);
|
||||
card.outerHTML = newCardHtml;
|
||||
|
||||
// Verify the card was actually updated
|
||||
const updatedCard = document.getElementById(`tidal-card-${playlistId}`);
|
||||
const newButtonText = updatedCard?.querySelector('.playlist-card-action-btn')?.textContent || 'unknown';
|
||||
|
||||
console.log(`🔄 [Card Update] Re-rendered Tidal card ${playlistId}:`);
|
||||
console.log(` 📊 Phase: ${phase}`);
|
||||
console.log(` 🔘 Button text: "${oldButtonText}" → "${newButtonText}"`);
|
||||
console.log(` ✅ Expected: "${getActionButtonText(phase)}"`);
|
||||
|
||||
if (newButtonText !== getActionButtonText(phase)) {
|
||||
console.error(`❌ [Card Update] Button text mismatch! Expected "${getActionButtonText(phase)}", got "${newButtonText}"`);
|
||||
}
|
||||
|
||||
// Re-attach click handler
|
||||
const newCard = document.getElementById(`tidal-card-${playlistId}`);
|
||||
if (newCard) {
|
||||
newCard.addEventListener('click', () => handleTidalCardClick(playlistId));
|
||||
console.debug(`🔗 [Card Update] Reattached click handler for Tidal card: ${playlistId}`);
|
||||
} else {
|
||||
console.error(`❌ [Card Update] Failed to find new card after rendering: tidal-card-${playlistId}`);
|
||||
}
|
||||
|
||||
// If we have sync progress and we're in sync/sync_complete phase, restore it
|
||||
|
|
|
|||
Loading…
Reference in a new issue