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);