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
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:
# Fetch artist image using provider-aware method
artist_image_url = scanner.get_artist_image_url(artist) or ''
@ -30662,98 +30676,104 @@ def start_watchlist_scan():
'error_message': str(e)
})())
# Store final results
watchlist_scan_state['status'] = 'completed'
watchlist_scan_state['results'] = scan_results
watchlist_scan_state['completed_at'] = datetime.now()
watchlist_scan_state['current_phase'] = 'completed'
# Calculate summary
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_added_to_wishlist = sum(r.tracks_added_to_wishlist for r in successful_scans)
watchlist_scan_state['summary'] = {
'total_artists': len(scan_results),
'successful_scans': len(successful_scans),
'new_tracks_found': total_new_tracks,
'tracks_added_to_wishlist': total_added_to_wishlist
}
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")
# 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['results'] = scan_results
watchlist_scan_state['completed_at'] = datetime.now()
watchlist_scan_state['current_phase'] = 'completed'
# Populate discovery pool from similar artists
print("🎵 Starting discovery pool population...")
watchlist_scan_state['current_phase'] = 'populating_discovery_pool'
try:
scanner.populate_discovery_pool(profile_id=scan_profile_id)
print("✅ Discovery pool population complete")
except Exception as discovery_error:
print(f"⚠️ Error populating discovery pool: {discovery_error}")
import traceback
traceback.print_exc()
# Calculate summary
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_added_to_wishlist = sum(r.tracks_added_to_wishlist for r in successful_scans)
# Update ListenBrainz playlists cache
print("🧠 Starting ListenBrainz playlists update...")
watchlist_scan_state['current_phase'] = 'updating_listenbrainz'
try:
from core.listenbrainz_manager import ListenBrainzManager
db = get_database()
db_path = str(db.database_path)
# Update for all profiles with LB tokens
lb_profiles = db.get_profiles_with_listenbrainz()
if lb_profiles:
for lb_prof in lb_profiles:
lb_manager = ListenBrainzManager(db_path, profile_id=lb_prof['id'], token=lb_prof['token'], base_url=lb_prof['base_url'])
watchlist_scan_state['summary'] = {
'total_artists': len(scan_results),
'successful_scans': len(successful_scans),
'new_tracks_found': total_new_tracks,
'tracks_added_to_wishlist': total_added_to_wishlist
}
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")
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
print("🎵 Starting discovery pool population...")
watchlist_scan_state['current_phase'] = 'populating_discovery_pool'
try:
scanner.populate_discovery_pool(profile_id=scan_profile_id)
print("✅ Discovery pool population complete")
except Exception as discovery_error:
print(f"⚠️ Error populating discovery pool: {discovery_error}")
import traceback
traceback.print_exc()
# Update ListenBrainz playlists cache
print("🧠 Starting ListenBrainz playlists update...")
watchlist_scan_state['current_phase'] = 'updating_listenbrainz'
try:
from core.listenbrainz_manager import ListenBrainzManager
db = get_database()
db_path = str(db.database_path)
# Update for all profiles with LB tokens
lb_profiles = db.get_profiles_with_listenbrainz()
if lb_profiles:
for lb_prof in lb_profiles:
lb_manager = ListenBrainzManager(db_path, profile_id=lb_prof['id'], token=lb_prof['token'], base_url=lb_prof['base_url'])
lb_result = lb_manager.update_all_playlists()
if lb_result.get('success'):
print(f"✅ ListenBrainz update complete for profile {lb_prof['id']}: {lb_result.get('summary', {})}")
else:
# Fallback: use global config token
lb_manager = ListenBrainzManager(db_path)
lb_result = lb_manager.update_all_playlists()
if lb_result.get('success'):
print(f"✅ ListenBrainz update complete for profile {lb_prof['id']}: {lb_result.get('summary', {})}")
else:
# Fallback: use global config token
lb_manager = ListenBrainzManager(db_path)
lb_result = lb_manager.update_all_playlists()
if lb_result.get('success'):
print(f"✅ ListenBrainz update complete (global): {lb_result.get('summary', {})}")
elif lb_result.get('error'):
print(f"⚠️ ListenBrainz update skipped: {lb_result.get('error')}")
except Exception as lb_error:
print(f"⚠️ Error updating ListenBrainz: {lb_error}")
import traceback
traceback.print_exc()
print(f"✅ ListenBrainz update complete (global): {lb_result.get('summary', {})}")
elif lb_result.get('error'):
print(f"⚠️ ListenBrainz update skipped: {lb_result.get('error')}")
except Exception as lb_error:
print(f"⚠️ Error updating ListenBrainz: {lb_error}")
import traceback
traceback.print_exc()
# Update current seasonal playlist (weekly refresh)
print("🎃 Starting seasonal content update...")
watchlist_scan_state['current_phase'] = 'updating_seasonal'
try:
from core.seasonal_discovery import get_seasonal_discovery_service
seasonal_service = get_seasonal_discovery_service(spotify_client, database)
# Update current seasonal playlist (weekly refresh)
print("🎃 Starting seasonal content update...")
watchlist_scan_state['current_phase'] = 'updating_seasonal'
try:
from core.seasonal_discovery import get_seasonal_discovery_service
seasonal_service = get_seasonal_discovery_service(spotify_client, database)
# Only update the current active season
current_season = seasonal_service.get_current_season()
if current_season:
if seasonal_service.should_populate_seasonal_content(current_season, days_threshold=7):
print(f"🎃 Updating {current_season} seasonal content...")
seasonal_service.populate_seasonal_content(current_season)
seasonal_service.curate_seasonal_playlist(current_season)
print(f"{current_season.capitalize()} seasonal content updated")
# Only update the current active season
current_season = seasonal_service.get_current_season()
if current_season:
if seasonal_service.should_populate_seasonal_content(current_season, days_threshold=7):
print(f"🎃 Updating {current_season} seasonal content...")
seasonal_service.populate_seasonal_content(current_season)
seasonal_service.curate_seasonal_playlist(current_season)
print(f"{current_season.capitalize()} seasonal content updated")
else:
print(f"⏭️ {current_season.capitalize()} seasonal content recently updated, skipping")
else:
print(f"⏭️ {current_season.capitalize()} seasonal content recently updated, skipping")
else:
print(" No active season at this time")
except Exception as seasonal_error:
print(f"⚠️ Error updating seasonal content: {seasonal_error}")
import traceback
traceback.print_exc()
print(" No active season at this time")
except Exception as seasonal_error:
print(f"⚠️ Error updating seasonal content: {seasonal_error}")
import traceback
traceback.print_exc()
# Sync Spotify library cache
print("📚 Syncing Spotify library cache...")
watchlist_scan_state['current_phase'] = 'syncing_spotify_library'
try:
scanner.sync_spotify_library_cache(profile_id=scan_profile_id)
print("✅ Spotify library cache sync complete")
except Exception as lib_error:
print(f"⚠️ Error syncing Spotify library: {lib_error}")
# Sync Spotify library cache
print("📚 Syncing Spotify library cache...")
watchlist_scan_state['current_phase'] = 'syncing_spotify_library'
try:
scanner.sync_spotify_library_cache(profile_id=scan_profile_id)
print("✅ Spotify library cache sync complete")
except Exception as lib_error:
print(f"⚠️ Error syncing Spotify library: {lib_error}")
except Exception as e:
print(f"Error during watchlist scan: {e}")
@ -30783,7 +30803,8 @@ def start_watchlist_scan():
'started_at': datetime.now(),
'results': [],
'summary': {},
'error': None
'error': None,
'cancel_requested': False
}
# Start scan in background
@ -30827,6 +30848,22 @@ def get_watchlist_scan_status():
print(f"Error getting watchlist scan status: {e}")
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 = {
'status': 'idle', # idle, running, completed, error
@ -31419,7 +31456,8 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
'recent_wishlist_additions': [],
'results': [],
'summary': {},
'error': None
'error': None,
'cancel_requested': False
}
scan_results = []
@ -31459,6 +31497,22 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
# Scan each artist with detailed tracking
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:
# Fetch artist image using provider-aware method
artist_image_url = scanner.get_artist_image_url(artist) or ''
@ -31665,158 +31719,166 @@ def _process_watchlist_scan_automatically(automation_id=None, profile_id=None):
})())
continue
# Update state with results
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_added_to_wishlist = sum(r.tracks_added_to_wishlist for r in successful_scans)
# 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]
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)
watchlist_scan_state['status'] = 'completed'
watchlist_scan_state['results'] = scan_results
watchlist_scan_state['completed_at'] = datetime.now()
watchlist_scan_state['summary'] = {
'total_artists': len(scan_results),
'successful_scans': len(successful_scans),
'new_tracks_found': total_new_tracks,
'tracks_added_to_wishlist': total_added_to_wishlist
}
watchlist_scan_state['status'] = 'completed'
watchlist_scan_state['results'] = scan_results
watchlist_scan_state['completed_at'] = datetime.now()
watchlist_scan_state['summary'] = {
'total_artists': len(scan_results),
'successful_scans': len(successful_scans),
'new_tracks_found': total_new_tracks,
'tracks_added_to_wishlist': total_added_to_wishlist
}
print(f"Automatic 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")
_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_type='success' if total_new_tracks > 0 else 'info')
print(f"Automatic 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")
_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_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")
# Populate discovery pool from similar artists (per-profile)
print("🎵 Starting discovery pool population...")
watchlist_scan_state['current_phase'] = 'populating_discovery_pool'
_update_automation_progress(automation_id, progress=96, phase='Populating discovery pool',
log_line='Building discovery pool from similar artists...', log_type='info')
try:
def _discovery_progress(event_type, message):
if event_type == 'artist':
_update_automation_progress(automation_id, phase=f'Discovery pool: {message}',
log_line=message, log_type='info',
current_item=message)
elif event_type == 'phase':
_update_automation_progress(automation_id, phase=message,
log_line=message, log_type='info')
elif event_type == 'success':
_update_automation_progress(automation_id,
log_line=message, log_type='success')
elif event_type == 'skip':
_update_automation_progress(automation_id,
log_line=message, log_type='info')
# Post-scan steps — skip if cancelled
if not was_cancelled:
# Populate discovery pool from similar artists (per-profile)
print("🎵 Starting discovery pool population...")
watchlist_scan_state['current_phase'] = 'populating_discovery_pool'
_update_automation_progress(automation_id, progress=96, phase='Populating discovery pool',
log_line='Building discovery pool from similar artists...', log_type='info')
try:
def _discovery_progress(event_type, message):
if event_type == 'artist':
_update_automation_progress(automation_id, phase=f'Discovery pool: {message}',
log_line=message, log_type='info',
current_item=message)
elif event_type == 'phase':
_update_automation_progress(automation_id, phase=message,
log_line=message, log_type='info')
elif event_type == 'success':
_update_automation_progress(automation_id,
log_line=message, log_type='success')
elif event_type == 'skip':
_update_automation_progress(automation_id,
log_line=message, log_type='info')
for p in all_profiles:
scanner.populate_discovery_pool(profile_id=p['id'], progress_callback=_discovery_progress)
print("✅ Discovery pool population complete")
except Exception as discovery_error:
print(f"⚠️ Error populating discovery pool: {discovery_error}")
import traceback
traceback.print_exc()
_update_automation_progress(automation_id,
log_line=f'Discovery pool error: {discovery_error}', log_type='error')
for p in all_profiles:
scanner.populate_discovery_pool(profile_id=p['id'], progress_callback=_discovery_progress)
print("✅ Discovery pool population complete")
except Exception as discovery_error:
print(f"⚠️ Error populating discovery pool: {discovery_error}")
import traceback
traceback.print_exc()
_update_automation_progress(automation_id,
log_line=f'Discovery pool error: {discovery_error}', log_type='error')
# Update ListenBrainz playlists cache
print("🧠 Starting ListenBrainz playlists update...")
watchlist_scan_state['current_phase'] = 'updating_listenbrainz'
_update_automation_progress(automation_id, progress=97, phase='Updating ListenBrainz',
log_line='Fetching ListenBrainz playlists...', log_type='info')
try:
from core.listenbrainz_manager import ListenBrainzManager
db = get_database()
db_path = str(db.database_path)
lb_profiles = db.get_profiles_with_listenbrainz()
if lb_profiles:
for lb_prof in lb_profiles:
lb_manager = ListenBrainzManager(db_path, profile_id=lb_prof['id'], token=lb_prof['token'], base_url=lb_prof['base_url'])
# Update ListenBrainz playlists cache
print("🧠 Starting ListenBrainz playlists update...")
watchlist_scan_state['current_phase'] = 'updating_listenbrainz'
_update_automation_progress(automation_id, progress=97, phase='Updating ListenBrainz',
log_line='Fetching ListenBrainz playlists...', log_type='info')
try:
from core.listenbrainz_manager import ListenBrainzManager
db = get_database()
db_path = str(db.database_path)
lb_profiles = db.get_profiles_with_listenbrainz()
if lb_profiles:
for lb_prof in lb_profiles:
lb_manager = ListenBrainzManager(db_path, profile_id=lb_prof['id'], token=lb_prof['token'], base_url=lb_prof['base_url'])
lb_result = lb_manager.update_all_playlists()
if lb_result.get('success'):
summary = lb_result.get('summary', {})
print(f"✅ ListenBrainz update complete for profile {lb_prof['id']}: {summary}")
_update_automation_progress(automation_id,
log_line=f'ListenBrainz (profile {lb_prof["id"]}): playlists updated', log_type='success')
else:
lb_manager = ListenBrainzManager(db_path)
lb_result = lb_manager.update_all_playlists()
if lb_result.get('success'):
summary = lb_result.get('summary', {})
print(f"✅ ListenBrainz update complete for profile {lb_prof['id']}: {summary}")
print(f"✅ ListenBrainz update complete (global): {summary}")
_update_automation_progress(automation_id,
log_line=f'ListenBrainz (profile {lb_prof["id"]}): playlists updated', log_type='success')
else:
lb_manager = ListenBrainzManager(db_path)
lb_result = lb_manager.update_all_playlists()
if lb_result.get('success'):
summary = lb_result.get('summary', {})
print(f"✅ ListenBrainz update complete (global): {summary}")
_update_automation_progress(automation_id,
log_line=f'ListenBrainz: playlists updated', log_type='success')
else:
print(f"⚠️ ListenBrainz update had issues: {lb_result.get('error', 'Unknown error')}")
_update_automation_progress(automation_id,
log_line=f'ListenBrainz: {lb_result.get("error", "Unknown error")}', log_type='error')
except Exception as lb_error:
print(f"⚠️ Error updating ListenBrainz: {lb_error}")
import traceback
traceback.print_exc()
_update_automation_progress(automation_id,
log_line=f'ListenBrainz error: {lb_error}', log_type='error')
# Update current seasonal playlist (weekly refresh)
print("🎃 Starting seasonal content update...")
watchlist_scan_state['current_phase'] = 'updating_seasonal'
_update_automation_progress(automation_id, progress=98, phase='Updating seasonal content',
log_line='Checking seasonal playlists...', log_type='info')
try:
from core.seasonal_discovery import get_seasonal_discovery_service
seasonal_service = get_seasonal_discovery_service(spotify_client, database)
# Only update the current active season
current_season = seasonal_service.get_current_season()
if current_season:
if seasonal_service.should_populate_seasonal_content(current_season, days_threshold=7):
print(f"🎃 Updating {current_season} seasonal content...")
_update_automation_progress(automation_id,
log_line=f'Updating {current_season} seasonal content...', log_type='info')
seasonal_service.populate_seasonal_content(current_season)
seasonal_service.curate_seasonal_playlist(current_season)
print(f"{current_season.capitalize()} seasonal content updated")
_update_automation_progress(automation_id,
log_line=f'{current_season.capitalize()} seasonal content updated', log_type='success')
else:
print(f"⏭️ {current_season.capitalize()} seasonal content recently updated, skipping")
_update_automation_progress(automation_id,
log_line=f'{current_season.capitalize()} seasonal content up to date', log_type='info')
else:
print(" No active season at this time")
log_line=f'ListenBrainz: playlists updated', log_type='success')
else:
print(f"⚠️ ListenBrainz update had issues: {lb_result.get('error', 'Unknown error')}")
_update_automation_progress(automation_id,
log_line=f'ListenBrainz: {lb_result.get("error", "Unknown error")}', log_type='error')
except Exception as lb_error:
print(f"⚠️ Error updating ListenBrainz: {lb_error}")
import traceback
traceback.print_exc()
_update_automation_progress(automation_id,
log_line='No active season', log_type='info')
except Exception as seasonal_error:
print(f"⚠️ Error updating seasonal content: {seasonal_error}")
import traceback
traceback.print_exc()
_update_automation_progress(automation_id,
log_line=f'Seasonal error: {seasonal_error}', log_type='error')
log_line=f'ListenBrainz error: {lb_error}', log_type='error')
# Sync Spotify library cache
print("📚 Syncing Spotify library cache...")
try:
for p in all_profiles:
scanner.sync_spotify_library_cache(profile_id=p['id'])
print("✅ Spotify library cache sync complete")
_update_automation_progress(automation_id,
log_line='Spotify library cache synced', log_type='info')
except Exception as lib_error:
print(f"⚠️ Error syncing Spotify library: {lib_error}")
_update_automation_progress(automation_id,
log_line=f'Library cache error: {lib_error}', log_type='error')
# Update current seasonal playlist (weekly refresh)
print("🎃 Starting seasonal content update...")
watchlist_scan_state['current_phase'] = 'updating_seasonal'
_update_automation_progress(automation_id, progress=98, phase='Updating seasonal content',
log_line='Checking seasonal playlists...', log_type='info')
try:
from core.seasonal_discovery import get_seasonal_discovery_service
seasonal_service = get_seasonal_discovery_service(spotify_client, database)
# Add activity for watchlist scan completion
if total_added_to_wishlist > 0:
add_activity_item("👁️", "Watchlist Scan Complete", f"{total_added_to_wishlist} new tracks added to wishlist", "Now")
# Only update the current active season
current_season = seasonal_service.get_current_season()
if current_season:
if seasonal_service.should_populate_seasonal_content(current_season, days_threshold=7):
print(f"🎃 Updating {current_season} seasonal content...")
_update_automation_progress(automation_id,
log_line=f'Updating {current_season} seasonal content...', log_type='info')
seasonal_service.populate_seasonal_content(current_season)
seasonal_service.curate_seasonal_playlist(current_season)
print(f"{current_season.capitalize()} seasonal content updated")
_update_automation_progress(automation_id,
log_line=f'{current_season.capitalize()} seasonal content updated', log_type='success')
else:
print(f"⏭️ {current_season.capitalize()} seasonal content recently updated, skipping")
_update_automation_progress(automation_id,
log_line=f'{current_season.capitalize()} seasonal content up to date', log_type='info')
else:
print(" No active season at this time")
_update_automation_progress(automation_id,
log_line='No active season', log_type='info')
except Exception as seasonal_error:
print(f"⚠️ Error updating seasonal content: {seasonal_error}")
import traceback
traceback.print_exc()
_update_automation_progress(automation_id,
log_line=f'Seasonal error: {seasonal_error}', log_type='error')
try:
if automation_engine:
automation_engine.emit('watchlist_scan_completed', {
'artists_scanned': str(len(scan_results)),
'new_tracks_found': str(total_new_tracks),
'tracks_added': str(total_added_to_wishlist),
})
except Exception:
pass
# Sync Spotify library cache
print("📚 Syncing Spotify library cache...")
try:
for p in all_profiles:
scanner.sync_spotify_library_cache(profile_id=p['id'])
print("✅ Spotify library cache sync complete")
_update_automation_progress(automation_id,
log_line='Spotify library cache synced', log_type='info')
except Exception as lib_error:
print(f"⚠️ Error syncing Spotify library: {lib_error}")
_update_automation_progress(automation_id,
log_line=f'Library cache error: {lib_error}', log_type='error')
# Add activity for watchlist scan completion
if total_added_to_wishlist > 0:
add_activity_item("👁️", "Watchlist Scan Complete", f"{total_added_to_wishlist} new tracks added to wishlist", "Now")
try:
if automation_engine:
automation_engine.emit('watchlist_scan_completed', {
'artists_scanned': str(len(scan_results)),
'new_tracks_found': str(total_new_tracks),
'tracks_added': str(total_added_to_wishlist),
})
except Exception:
pass
except Exception as e:
print(f"❌ Error in automatic watchlist scan: {e}")

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>
${scanStatus === 'scanning' ? 'Scanning...' : 'Scan for New Releases'}
</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"
id="update-similar-artists-btn"
onclick="updateSimilarArtists()"
@ -34899,6 +34906,37 @@ function filterWatchlistArtists() {
/**
* 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() {
try {
const button = document.getElementById('scan-watchlist-btn');
@ -34918,6 +34956,14 @@ async function startWatchlistScan() {
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
const statusDiv = document.getElementById('watchlist-scan-status');
if (statusDiv) {
@ -34944,6 +34990,12 @@ function handleWatchlistScanData(data) {
const button = document.getElementById('scan-watchlist-btn');
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
if (liveActivity && data.status === 'scanning') {
liveActivity.style.display = 'flex';
@ -35049,6 +35101,53 @@ function handleWatchlistScanData(data) {
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') {
if (button) {
button.disabled = false;
@ -35056,6 +35155,12 @@ function handleWatchlistScanData(data) {
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
if (liveActivity) {
liveActivity.style.display = 'none';
@ -35073,7 +35178,7 @@ async function pollWatchlistScanStatus() {
if (data.success) {
handleWatchlistScanData(data);
if (data.status === 'completed' || data.status === 'error') {
if (data.status === 'completed' || data.status === 'error' || data.status === 'cancelled') {
return; // Stop polling
}
}