Add cancel button for watchlist scans (manual and automation-triggered)
This commit is contained in:
parent
c90fff37f1
commit
7871f4581c
2 changed files with 393 additions and 226 deletions
506
web_server.py
506
web_server.py
|
|
@ -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}")
|
||||||
|
|
|
||||||
|
|
@ -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"> • </span>
|
||||||
|
<span class="sync-stat">New tracks: ${newTracks}</span>
|
||||||
|
<span class="sync-separator"> • </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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue