Add cancel button for watchlist scans (manual and automation-triggered)

This commit is contained in:
Broque Thomas 2026-03-15 23:24:18 -07:00
parent c90fff37f1
commit 7871f4581c
2 changed files with 393 additions and 226 deletions

View file

@ -30481,6 +30481,20 @@ def start_watchlist_scan():
circuit_breaker_pause = 60 # seconds, doubles each trigger, max 600s circuit_breaker_pause = 60 # seconds, doubles each trigger, max 600s
for i, artist in enumerate(watchlist_artists): for i, artist in enumerate(watchlist_artists):
# Check for cancel request
if watchlist_scan_state.get('cancel_requested'):
print(f"🛑 [Manual Watchlist Scan] Cancel requested after {i}/{len(watchlist_artists)} artists")
watchlist_scan_state['status'] = 'cancelled'
watchlist_scan_state['current_phase'] = 'cancelled'
watchlist_scan_state['summary'] = {
'total_artists': i,
'successful_scans': len([r for r in scan_results if r.success]),
'new_tracks_found': sum(r.new_tracks_found for r in scan_results if r.success),
'tracks_added_to_wishlist': sum(r.tracks_added_to_wishlist for r in scan_results if r.success),
'cancelled': True
}
break
try: try:
# Fetch artist image using provider-aware method # Fetch artist image using provider-aware method
artist_image_url = scanner.get_artist_image_url(artist) or '' artist_image_url = scanner.get_artist_image_url(artist) or ''
@ -30662,7 +30676,9 @@ def start_watchlist_scan():
'error_message': str(e) 'error_message': str(e)
})()) })())
# Store final results # Store final results (skip if cancelled — already set by cancel handler)
was_cancelled = watchlist_scan_state.get('cancel_requested', False)
if not was_cancelled:
watchlist_scan_state['status'] = 'completed' watchlist_scan_state['status'] = 'completed'
watchlist_scan_state['results'] = scan_results watchlist_scan_state['results'] = scan_results
watchlist_scan_state['completed_at'] = datetime.now() watchlist_scan_state['completed_at'] = datetime.now()
@ -30682,7 +30698,11 @@ def start_watchlist_scan():
print(f"Watchlist scan completed: {len(successful_scans)}/{len(scan_results)} artists scanned successfully") print(f"Watchlist scan completed: {len(successful_scans)}/{len(scan_results)} artists scanned successfully")
print(f"Found {total_new_tracks} new tracks, added {total_added_to_wishlist} to wishlist") print(f"Found {total_new_tracks} new tracks, added {total_added_to_wishlist} to wishlist")
else:
print(f"🛑 Watchlist scan cancelled — skipping post-scan steps")
# Post-scan steps — skip if cancelled
if not was_cancelled:
# Populate discovery pool from similar artists # Populate discovery pool from similar artists
print("🎵 Starting discovery pool population...") print("🎵 Starting discovery pool population...")
watchlist_scan_state['current_phase'] = 'populating_discovery_pool' watchlist_scan_state['current_phase'] = 'populating_discovery_pool'
@ -30783,7 +30803,8 @@ def start_watchlist_scan():
'started_at': datetime.now(), 'started_at': datetime.now(),
'results': [], 'results': [],
'summary': {}, 'summary': {},
'error': None 'error': None,
'cancel_requested': False
} }
# Start scan in background # Start scan in background
@ -30827,6 +30848,22 @@ def get_watchlist_scan_status():
print(f"Error getting watchlist scan status: {e}") print(f"Error getting watchlist scan status: {e}")
return jsonify({"success": False, "error": str(e)}), 500 return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/watchlist/scan/cancel', methods=['POST'])
def cancel_watchlist_scan():
"""Cancel a running watchlist scan"""
try:
global watchlist_scan_state
if watchlist_scan_state.get('status') != 'scanning':
return jsonify({"success": False, "error": "No scan is currently running"}), 400
watchlist_scan_state['cancel_requested'] = True
print("🛑 [Watchlist Scan] Cancel requested by user")
return jsonify({"success": True, "message": "Cancel request sent"})
except Exception as e:
print(f"Error cancelling watchlist scan: {e}")
return jsonify({"success": False, "error": str(e)}), 500
# Similar Artists Update State # Similar Artists Update State
similar_artists_update_state = { similar_artists_update_state = {
'status': 'idle', # idle, running, completed, error 'status': 'idle', # idle, running, completed, error
@ -31419,7 +31456,8 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
'recent_wishlist_additions': [], 'recent_wishlist_additions': [],
'results': [], 'results': [],
'summary': {}, 'summary': {},
'error': None 'error': None,
'cancel_requested': False
} }
scan_results = [] scan_results = []
@ -31459,6 +31497,22 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
# Scan each artist with detailed tracking # Scan each artist with detailed tracking
for i, artist in enumerate(watchlist_artists): for i, artist in enumerate(watchlist_artists):
# Check for cancel request
if watchlist_scan_state.get('cancel_requested'):
print(f"🛑 [Auto-Watchlist] Cancel requested after {i}/{len(watchlist_artists)} artists")
watchlist_scan_state['status'] = 'cancelled'
watchlist_scan_state['current_phase'] = 'cancelled'
watchlist_scan_state['summary'] = {
'total_artists': i,
'successful_scans': len([r for r in scan_results if r.success]),
'new_tracks_found': sum(r.new_tracks_found for r in scan_results if r.success),
'tracks_added_to_wishlist': sum(r.tracks_added_to_wishlist for r in scan_results if r.success),
'cancelled': True
}
_update_automation_progress(automation_id, progress=100, phase='Cancelled by user',
log_line='Scan cancelled by user', log_type='warning')
break
try: try:
# Fetch artist image using provider-aware method # Fetch artist image using provider-aware method
artist_image_url = scanner.get_artist_image_url(artist) or '' artist_image_url = scanner.get_artist_image_url(artist) or ''
@ -31665,7 +31719,9 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
})()) })())
continue continue
# Update state with results # Update state with results (skip if cancelled — already set by cancel handler)
was_cancelled = watchlist_scan_state.get('cancel_requested', False)
if not was_cancelled:
successful_scans = [r for r in scan_results if r.success] successful_scans = [r for r in scan_results if r.success]
total_new_tracks = sum(r.new_tracks_found for r in successful_scans) total_new_tracks = sum(r.new_tracks_found for r in successful_scans)
total_added_to_wishlist = sum(r.tracks_added_to_wishlist for r in successful_scans) total_added_to_wishlist = sum(r.tracks_added_to_wishlist for r in successful_scans)
@ -31685,7 +31741,13 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
_update_automation_progress(automation_id, progress=95, phase='Scan complete', _update_automation_progress(automation_id, progress=95, phase='Scan complete',
log_line=f'Scanned {len(successful_scans)} artists — {total_new_tracks} new tracks, {total_added_to_wishlist} added to wishlist', log_line=f'Scanned {len(successful_scans)} artists — {total_new_tracks} new tracks, {total_added_to_wishlist} added to wishlist',
log_type='success' if total_new_tracks > 0 else 'info') log_type='success' if total_new_tracks > 0 else 'info')
else:
total_new_tracks = watchlist_scan_state.get('summary', {}).get('new_tracks_found', 0)
total_added_to_wishlist = watchlist_scan_state.get('summary', {}).get('tracks_added_to_wishlist', 0)
print(f"🛑 Automatic watchlist scan cancelled — skipping post-scan steps")
# Post-scan steps — skip if cancelled
if not was_cancelled:
# Populate discovery pool from similar artists (per-profile) # Populate discovery pool from similar artists (per-profile)
print("🎵 Starting discovery pool population...") print("🎵 Starting discovery pool population...")
watchlist_scan_state['current_phase'] = 'populating_discovery_pool' watchlist_scan_state['current_phase'] = 'populating_discovery_pool'

View file

@ -33903,6 +33903,13 @@ async function showWatchlistModal() {
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
${scanStatus === 'scanning' ? 'Scanning...' : 'Scan for New Releases'} ${scanStatus === 'scanning' ? 'Scanning...' : 'Scan for New Releases'}
</button> </button>
<button class="playlist-modal-btn playlist-modal-btn-secondary watchlist-btn-cancel"
id="cancel-watchlist-scan-btn"
onclick="cancelWatchlistScan()"
style="display: ${scanStatus === 'scanning' ? '' : 'none'};">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
Cancel Scan
</button>
<button class="playlist-modal-btn playlist-modal-btn-secondary watchlist-btn-similar" <button class="playlist-modal-btn playlist-modal-btn-secondary watchlist-btn-similar"
id="update-similar-artists-btn" id="update-similar-artists-btn"
onclick="updateSimilarArtists()" onclick="updateSimilarArtists()"
@ -34899,6 +34906,37 @@ function filterWatchlistArtists() {
/** /**
* Start watchlist scan * Start watchlist scan
*/ */
async function cancelWatchlistScan() {
try {
const cancelBtn = document.getElementById('cancel-watchlist-scan-btn');
if (cancelBtn) {
cancelBtn.disabled = true;
cancelBtn.textContent = 'Cancelling...';
}
const response = await fetch('/api/watchlist/scan/cancel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Failed to cancel scan');
}
showToast('Cancel request sent — scan will stop after current artist', 'info');
} catch (error) {
console.error('Error cancelling watchlist scan:', error);
showToast(`Error cancelling scan: ${error.message}`, 'error');
const cancelBtn = document.getElementById('cancel-watchlist-scan-btn');
if (cancelBtn) {
cancelBtn.disabled = false;
cancelBtn.textContent = 'Cancel Scan';
}
}
}
async function startWatchlistScan() { async function startWatchlistScan() {
try { try {
const button = document.getElementById('scan-watchlist-btn'); const button = document.getElementById('scan-watchlist-btn');
@ -34918,6 +34956,14 @@ async function startWatchlistScan() {
button.textContent = 'Scanning...'; button.textContent = 'Scanning...';
// Show cancel button
const cancelBtn = document.getElementById('cancel-watchlist-scan-btn');
if (cancelBtn) {
cancelBtn.style.display = '';
cancelBtn.disabled = false;
cancelBtn.textContent = 'Cancel Scan';
}
// Show scan status // Show scan status
const statusDiv = document.getElementById('watchlist-scan-status'); const statusDiv = document.getElementById('watchlist-scan-status');
if (statusDiv) { if (statusDiv) {
@ -34944,6 +34990,12 @@ function handleWatchlistScanData(data) {
const button = document.getElementById('scan-watchlist-btn'); const button = document.getElementById('scan-watchlist-btn');
const liveActivity = document.getElementById('watchlist-live-activity'); const liveActivity = document.getElementById('watchlist-live-activity');
// Show/hide cancel button based on scan status
const cancelBtn = document.getElementById('cancel-watchlist-scan-btn');
if (cancelBtn) {
cancelBtn.style.display = data.status === 'scanning' ? '' : 'none';
}
// Update live visual activity display // Update live visual activity display
if (liveActivity && data.status === 'scanning') { if (liveActivity && data.status === 'scanning') {
liveActivity.style.display = 'flex'; liveActivity.style.display = 'flex';
@ -35049,6 +35101,53 @@ function handleWatchlistScanData(data) {
console.log('Watchlist scan completed:', data.summary); console.log('Watchlist scan completed:', data.summary);
} else if (data.status === 'cancelled') {
if (button) {
button.disabled = false;
button.textContent = 'Scan for New Releases';
button.classList.remove('btn-processing');
}
// Hide cancel button
const cancelBtn = document.getElementById('cancel-watchlist-scan-btn');
if (cancelBtn) {
cancelBtn.style.display = 'none';
cancelBtn.disabled = false;
cancelBtn.textContent = 'Cancel Scan';
}
// Hide live activity
if (liveActivity) {
liveActivity.style.display = 'none';
}
// Show cancellation message
const statusDiv = document.getElementById('watchlist-scan-status');
if (statusDiv && data.summary) {
const scanned = data.summary.total_artists || 0;
const newTracks = data.summary.new_tracks_found || 0;
const addedTracks = data.summary.tracks_added_to_wishlist || 0;
statusDiv.innerHTML = `
<div class="watchlist-scan-completion">
<div class="watchlist-scan-completion-message">Scan cancelled after ${scanned} artist${scanned !== 1 ? 's' : ''}</div>
<div style="font-size: 13px; opacity: 0.8;">
<span class="sync-stat">Scanned: ${scanned}</span>
<span class="sync-separator"> &bull; </span>
<span class="sync-stat">New tracks: ${newTracks}</span>
<span class="sync-separator"> &bull; </span>
<span class="sync-stat">Added to wishlist: ${addedTracks}</span>
</div>
</div>
`;
}
// Update watchlist count
updateWatchlistButtonCount();
showToast('Watchlist scan cancelled', 'info');
console.log('Watchlist scan cancelled:', data.summary);
} else if (data.status === 'error') { } else if (data.status === 'error') {
if (button) { if (button) {
button.disabled = false; button.disabled = false;
@ -35056,6 +35155,12 @@ function handleWatchlistScanData(data) {
button.classList.remove('btn-processing'); button.classList.remove('btn-processing');
} }
// Hide cancel button
const cancelBtn = document.getElementById('cancel-watchlist-scan-btn');
if (cancelBtn) {
cancelBtn.style.display = 'none';
}
// Hide live activity // Hide live activity
if (liveActivity) { if (liveActivity) {
liveActivity.style.display = 'none'; liveActivity.style.display = 'none';
@ -35073,7 +35178,7 @@ async function pollWatchlistScanStatus() {
if (data.success) { if (data.success) {
handleWatchlistScanData(data); handleWatchlistScanData(data);
if (data.status === 'completed' || data.status === 'error') { if (data.status === 'completed' || data.status === 'error' || data.status === 'cancelled') {
return; // Stop polling return; // Stop polling
} }
} }