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:
|
if 'queued_start_time' not in task:
|
||||||
task['queued_start_time'] = current_time
|
task['queued_start_time'] = current_time
|
||||||
return False
|
return False
|
||||||
elif current_time - task['queued_start_time'] > 90:
|
else:
|
||||||
print(f"⚠️ Task stuck in queue for 90+ seconds")
|
queue_time = current_time - task['queued_start_time']
|
||||||
return True
|
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)
|
# Check for downloading at 0% timeout (90 seconds like GUI)
|
||||||
elif 'InProgress' in state_str and progress < 1:
|
elif 'InProgress' in state_str and progress < 1:
|
||||||
if 'downloading_start_time' not in task:
|
if 'downloading_start_time' not in task:
|
||||||
task['downloading_start_time'] = current_time
|
task['downloading_start_time'] = current_time
|
||||||
return False
|
return False
|
||||||
elif current_time - task['downloading_start_time'] > 90:
|
else:
|
||||||
print(f"⚠️ Task stuck at 0% for 90+ seconds")
|
download_time = current_time - task['downloading_start_time']
|
||||||
return True
|
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:
|
else:
|
||||||
# Progress being made, reset timers
|
# Progress being made, reset timers and retry counts
|
||||||
task.pop('queued_start_time', None)
|
task.pop('queued_start_time', None)
|
||||||
task.pop('downloading_start_time', None)
|
task.pop('downloading_start_time', None)
|
||||||
|
task.pop('stuck_retry_count', None)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -335,7 +372,7 @@ class WebUIDownloadMonitor:
|
||||||
"""Trigger retry for a stuck/failed task"""
|
"""Trigger retry for a stuck/failed task"""
|
||||||
try:
|
try:
|
||||||
# Cancel the stuck download first (like GUI)
|
# 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
|
# Update task for retry
|
||||||
with tasks_lock:
|
with tasks_lock:
|
||||||
|
|
@ -358,7 +395,7 @@ class WebUIDownloadMonitor:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ Error triggering retry for task {task_id}: {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)"""
|
"""Cancel current download before retry (matches GUI cancel_download_before_retry)"""
|
||||||
try:
|
try:
|
||||||
download_id = task.get('download_id')
|
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
|
# Only attempt cancellation if we have what looks like a proper download ID
|
||||||
# (not a filename fallback which would be much longer)
|
# (not a filename fallback which would be much longer)
|
||||||
if download_id and username and len(download_id) < 100:
|
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:
|
try:
|
||||||
success = asyncio.run(soulseek_client.cancel_download(download_id, username, remove=False))
|
success = asyncio.run(soulseek_client.cancel_download(download_id, username, remove=False))
|
||||||
if success:
|
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:
|
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:
|
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:
|
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:
|
except Exception as e:
|
||||||
print(f"⚠️ Error in cancellation logic, proceeding with retry: {e}")
|
print(f"⚠️ Error in cancellation logic, proceeding with retry: {e}")
|
||||||
|
|
|
||||||
|
|
@ -3000,10 +3000,38 @@ function closeDownloadMissingModal(playlistId) {
|
||||||
updateYouTubeCardPhase(urlHash, 'discovered');
|
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_')) {
|
if (playlistId.startsWith('tidal_')) {
|
||||||
const tidalPlaylistId = playlistId.replace('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');
|
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
|
// Clear wishlist modal state when modal is fully closed
|
||||||
|
|
@ -3299,6 +3327,8 @@ function startModalDownloadPolling(playlistId) {
|
||||||
if (!process || !process.batchId) return;
|
if (!process || !process.batchId) return;
|
||||||
if (process.poller) clearInterval(process.poller);
|
if (process.poller) clearInterval(process.poller);
|
||||||
|
|
||||||
|
console.log(`🔄 [Polling] Starting status polling for playlistId: ${playlistId}, batchId: ${process.batchId}`);
|
||||||
|
|
||||||
process.poller = setInterval(async () => {
|
process.poller = setInterval(async () => {
|
||||||
if (!activeDownloadProcesses[playlistId]) {
|
if (!activeDownloadProcesses[playlistId]) {
|
||||||
clearInterval(process.poller);
|
clearInterval(process.poller);
|
||||||
|
|
@ -3306,9 +3336,15 @@ function startModalDownloadPolling(playlistId) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/playlists/${process.batchId}/download_status`);
|
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();
|
const data = await response.json();
|
||||||
if (data.error) throw new Error(data.error);
|
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
|
// Auto-show wishlist modal during active auto-processing
|
||||||
const isWishlist = (playlistId === 'wishlist');
|
const isWishlist = (playlistId === 'wishlist');
|
||||||
const isAutoInitiated = data.auto_initiated || false;
|
const isAutoInitiated = data.auto_initiated || false;
|
||||||
|
|
@ -3335,6 +3371,8 @@ function startModalDownloadPolling(playlistId) {
|
||||||
document.getElementById(`stat-missing-${playlistId}`).textContent = missingCount;
|
document.getElementById(`stat-missing-${playlistId}`).textContent = missingCount;
|
||||||
}
|
}
|
||||||
} else if (data.phase === 'downloading' || data.phase === 'complete' || data.phase === 'error') {
|
} 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%') {
|
if (document.getElementById(`analysis-progress-fill-${playlistId}`).style.width !== '100%') {
|
||||||
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!';
|
document.getElementById(`analysis-progress-text-${playlistId}`).textContent = 'Analysis complete!';
|
||||||
|
|
@ -3350,10 +3388,20 @@ function startModalDownloadPolling(playlistId) {
|
||||||
const missingCount = missingTracks.length;
|
const missingCount = missingTracks.length;
|
||||||
let completedCount = 0;
|
let completedCount = 0;
|
||||||
let failedOrCancelledCount = 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 => {
|
(data.tasks || []).forEach(task => {
|
||||||
const row = document.querySelector(`#download-missing-modal-${playlistId} tr[data-track-index="${task.track_index}"]`);
|
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
|
// Stronger protection: Don't override locally cancelled tracks with any backend updates
|
||||||
if (row.dataset.locallyCancelled === 'true') {
|
if (row.dataset.locallyCancelled === 'true') {
|
||||||
|
|
@ -3374,7 +3422,12 @@ function startModalDownloadPolling(playlistId) {
|
||||||
case 'cancelled': statusText = '🚫 Cancelled'; failedOrCancelledCount++; break;
|
case 'cancelled': statusText = '🚫 Cancelled'; failedOrCancelledCount++; break;
|
||||||
default: statusText = `⚪ ${task.status}`; 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 === '-') {
|
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>`;
|
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_', '');
|
const tidalPlaylistId = playlistId.replace('tidal_', '');
|
||||||
if (tidalPlaylistStates[tidalPlaylistId]) {
|
if (tidalPlaylistStates[tidalPlaylistId]) {
|
||||||
tidalPlaylistStates[tidalPlaylistId].phase = 'download_complete';
|
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');
|
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) {
|
} 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);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
@ -6445,10 +6507,28 @@ function createTidalCard(playlist) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleTidalCardClick(playlistId) {
|
async function handleTidalCardClick(playlistId) {
|
||||||
|
// Robust state validation
|
||||||
const state = tidalPlaylistStates[playlistId];
|
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') {
|
if (state.phase === 'fresh') {
|
||||||
// No need to fetch data - we already have all tracks from initial load (like sync.py)
|
// 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);
|
openTidalDiscoveryModal(playlistId, state.playlist);
|
||||||
|
|
||||||
} else if (state.phase === 'discovering' || state.phase === 'discovered' || state.phase === 'syncing' || state.phase === 'sync_complete') {
|
} 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);
|
openTidalDiscoveryModal(playlistId, state.playlist);
|
||||||
} else if (state.phase === 'downloading' || state.phase === 'download_complete') {
|
} else if (state.phase === 'downloading' || state.phase === 'download_complete') {
|
||||||
// Open download modal if we have the converted playlist ID
|
// Open download modal if we have the converted playlist ID
|
||||||
if (state.convertedSpotifyPlaylistId) {
|
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
|
// Check if modal already exists, if not create it
|
||||||
if (activeDownloadProcesses[state.convertedSpotifyPlaylistId]) {
|
if (activeDownloadProcesses[state.convertedSpotifyPlaylistId]) {
|
||||||
const process = activeDownloadProcesses[state.convertedSpotifyPlaylistId];
|
const process = activeDownloadProcesses[state.convertedSpotifyPlaylistId];
|
||||||
if (process.modalElement) {
|
if (process.modalElement) {
|
||||||
|
console.log(`📱 [Card Click] Showing existing download modal for ${state.phase} phase`);
|
||||||
process.modalElement.style.display = 'flex';
|
process.modalElement.style.display = 'flex';
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ [Card Click] Download process exists but modal element missing - rehydrating`);
|
||||||
|
await rehydrateTidalDownloadModal(playlistId, state);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Need to create the download modal - fetch the discovery results
|
// 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);
|
await rehydrateTidalDownloadModal(playlistId, state);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error('❌ No converted Spotify playlist ID found for Tidal download modal');
|
console.error('❌ [Card Click] No converted Spotify playlist ID found for Tidal download modal');
|
||||||
showToast('Unable to open download modal - missing playlist data', 'error');
|
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) {
|
async function rehydrateTidalDownloadModal(playlistId, state) {
|
||||||
try {
|
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
|
// Get discovery results from backend if not already loaded
|
||||||
if (!state.discovery_results) {
|
if (!state.discovery_results) {
|
||||||
|
|
@ -6545,6 +6670,27 @@ async function rehydrateTidalDownloadModal(playlistId, state) {
|
||||||
if (beginBtn) beginBtn.style.display = 'none';
|
if (beginBtn) beginBtn.style.display = 'none';
|
||||||
if (cancelBtn) cancelBtn.style.display = 'none';
|
if (cancelBtn) cancelBtn.style.display = 'none';
|
||||||
console.log(`✅ Showing completed Tidal download results: ${state.download_process_id}`);
|
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) {
|
function updateTidalCardPhase(playlistId, phase) {
|
||||||
const state = tidalPlaylistStates[playlistId];
|
const state = tidalPlaylistStates[playlistId];
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
|
@ -6566,13 +6792,30 @@ function updateTidalCardPhase(playlistId, phase) {
|
||||||
// Re-render the card with new phase
|
// Re-render the card with new phase
|
||||||
const card = document.getElementById(`tidal-card-${playlistId}`);
|
const card = document.getElementById(`tidal-card-${playlistId}`);
|
||||||
if (card) {
|
if (card) {
|
||||||
|
const oldButtonText = card.querySelector('.playlist-card-action-btn')?.textContent || 'unknown';
|
||||||
const newCardHtml = createTidalCard(state.playlist);
|
const newCardHtml = createTidalCard(state.playlist);
|
||||||
card.outerHTML = newCardHtml;
|
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
|
// Re-attach click handler
|
||||||
const newCard = document.getElementById(`tidal-card-${playlistId}`);
|
const newCard = document.getElementById(`tidal-card-${playlistId}`);
|
||||||
if (newCard) {
|
if (newCard) {
|
||||||
newCard.addEventListener('click', () => handleTidalCardClick(playlistId));
|
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
|
// If we have sync progress and we're in sync/sync_complete phase, restore it
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue