cancel button
This commit is contained in:
parent
68c1dca27e
commit
fcdf41aac2
2 changed files with 105 additions and 44 deletions
|
|
@ -3307,6 +3307,21 @@ def _attempt_download_with_candidates(task_id, candidates, track, batch_id=None)
|
||||||
# Update task with successful download info
|
# Update task with successful download info
|
||||||
with tasks_lock:
|
with tasks_lock:
|
||||||
if task_id in download_tasks:
|
if task_id in download_tasks:
|
||||||
|
# PHASE 3: Final cancellation check after download started (GUI PARITY)
|
||||||
|
if download_tasks[task_id]['status'] == 'cancelled':
|
||||||
|
print(f"🚫 [Modal Worker] Task {task_id} cancelled after download {download_id} started - attempting to cancel download")
|
||||||
|
# Try to cancel the download immediately
|
||||||
|
try:
|
||||||
|
asyncio.run(soulseek_client.cancel_download(download_id, username, remove=True))
|
||||||
|
print(f"✅ Successfully cancelled active download {download_id}")
|
||||||
|
except Exception as cancel_error:
|
||||||
|
print(f"⚠️ Warning: Failed to cancel active download {download_id}: {cancel_error}")
|
||||||
|
|
||||||
|
# Free worker slot
|
||||||
|
if batch_id:
|
||||||
|
_on_download_completed(batch_id, task_id, success=False)
|
||||||
|
return False
|
||||||
|
|
||||||
download_tasks[task_id]['download_id'] = download_id
|
download_tasks[task_id]['download_id'] = download_id
|
||||||
download_tasks[task_id]['username'] = username
|
download_tasks[task_id]['username'] = username
|
||||||
download_tasks[task_id]['filename'] = filename
|
download_tasks[task_id]['filename'] = filename
|
||||||
|
|
|
||||||
|
|
@ -1614,6 +1614,9 @@ let currentDownloadBatchId = null;
|
||||||
let modalDownloadPoller = null;
|
let modalDownloadPoller = null;
|
||||||
let currentModalPlaylistId = null;
|
let currentModalPlaylistId = null;
|
||||||
|
|
||||||
|
// PHASE 2: Local cancelled track management (GUI PARITY)
|
||||||
|
let cancelledTracks = new Set(); // Track cancelled track indices like GUI's cancelled_tracks
|
||||||
|
|
||||||
async function openDownloadMissingModal(playlistId) {
|
async function openDownloadMissingModal(playlistId) {
|
||||||
console.log(`📥 Opening Download Missing Tracks modal for playlist: ${playlistId}`);
|
console.log(`📥 Opening Download Missing Tracks modal for playlist: ${playlistId}`);
|
||||||
|
|
||||||
|
|
@ -1973,7 +1976,15 @@ async function initiateMissingDownloads() {
|
||||||
|
|
||||||
// Store batch ID for polling
|
// Store batch ID for polling
|
||||||
currentDownloadBatchId = data.batch_id;
|
currentDownloadBatchId = data.batch_id;
|
||||||
console.log(`✅ Started download batch: ${currentDownloadBatchId}`);
|
|
||||||
|
// PHASE 2: Clear cancelled tracks for new session (GUI PARITY)
|
||||||
|
cancelledTracks.clear();
|
||||||
|
console.log(`✅ Started download batch: ${currentDownloadBatchId}, cleared cancelled tracks`);
|
||||||
|
|
||||||
|
// Clear any locally cancelled flags from previous sessions
|
||||||
|
document.querySelectorAll('tr[data-locally-cancelled="true"]').forEach(row => {
|
||||||
|
row.removeAttribute('data-locally-cancelled');
|
||||||
|
});
|
||||||
|
|
||||||
// Start live polling
|
// Start live polling
|
||||||
startModalDownloadPolling();
|
startModalDownloadPolling();
|
||||||
|
|
@ -2026,6 +2037,26 @@ function startModalDownloadPolling() {
|
||||||
const isMissingTrack = missingTracks.some(mt => mt.track_index === trackIndex);
|
const isMissingTrack = missingTracks.some(mt => mt.track_index === trackIndex);
|
||||||
|
|
||||||
if (row && isMissingTrack) {
|
if (row && isMissingTrack) {
|
||||||
|
// PHASE 2 & 5: Respect local cancellation with recovery (GUI PARITY)
|
||||||
|
// Skip polling updates for tracks that were cancelled locally
|
||||||
|
if (row.dataset.locallyCancelled === 'true') {
|
||||||
|
console.log(`⏭️ Skipping polling update for locally cancelled track ${trackIndex}`);
|
||||||
|
|
||||||
|
// PHASE 5: Recovery check - ensure cancelled status is maintained
|
||||||
|
const statusElement = row.querySelector('.track-download-status');
|
||||||
|
if (statusElement && !statusElement.textContent.includes('🚫 Cancelled')) {
|
||||||
|
console.warn(`🔧 Recovering cancelled status for track ${trackIndex}`);
|
||||||
|
statusElement.textContent = '🚫 Cancelled';
|
||||||
|
statusElement.className = 'track-download-status download-cancelled';
|
||||||
|
|
||||||
|
const actionsElement = row.querySelector('.track-actions');
|
||||||
|
if (actionsElement) {
|
||||||
|
actionsElement.innerHTML = '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const statusElement = row.querySelector('.track-download-status');
|
const statusElement = row.querySelector('.track-download-status');
|
||||||
const actionsElement = row.querySelector('.track-actions');
|
const actionsElement = row.querySelector('.track-actions');
|
||||||
row.dataset.taskId = task.task_id;
|
row.dataset.taskId = task.task_id;
|
||||||
|
|
@ -2075,15 +2106,18 @@ function startModalDownloadPolling() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update progress bar and stats
|
// PHASE 2: Include locally cancelled tracks in progress calculation (GUI PARITY)
|
||||||
const progressPercent = totalTasks > 0 ? ((completedCount + failedCount) / totalTasks) * 100 : 0;
|
const locallyCancelledCount = cancelledTracks.size;
|
||||||
|
const totalFinished = completedCount + failedCount + locallyCancelledCount;
|
||||||
|
const progressPercent = totalTasks > 0 ? (totalFinished / totalTasks) * 100 : 0;
|
||||||
|
|
||||||
document.getElementById('download-progress-fill').style.width = `${progressPercent}%`;
|
document.getElementById('download-progress-fill').style.width = `${progressPercent}%`;
|
||||||
document.getElementById('download-progress-text').textContent =
|
document.getElementById('download-progress-text').textContent =
|
||||||
`${completedCount}/${totalTasks} completed (${progressPercent.toFixed(0)}%)`;
|
`${completedCount}/${totalTasks} completed (${progressPercent.toFixed(0)}%)`;
|
||||||
document.getElementById('stat-downloaded').textContent = completedCount;
|
document.getElementById('stat-downloaded').textContent = completedCount;
|
||||||
|
|
||||||
// Stop polling when all tasks are complete
|
// Stop polling when all tasks are complete (including locally cancelled)
|
||||||
if (completedCount + failedCount >= totalTasks && totalTasks > 0) {
|
if (totalFinished >= totalTasks && totalTasks > 0) {
|
||||||
clearInterval(modalDownloadPoller);
|
clearInterval(modalDownloadPoller);
|
||||||
modalDownloadPoller = null;
|
modalDownloadPoller = null;
|
||||||
document.getElementById('cancel-all-btn').style.display = 'none';
|
document.getElementById('cancel-all-btn').style.display = 'none';
|
||||||
|
|
@ -2224,70 +2258,82 @@ async function cancelTrackDownload(trackIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the task ID that was stored by the polling function
|
// PHASE 5: Prevent double cancellation and check terminal states (GUI PARITY)
|
||||||
const taskId = row.dataset.taskId;
|
if (row.dataset.locallyCancelled === 'true') {
|
||||||
if (!taskId) {
|
console.warn(`Track ${trackIndex} is already cancelled locally`);
|
||||||
console.warn(`No task ID found for track ${trackIndex}, cancelling locally only`);
|
showToast('Track is already cancelled', 'info');
|
||||||
// Update UI immediately for local cancellation
|
|
||||||
const statusElement = row.querySelector('.track-download-status');
|
|
||||||
const actionsElement = row.querySelector('.track-actions');
|
|
||||||
|
|
||||||
if (statusElement) {
|
|
||||||
statusElement.textContent = '❌ Cancelled';
|
|
||||||
statusElement.className = 'track-download-status download-cancelled';
|
|
||||||
}
|
|
||||||
if (actionsElement) {
|
|
||||||
actionsElement.innerHTML = '-';
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update UI immediately to show cancellation in progress
|
|
||||||
const statusElement = row.querySelector('.track-download-status');
|
const statusElement = row.querySelector('.track-download-status');
|
||||||
|
const currentStatus = statusElement?.textContent || '';
|
||||||
|
|
||||||
|
// Check if track is in a terminal state where cancellation doesn't make sense
|
||||||
|
if (currentStatus.includes('✅ Completed') || currentStatus.includes('❌ Failed')) {
|
||||||
|
console.warn(`Cannot cancel track ${trackIndex} - already in terminal state: ${currentStatus}`);
|
||||||
|
showToast(`Cannot cancel - track is already ${currentStatus.includes('✅') ? 'completed' : 'failed'}`, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHASE 1: Immediate UI response (GUI PARITY)
|
||||||
|
// Update UI immediately to show definitive cancellation like GUI
|
||||||
const actionsElement = row.querySelector('.track-actions');
|
const actionsElement = row.querySelector('.track-actions');
|
||||||
|
|
||||||
if (statusElement) {
|
if (statusElement) {
|
||||||
statusElement.textContent = '⏹️ Cancelling...';
|
statusElement.textContent = '🚫 Cancelled';
|
||||||
statusElement.className = 'track-download-status download-cancelling';
|
statusElement.className = 'track-download-status download-cancelled';
|
||||||
}
|
}
|
||||||
if (actionsElement) {
|
if (actionsElement) {
|
||||||
actionsElement.innerHTML = '-';
|
actionsElement.innerHTML = '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the backend cancel endpoint
|
// PHASE 2: Add to cancelled tracks Set and mark locally cancelled (GUI PARITY)
|
||||||
|
cancelledTracks.add(trackIndex);
|
||||||
|
row.dataset.locallyCancelled = 'true';
|
||||||
|
console.log(`✅ Track ${trackIndex} added to cancelledTracks Set and marked locally cancelled (immediate UI response)`);
|
||||||
|
|
||||||
|
// Get the task ID for backend cancellation
|
||||||
|
const taskId = row.dataset.taskId;
|
||||||
|
if (!taskId) {
|
||||||
|
console.warn(`No task ID found for track ${trackIndex}, using local cancellation only`);
|
||||||
|
showToast('Track cancelled (local only)', 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHASE 5: Call backend with timeout to avoid hanging (GUI PARITY)
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
|
||||||
|
|
||||||
const response = await fetch('/api/downloads/cancel_task', {
|
const response = await fetch('/api/downloads/cancel_task', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ task_id: taskId })
|
body: JSON.stringify({ task_id: taskId }),
|
||||||
|
signal: controller.signal
|
||||||
});
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
console.log(`✅ Successfully cancelled task ${taskId}`);
|
console.log(`✅ Successfully cancelled task ${taskId} on backend`);
|
||||||
// The polling function will update the UI with the final cancelled state
|
|
||||||
showToast('Download cancelled successfully', 'info');
|
showToast('Download cancelled successfully', 'info');
|
||||||
} else {
|
} else {
|
||||||
throw new Error(data.error || 'Failed to cancel download');
|
console.warn(`Backend cancellation failed for task ${taskId}: ${data.error}`);
|
||||||
|
// Don't change UI - we already showed cancelled status immediately
|
||||||
|
showToast('Download cancelled (backend error, but stopped locally)', 'warning');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error cancelling download:', error);
|
// PHASE 5: Handle different error types appropriately (GUI PARITY)
|
||||||
showToast(`Failed to cancel download: ${error.message}`, 'error');
|
if (error.name === 'AbortError') {
|
||||||
|
console.warn('⏱️ Backend cancellation timed out after 5 seconds');
|
||||||
// Reset UI on error
|
showToast('Download cancelled (backend timeout, but stopped locally)', 'warning');
|
||||||
const row = document.querySelector(`tr[data-track-index="${trackIndex}"]`);
|
} else {
|
||||||
if (row) {
|
console.error('❌ Error with backend cancellation:', error);
|
||||||
const statusElement = row.querySelector('.track-download-status');
|
showToast('Download cancelled (network error, but stopped locally)', 'warning');
|
||||||
const actionsElement = row.querySelector('.track-actions');
|
|
||||||
|
|
||||||
if (statusElement && statusElement.textContent === '⏹️ Cancelling...') {
|
|
||||||
statusElement.textContent = '❌ Cancel Failed';
|
|
||||||
statusElement.className = 'track-download-status download-failed';
|
|
||||||
}
|
|
||||||
if (actionsElement) {
|
|
||||||
actionsElement.innerHTML = `<button class="cancel-track-btn" onclick="cancelTrackDownload(${trackIndex})">Cancel</button>`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Don't revert UI - track is still cancelled locally like GUI behavior
|
||||||
|
console.log(`Track ${trackIndex} remains cancelled locally despite backend error`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue