diff --git a/web_server.py b/web_server.py index c3bf6729..cdd12492 100644 --- a/web_server.py +++ b/web_server.py @@ -992,7 +992,7 @@ class WebUIDownloadMonitor: task_status = download_tasks[task_id]['status'] if task_status in ['searching', 'downloading', 'queued', 'post_processing']: actually_active += 1 - elif task_status in ['failed', 'complete', 'cancelled'] and task_id in queue[queue_index:]: + elif task_status in ['failed', 'complete', 'cancelled', 'not_found'] and task_id in queue[queue_index:]: # These are orphaned tasks - they're done but still in active queue orphaned_tasks.append(task_id) @@ -1067,7 +1067,7 @@ def validate_and_heal_batch_states(): task_status = download_tasks[task_id]['status'] if task_status in ['searching', 'downloading', 'queued', 'post_processing']: actually_active += 1 - elif task_status in ['failed', 'completed', 'cancelled']: + elif task_status in ['failed', 'completed', 'cancelled', 'not_found']: orphaned_tasks.append(task_id) else: # Task in queue but not in download_tasks dict @@ -12953,7 +12953,7 @@ def _on_download_completed(batch_id, task_id, success=True): 'artist_name': _get_track_artist_name(original_track_info), 'retry_count': task.get('retry_count', 0), 'spotify_track': spotify_track_data, # Properly formatted spotify track for wishlist - 'failure_reason': 'Download cancelled' if task_status == 'cancelled' else 'Download failed', + 'failure_reason': 'Download cancelled' if task_status == 'cancelled' else ('Not found on Soulseek' if task_status == 'not_found' else 'Download failed'), 'candidates': task.get('cached_candidates', []) # Include search results if available } @@ -12961,10 +12961,14 @@ def _on_download_completed(batch_id, task_id, success=True): download_batches[batch_id]['cancelled_tracks'].add(task.get('track_index', 0)) print(f"🚫 [Batch Manager] Added cancelled track to batch tracking: {track_info['track_name']}") add_activity_item("🚫", "Download Cancelled", f"'{track_info['track_name']}'", "Now") - elif task_status == 'failed': + elif task_status in ('failed', 'not_found'): download_batches[batch_id]['permanently_failed_tracks'].append(track_info) - print(f"❌ [Batch Manager] Added failed track to batch tracking: {track_info['track_name']}") - add_activity_item("❌", "Download Failed", f"'{track_info['track_name']}'", "Now") + if task_status == 'not_found': + print(f"🔇 [Batch Manager] Added not-found track to batch tracking: {track_info['track_name']}") + add_activity_item("🔇", "Not Found", f"'{track_info['track_name']}'", "Now") + else: + print(f"❌ [Batch Manager] Added failed track to batch tracking: {track_info['track_name']}") + add_activity_item("❌", "Download Failed", f"'{track_info['track_name']}'", "Now") # WISHLIST REMOVAL: Handle successful downloads for wishlist removal if success and task_id in download_tasks: @@ -13031,8 +13035,8 @@ def _on_download_completed(batch_id, task_id, success=True): if task_status == 'searching': task_age = current_time - task.get('status_change_time', current_time) if task_age > 600: # 10 minutes - print(f"⏰ [Stuck Detection] Task {task_id} stuck in searching for {task_age:.0f}s - forcing failure") - task['status'] = 'failed' + print(f"⏰ [Stuck Detection] Task {task_id} stuck in searching for {task_age:.0f}s - forcing not_found") + task['status'] = 'not_found' task['error_message'] = f'Search stuck for {int(task_age // 60)} minutes with no results — timed out' finished_count += 1 else: @@ -13045,7 +13049,7 @@ def _on_download_completed(batch_id, task_id, success=True): finished_count += 1 else: retrying_count += 1 - elif task_status in ['completed', 'failed', 'cancelled']: + elif task_status in ['completed', 'failed', 'cancelled', 'not_found']: finished_count += 1 else: # Task ID in queue but not in download_tasks - treat as completed to prevent blocking @@ -13960,7 +13964,7 @@ def _download_track_worker(task_id, batch_id=None): print(f"❌ [Modal Worker] No valid candidates found for '{track.name}' after trying all {len(search_queries)} queries.") with tasks_lock: if task_id in download_tasks: - download_tasks[task_id]['status'] = 'failed' + download_tasks[task_id]['status'] = 'not_found' _diag_summary = ' | '.join(search_diagnostics) if search_diagnostics else 'no queries attempted' download_tasks[task_id]['error_message'] = f'No match found for "{track_name}" by {artist_name or "Unknown"} after {len(search_queries)} queries. Breakdown: {_diag_summary}' @@ -14626,9 +14630,14 @@ def _build_batch_status_data(batch_id, batch, live_transfers_lookup): print(f"⚠️ [Safety Valve] Error checking for completed file: {e}") if not recovered: - print(f"⏰ [Safety Valve] Task {task_id} stuck for {task_age:.1f}s - forcing failure") - task['status'] = 'failed' - task['error_message'] = f'Task stuck in {stuck_state} state for {int(task_age // 60)} minutes — forcibly stopped' + if stuck_state == 'searching': + print(f"⏰ [Safety Valve] Task {task_id} stuck in searching for {task_age:.1f}s - marking not_found") + task['status'] = 'not_found' + task['error_message'] = f'Search stuck for {int(task_age // 60)} minutes with no results — timed out' + else: + print(f"⏰ [Safety Valve] Task {task_id} stuck for {task_age:.1f}s - forcing failure") + task['status'] = 'failed' + task['error_message'] = f'Task stuck in {stuck_state} state for {int(task_age // 60)} minutes — forcibly stopped' task_status = { 'task_id': task_id, @@ -14654,7 +14663,7 @@ def _build_batch_status_data(batch_id, batch, live_transfers_lookup): state_str = live_info.get('state', 'Unknown') # Don't override tasks that are already in terminal states or post-processing - if task['status'] not in ['completed', 'failed', 'cancelled', 'post_processing']: + if task['status'] not in ['completed', 'failed', 'cancelled', 'not_found', 'post_processing']: # SYNC.PY PARITY: Prioritized state checking (Errored/Cancelled before Completed) # This prevents "Completed, Errored" states from being marked as completed if 'Cancelled' in state_str or 'Canceled' in state_str: @@ -15269,8 +15278,8 @@ def _check_batch_completion_v2(batch_id): if task_status == 'searching': task_age = current_time - task.get('status_change_time', current_time) if task_age > 600: # 10 minutes - print(f"⏰ [Stuck Detection V2] Task {task_id} stuck in searching for {task_age:.0f}s - forcing failure") - task['status'] = 'failed' + print(f"⏰ [Stuck Detection V2] Task {task_id} stuck in searching for {task_age:.0f}s - forcing not_found") + task['status'] = 'not_found' task['error_message'] = f'Search stuck for {int(task_age // 60)} minutes with no results — timed out' finished_count += 1 else: @@ -15283,7 +15292,7 @@ def _check_batch_completion_v2(batch_id): finished_count += 1 else: retrying_count += 1 - elif task_status in ['completed', 'failed', 'cancelled']: + elif task_status in ['completed', 'failed', 'cancelled', 'not_found']: finished_count += 1 else: # Task ID in queue but not in download_tasks - treat as completed to prevent blocking @@ -15481,7 +15490,7 @@ def cancel_batch(batch_id): for task_id in download_batches[batch_id].get('queue', []): if task_id in download_tasks: task = download_tasks[task_id] - if task['status'] not in ['completed', 'cancelled']: + if task['status'] not in ['completed', 'failed', 'not_found', 'cancelled']: task['status'] = 'cancelled' cancelled_count += 1 diff --git a/webui/static/script.js b/webui/static/script.js index f8b50202..2c98814a 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -8048,6 +8048,7 @@ function processModalStatusUpdate(playlistId, data) { const missingCount = missingTracks.length; let completedCount = 0; let failedOrCancelledCount = 0; + let notFoundCount = 0; // Verify modal exists before processing tasks const modal = document.getElementById(`download-missing-modal-${playlistId}`); @@ -8107,6 +8108,7 @@ function processModalStatusUpdate(playlistId, data) { case 'downloading': statusText = `⏬ Downloading... ${Math.round(task.progress || 0)}%`; break; case 'post_processing': statusText = '⌛ Processing...'; break; case 'completed': statusText = '✅ Completed'; completedCount++; break; + case 'not_found': statusText = '🔇 Not Found'; notFoundCount++; break; case 'failed': statusText = '❌ Failed'; failedOrCancelledCount++; break; case 'cancelled': statusText = '🚫 Cancelled'; failedOrCancelledCount++; break; default: statusText = `⚪ ${task.status}`; break; @@ -8119,7 +8121,7 @@ function processModalStatusUpdate(playlistId, data) { statusEl.removeAttribute('data-error-msg'); statusEl.textContent = statusText; - if ((task.status === 'failed' || task.status === 'cancelled') && task.error_message) { + if ((task.status === 'failed' || task.status === 'cancelled' || task.status === 'not_found') && task.error_message) { statusEl.classList.add('has-error-tooltip'); statusEl.dataset.errorMsg = task.error_message; _ensureErrorTooltipListeners(statusEl); @@ -8130,7 +8132,7 @@ function processModalStatusUpdate(playlistId, data) { } // V2 SYSTEM: Smart button management with persistent state awareness - if (actionsEl && !['completed', 'failed', 'cancelled', 'post_processing'].includes(task.status)) { + if (actionsEl && !['completed', 'failed', 'cancelled', 'not_found', 'post_processing'].includes(task.status)) { // Check if we're in a cancelling state if (isV2Task && uiState === 'cancelling') { actionsEl.innerHTML = 'Cancelling...'; @@ -8139,7 +8141,7 @@ function processModalStatusUpdate(playlistId, data) { const onclickHandler = isV2Task ? 'cancelTrackDownloadV2' : 'cancelTrackDownload'; actionsEl.innerHTML = ``; } - } else if (actionsEl && ['completed', 'failed', 'cancelled', 'post_processing'].includes(task.status)) { + } else if (actionsEl && ['completed', 'failed', 'cancelled', 'not_found', 'post_processing'].includes(task.status)) { actionsEl.innerHTML = '-'; // No actions available for terminal or processing states } }); @@ -8168,7 +8170,7 @@ function processModalStatusUpdate(playlistId, data) { console.debug(`📊 [Worker Status] ${playlistId}: ${serverActiveWorkers}/${maxWorkers} active workers, ${clientActiveWorkers} client-side active tasks`); - const totalFinished = completedCount + failedOrCancelledCount; + const totalFinished = completedCount + failedOrCancelledCount + notFoundCount; const progressPercent = missingCount > 0 ? (totalFinished / missingCount) * 100 : 0; document.getElementById(`download-progress-fill-${playlistId}`).style.width = `${progressPercent}%`; document.getElementById(`download-progress-text-${playlistId}`).textContent = `${completedCount}/${missingCount} completed (${progressPercent.toFixed(0)}%)`; @@ -8208,7 +8210,10 @@ function processModalStatusUpdate(playlistId, data) { autoSavePlaylistM3U(playlistId); // Show completion message - const completionMessage = `Download complete! ${completedCount} downloaded, ${failedOrCancelledCount} failed.`; + let completionParts = [`${completedCount} downloaded`]; + if (notFoundCount > 0) completionParts.push(`${notFoundCount} not found`); + if (failedOrCancelledCount > 0) completionParts.push(`${failedOrCancelledCount} failed`); + const completionMessage = `Download complete! ${completionParts.join(', ')}.`; showToast(completionMessage, 'success'); // Auto-close wishlist modal when completed (for auto-processing) @@ -8317,7 +8322,7 @@ function processModalStatusUpdate(playlistId, data) { // Handle background wishlist processing completion specially if (isBackgroundWishlist) { - console.log(`🎉 Background wishlist processing complete: ${completedCount} downloaded, ${failedOrCancelledCount} failed`); + console.log(`🎉 Background wishlist processing complete: ${completedCount} downloaded, ${notFoundCount} not found, ${failedOrCancelledCount} failed`); // Reset modal to idle state to prevent "complete" phase disruption setTimeout(() => { @@ -8335,7 +8340,10 @@ function processModalStatusUpdate(playlistId, data) { // Check for wishlist summary from backend (added when failed/cancelled tracks are processed) if (data.wishlist_summary) { const summary = data.wishlist_summary; - completionMessage = `Download process complete! Downloaded: ${completedCount}, Failed/Cancelled: ${failedOrCancelledCount}.`; + let summaryParts = [`Downloaded: ${completedCount}`]; + if (notFoundCount > 0) summaryParts.push(`Not Found: ${notFoundCount}`); + if (failedOrCancelledCount > 0) summaryParts.push(`Failed: ${failedOrCancelledCount}`); + completionMessage = `Download process complete! ${summaryParts.join(', ')}.`; if (summary.tracks_added > 0) { completionMessage += ` Added ${summary.tracks_added} failed track${summary.tracks_added !== 1 ? 's' : ''} to wishlist for automatic retry.`; @@ -14037,6 +14045,7 @@ function updateCompletedModalResults(playlistId, downloadData) { const missingTracks = (downloadData.analysis_results || []).filter(r => !r.found); let completedCount = 0; let failedOrCancelledCount = 0; + let notFoundCount = 0; (downloadData.tasks || []).forEach(task => { const row = document.querySelector(`#download-missing-modal-${CSS.escape(playlistId)} tr[data-track-index="${task.track_index}"]`); @@ -14053,6 +14062,7 @@ function updateCompletedModalResults(playlistId, downloadData) { case 'downloading': statusText = `⏬ Downloading... ${Math.round(task.progress || 0)}%`; break; case 'post_processing': statusText = '⌛ Processing...'; break; // NEW VERIFICATION WORKFLOW case 'completed': statusText = '✅ Completed'; completedCount++; break; + case 'not_found': statusText = '🔇 Not Found'; notFoundCount++; break; case 'failed': statusText = '❌ Failed'; failedOrCancelledCount++; break; case 'cancelled': statusText = '🚫 Cancelled'; failedOrCancelledCount++; break; default: statusText = `⚪ ${task.status}`; break; @@ -14063,7 +14073,7 @@ function updateCompletedModalResults(playlistId, downloadData) { }); // Update download progress to final state - const totalFinished = completedCount + failedOrCancelledCount; + const totalFinished = completedCount + failedOrCancelledCount + notFoundCount; const missingCount = missingTracks.length; const progressPercent = missingCount > 0 ? (totalFinished / missingCount) * 100 : 100; @@ -14075,7 +14085,7 @@ function updateCompletedModalResults(playlistId, downloadData) { 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`); + console.log(`✅ [Completed Results] Updated modal with ${completedCount} completed, ${notFoundCount} not found, ${failedOrCancelledCount} failed tasks`); } catch (error) { console.error(`❌ [Completed Results] Error updating completed modal results:`, error);