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