Differentiate "not found" from download errors with distinct status and UI

This commit is contained in:
Broque Thomas 2026-02-19 12:05:20 -08:00
parent c70cfd335a
commit c018c5fd98
2 changed files with 46 additions and 27 deletions

View file

@ -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

View file

@ -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 = '<span style="color: #666;">Cancelling...</span>';
@ -8139,7 +8141,7 @@ function processModalStatusUpdate(playlistId, data) {
const onclickHandler = isV2Task ? 'cancelTrackDownloadV2' : 'cancelTrackDownload';
actionsEl.innerHTML = `<button class="cancel-track-btn" title="Cancel this download" onclick="${onclickHandler}('${playlistId}', ${task.track_index})">×</button>`;
}
} 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);