Add artist watchlist feature with UI and API endpoints
Introduces a watchlist system for tracking artists, including backend API endpoints for managing the watchlist and scanning for new releases. Updates the frontend to allow users to add/remove artists to/from the watchlist from both artist cards and detail pages, view and scan their watchlist via a modal, and see real-time scan progress. Adds related styles for watchlist UI components.
This commit is contained in:
parent
42002d1b45
commit
6011c50304
4 changed files with 1213 additions and 10 deletions
424
web_server.py
424
web_server.py
|
|
@ -4969,6 +4969,15 @@ def _process_wishlist_automatically():
|
||||||
schedule_next_wishlist_processing()
|
schedule_next_wishlist_processing()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Check if watchlist scan is currently running
|
||||||
|
global watchlist_scan_state
|
||||||
|
if (watchlist_scan_state and
|
||||||
|
isinstance(watchlist_scan_state, dict) and
|
||||||
|
watchlist_scan_state.get('status') == 'scanning'):
|
||||||
|
print("👁️ Watchlist scan in progress, skipping automatic wishlist processing to avoid conflicts.")
|
||||||
|
schedule_next_wishlist_processing()
|
||||||
|
return
|
||||||
|
|
||||||
wishlist_auto_processing = True
|
wishlist_auto_processing = True
|
||||||
|
|
||||||
# Use app context for database operations
|
# Use app context for database operations
|
||||||
|
|
@ -9733,6 +9742,416 @@ def hydrate_artist_bubbles():
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
# --- Watchlist API Endpoints ---
|
||||||
|
|
||||||
|
@app.route('/api/watchlist/count', methods=['GET'])
|
||||||
|
def get_watchlist_count():
|
||||||
|
"""Get the number of artists in the watchlist"""
|
||||||
|
try:
|
||||||
|
database = get_database()
|
||||||
|
count = database.get_watchlist_count()
|
||||||
|
return jsonify({"success": True, "count": count})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting watchlist count: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/watchlist/artists', methods=['GET'])
|
||||||
|
def get_watchlist_artists():
|
||||||
|
"""Get all artists in the watchlist"""
|
||||||
|
try:
|
||||||
|
database = get_database()
|
||||||
|
watchlist_artists = database.get_watchlist_artists()
|
||||||
|
|
||||||
|
# Convert to JSON serializable format
|
||||||
|
artists_data = []
|
||||||
|
for artist in watchlist_artists:
|
||||||
|
artists_data.append({
|
||||||
|
"id": artist.id,
|
||||||
|
"spotify_artist_id": artist.spotify_artist_id,
|
||||||
|
"artist_name": artist.artist_name,
|
||||||
|
"date_added": artist.date_added.isoformat() if artist.date_added else None,
|
||||||
|
"last_scan_timestamp": artist.last_scan_timestamp.isoformat() if artist.last_scan_timestamp else None,
|
||||||
|
"created_at": artist.created_at.isoformat() if artist.created_at else None,
|
||||||
|
"updated_at": artist.updated_at.isoformat() if artist.updated_at else None
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({"success": True, "artists": artists_data})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting watchlist artists: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/watchlist/add', methods=['POST'])
|
||||||
|
def add_to_watchlist():
|
||||||
|
"""Add an artist to the watchlist"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
artist_id = data.get('artist_id')
|
||||||
|
artist_name = data.get('artist_name')
|
||||||
|
|
||||||
|
if not artist_id or not artist_name:
|
||||||
|
return jsonify({"success": False, "error": "Missing artist_id or artist_name"}), 400
|
||||||
|
|
||||||
|
database = get_database()
|
||||||
|
success = database.add_artist_to_watchlist(artist_id, artist_name)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True, "message": f"Added {artist_name} to watchlist"})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "error": "Failed to add artist to watchlist"}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error adding to watchlist: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/watchlist/remove', methods=['POST'])
|
||||||
|
def remove_from_watchlist():
|
||||||
|
"""Remove an artist from the watchlist"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
artist_id = data.get('artist_id')
|
||||||
|
|
||||||
|
if not artist_id:
|
||||||
|
return jsonify({"success": False, "error": "Missing artist_id"}), 400
|
||||||
|
|
||||||
|
database = get_database()
|
||||||
|
success = database.remove_artist_from_watchlist(artist_id)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True, "message": "Removed artist from watchlist"})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "error": "Failed to remove artist from watchlist"}), 500
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error removing from watchlist: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/watchlist/check', methods=['POST'])
|
||||||
|
def check_watchlist_status():
|
||||||
|
"""Check if an artist is in the watchlist"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
artist_id = data.get('artist_id')
|
||||||
|
|
||||||
|
if not artist_id:
|
||||||
|
return jsonify({"success": False, "error": "Missing artist_id"}), 400
|
||||||
|
|
||||||
|
database = get_database()
|
||||||
|
is_watching = database.is_artist_in_watchlist(artist_id)
|
||||||
|
|
||||||
|
return jsonify({"success": True, "is_watching": is_watching})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error checking watchlist status: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/watchlist/scan', methods=['POST'])
|
||||||
|
def start_watchlist_scan():
|
||||||
|
"""Start a watchlist scan for new releases"""
|
||||||
|
try:
|
||||||
|
if not spotify_client or not spotify_client.is_authenticated():
|
||||||
|
return jsonify({"success": False, "error": "Spotify client not available or not authenticated"}), 400
|
||||||
|
|
||||||
|
# Check if wishlist auto-processing is currently running
|
||||||
|
global wishlist_auto_processing
|
||||||
|
if wishlist_auto_processing:
|
||||||
|
return jsonify({"success": False, "error": "Wishlist auto-processing is currently running. Please wait for it to complete before starting a watchlist scan."}), 409
|
||||||
|
|
||||||
|
# Start the scan in a background thread
|
||||||
|
def run_scan():
|
||||||
|
try:
|
||||||
|
global watchlist_scan_state
|
||||||
|
from core.watchlist_scanner import get_watchlist_scanner
|
||||||
|
from database.music_database import get_database
|
||||||
|
|
||||||
|
# Get list of artists to scan
|
||||||
|
database = get_database()
|
||||||
|
watchlist_artists = database.get_watchlist_artists()
|
||||||
|
|
||||||
|
if not watchlist_artists:
|
||||||
|
watchlist_scan_state['status'] = 'completed'
|
||||||
|
watchlist_scan_state['summary'] = {
|
||||||
|
'total_artists': 0,
|
||||||
|
'successful_scans': 0,
|
||||||
|
'new_tracks_found': 0,
|
||||||
|
'tracks_added_to_wishlist': 0
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
scanner = get_watchlist_scanner(spotify_client)
|
||||||
|
|
||||||
|
# Initialize detailed progress tracking
|
||||||
|
watchlist_scan_state.update({
|
||||||
|
'total_artists': len(watchlist_artists),
|
||||||
|
'current_artist_index': 0,
|
||||||
|
'current_artist_name': '',
|
||||||
|
'current_phase': 'starting',
|
||||||
|
'albums_to_check': 0,
|
||||||
|
'albums_checked': 0,
|
||||||
|
'current_album': '',
|
||||||
|
'tracks_found_this_scan': 0,
|
||||||
|
'tracks_added_this_scan': 0
|
||||||
|
})
|
||||||
|
|
||||||
|
scan_results = []
|
||||||
|
|
||||||
|
for i, artist in enumerate(watchlist_artists):
|
||||||
|
try:
|
||||||
|
# Update progress
|
||||||
|
watchlist_scan_state.update({
|
||||||
|
'current_artist_index': i + 1,
|
||||||
|
'current_artist_name': artist.artist_name,
|
||||||
|
'current_phase': 'fetching_discography',
|
||||||
|
'albums_to_check': 0,
|
||||||
|
'albums_checked': 0,
|
||||||
|
'current_album': ''
|
||||||
|
})
|
||||||
|
|
||||||
|
# Get artist discography
|
||||||
|
albums = scanner.get_artist_discography(artist.spotify_artist_id, artist.last_scan_timestamp)
|
||||||
|
|
||||||
|
if albums is None:
|
||||||
|
scan_results.append(type('ScanResult', (), {
|
||||||
|
'artist_name': artist.artist_name,
|
||||||
|
'spotify_artist_id': artist.spotify_artist_id,
|
||||||
|
'albums_checked': 0,
|
||||||
|
'new_tracks_found': 0,
|
||||||
|
'tracks_added_to_wishlist': 0,
|
||||||
|
'success': False,
|
||||||
|
'error_message': "Failed to get artist discography"
|
||||||
|
})())
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Update with album count
|
||||||
|
watchlist_scan_state.update({
|
||||||
|
'current_phase': 'checking_albums',
|
||||||
|
'albums_to_check': len(albums),
|
||||||
|
'albums_checked': 0
|
||||||
|
})
|
||||||
|
|
||||||
|
# Track progress for this artist
|
||||||
|
artist_new_tracks = 0
|
||||||
|
artist_added_tracks = 0
|
||||||
|
|
||||||
|
# Scan each album
|
||||||
|
for album_index, album in enumerate(albums):
|
||||||
|
watchlist_scan_state.update({
|
||||||
|
'albums_checked': album_index + 1,
|
||||||
|
'current_album': album.name,
|
||||||
|
'current_phase': f'checking_album_{album_index + 1}_of_{len(albums)}'
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get album tracks
|
||||||
|
album_data = scanner.spotify_client.get_album(album.id)
|
||||||
|
if not album_data or 'tracks' not in album_data:
|
||||||
|
continue
|
||||||
|
|
||||||
|
tracks = album_data['tracks']['items']
|
||||||
|
|
||||||
|
# Check each track
|
||||||
|
for track in tracks:
|
||||||
|
if scanner.is_track_missing_from_library(track):
|
||||||
|
artist_new_tracks += 1
|
||||||
|
watchlist_scan_state['tracks_found_this_scan'] += 1
|
||||||
|
|
||||||
|
# Add to wishlist
|
||||||
|
if scanner.add_track_to_wishlist(track, album_data, artist):
|
||||||
|
artist_added_tracks += 1
|
||||||
|
watchlist_scan_state['tracks_added_this_scan'] += 1
|
||||||
|
|
||||||
|
# Small delay between albums
|
||||||
|
import time
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error checking album {album.name}: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Update scan timestamp
|
||||||
|
scanner.update_artist_scan_timestamp(artist.spotify_artist_id)
|
||||||
|
|
||||||
|
# Store result
|
||||||
|
scan_results.append(type('ScanResult', (), {
|
||||||
|
'artist_name': artist.artist_name,
|
||||||
|
'spotify_artist_id': artist.spotify_artist_id,
|
||||||
|
'albums_checked': len(albums),
|
||||||
|
'new_tracks_found': artist_new_tracks,
|
||||||
|
'tracks_added_to_wishlist': artist_added_tracks,
|
||||||
|
'success': True,
|
||||||
|
'error_message': None
|
||||||
|
})())
|
||||||
|
|
||||||
|
print(f"✅ Scanned {artist.artist_name}: {artist_new_tracks} new tracks found, {artist_added_tracks} added to wishlist")
|
||||||
|
|
||||||
|
# Delay between artists
|
||||||
|
if i < len(watchlist_artists) - 1:
|
||||||
|
watchlist_scan_state['current_phase'] = 'rate_limiting'
|
||||||
|
time.sleep(2.0)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error scanning artist {artist.artist_name}: {e}")
|
||||||
|
scan_results.append(type('ScanResult', (), {
|
||||||
|
'artist_name': artist.artist_name,
|
||||||
|
'spotify_artist_id': artist.spotify_artist_id,
|
||||||
|
'albums_checked': 0,
|
||||||
|
'new_tracks_found': 0,
|
||||||
|
'tracks_added_to_wishlist': 0,
|
||||||
|
'success': False,
|
||||||
|
'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")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during watchlist scan: {e}")
|
||||||
|
watchlist_scan_state['status'] = 'error'
|
||||||
|
watchlist_scan_state['error'] = str(e)
|
||||||
|
|
||||||
|
# Initialize scan state
|
||||||
|
global watchlist_scan_state
|
||||||
|
watchlist_scan_state = {
|
||||||
|
'status': 'scanning',
|
||||||
|
'started_at': datetime.now(),
|
||||||
|
'results': [],
|
||||||
|
'summary': {},
|
||||||
|
'error': None
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start scan in background
|
||||||
|
thread = threading.Thread(target=run_scan)
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return jsonify({"success": True, "message": "Watchlist scan started"})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error starting watchlist scan: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/watchlist/scan/status', methods=['GET'])
|
||||||
|
def get_watchlist_scan_status():
|
||||||
|
"""Get the current status of watchlist scanning"""
|
||||||
|
try:
|
||||||
|
global watchlist_scan_state
|
||||||
|
if 'watchlist_scan_state' not in globals():
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"status": "idle",
|
||||||
|
"summary": {}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Convert datetime objects to ISO format for JSON serialization
|
||||||
|
state = watchlist_scan_state.copy()
|
||||||
|
if 'started_at' in state and state['started_at']:
|
||||||
|
state['started_at'] = state['started_at'].isoformat()
|
||||||
|
if 'completed_at' in state and state['completed_at']:
|
||||||
|
state['completed_at'] = state['completed_at'].isoformat()
|
||||||
|
|
||||||
|
return jsonify({"success": True, **state})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error getting watchlist scan status: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
# --- Watchlist Auto-Scanning System ---
|
||||||
|
|
||||||
|
watchlist_scan_state = {
|
||||||
|
'status': 'idle',
|
||||||
|
'results': [],
|
||||||
|
'summary': {},
|
||||||
|
'error': None
|
||||||
|
}
|
||||||
|
|
||||||
|
def start_watchlist_auto_scanning():
|
||||||
|
"""Start automatic daily watchlist scanning"""
|
||||||
|
def daily_scan():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Wait 24 hours (86400 seconds)
|
||||||
|
time.sleep(86400)
|
||||||
|
|
||||||
|
# Check if we have artists to scan and Spotify client is available
|
||||||
|
database = get_database()
|
||||||
|
watchlist_count = database.get_watchlist_count()
|
||||||
|
|
||||||
|
if watchlist_count > 0 and spotify_client and spotify_client.is_authenticated():
|
||||||
|
# Check if wishlist auto-processing is currently running
|
||||||
|
global wishlist_auto_processing
|
||||||
|
if wishlist_auto_processing:
|
||||||
|
print("👁️ Skipping automatic daily watchlist scan: wishlist auto-processing is currently running")
|
||||||
|
continue # Skip this cycle, will try again in 24 hours
|
||||||
|
|
||||||
|
print(f"Starting automatic daily watchlist scan for {watchlist_count} artists...")
|
||||||
|
|
||||||
|
# Update global scan state
|
||||||
|
global watchlist_scan_state
|
||||||
|
watchlist_scan_state = {
|
||||||
|
'status': 'scanning',
|
||||||
|
'started_at': datetime.now(),
|
||||||
|
'results': [],
|
||||||
|
'summary': {},
|
||||||
|
'error': None
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run the scan
|
||||||
|
from core.watchlist_scanner import get_watchlist_scanner
|
||||||
|
scanner = get_watchlist_scanner(spotify_client)
|
||||||
|
results = scanner.scan_all_watchlist_artists()
|
||||||
|
|
||||||
|
# Update state with results
|
||||||
|
watchlist_scan_state['status'] = 'completed'
|
||||||
|
watchlist_scan_state['results'] = results
|
||||||
|
watchlist_scan_state['completed_at'] = datetime.now()
|
||||||
|
|
||||||
|
# Calculate summary
|
||||||
|
successful_scans = [r for r in 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(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(results)} artists scanned successfully")
|
||||||
|
print(f"Found {total_new_tracks} new tracks, added {total_added_to_wishlist} to wishlist")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Skipping automatic watchlist scan: no artists in watchlist or Spotify client unavailable")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during automatic watchlist scan: {e}")
|
||||||
|
if 'watchlist_scan_state' in globals():
|
||||||
|
watchlist_scan_state['status'] = 'error'
|
||||||
|
watchlist_scan_state['error'] = str(e)
|
||||||
|
|
||||||
|
# Start the daily scanning thread
|
||||||
|
thread = threading.Thread(target=daily_scan)
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
|
print("✅ Automatic daily watchlist scanning started")
|
||||||
|
|
||||||
# --- Main Execution ---
|
# --- Main Execution ---
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
@ -9749,4 +10168,9 @@ if __name__ == '__main__':
|
||||||
start_wishlist_auto_processing()
|
start_wishlist_auto_processing()
|
||||||
print("✅ Automatic wishlist processing started")
|
print("✅ Automatic wishlist processing started")
|
||||||
|
|
||||||
|
# Start automatic watchlist scanning when server starts
|
||||||
|
print("🔧 Starting automatic watchlist scanning...")
|
||||||
|
start_watchlist_auto_scanning()
|
||||||
|
print("✅ Automatic watchlist scanning started")
|
||||||
|
|
||||||
app.run(host='0.0.0.0', port=5001, debug=True)
|
app.run(host='0.0.0.0', port=5001, debug=True)
|
||||||
|
|
|
||||||
|
|
@ -577,18 +577,25 @@
|
||||||
<!-- Artist Detail State -->
|
<!-- Artist Detail State -->
|
||||||
<div class="artist-detail-state hidden" id="artist-detail-state">
|
<div class="artist-detail-state hidden" id="artist-detail-state">
|
||||||
<div class="artist-detail-header">
|
<div class="artist-detail-header">
|
||||||
<button class="artist-detail-back-button" id="artist-detail-back-button">
|
<div class="artist-detail-header-left">
|
||||||
<span class="back-icon">←</span>
|
<button class="artist-detail-back-button" id="artist-detail-back-button">
|
||||||
<span>Back to Results</span>
|
<span class="back-icon">←</span>
|
||||||
</button>
|
<span>Back to Results</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="artist-detail-info">
|
<div class="artist-detail-info">
|
||||||
<div class="artist-detail-image" id="artist-detail-image"></div>
|
<div class="artist-detail-image" id="artist-detail-image"></div>
|
||||||
<div class="artist-detail-text">
|
<div class="artist-detail-text">
|
||||||
<h2 class="artist-detail-name" id="artist-detail-name">Artist Name</h2>
|
<h2 class="artist-detail-name" id="artist-detail-name">Artist Name</h2>
|
||||||
<p class="artist-detail-genres" id="artist-detail-genres">Genres</p>
|
<p class="artist-detail-genres" id="artist-detail-genres">Genres</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button class="artist-detail-watchlist-btn" id="artist-detail-watchlist-btn">
|
||||||
|
<span class="watchlist-icon">👁️</span>
|
||||||
|
<span class="watchlist-text">Add to Watchlist</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="artist-detail-content">
|
<div class="artist-detail-content">
|
||||||
|
|
|
||||||
|
|
@ -284,6 +284,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
initializeMediaPlayer();
|
initializeMediaPlayer();
|
||||||
initializeDonationWidget();
|
initializeDonationWidget();
|
||||||
initializeSyncPage();
|
initializeSyncPage();
|
||||||
|
initializeWatchlist();
|
||||||
|
|
||||||
|
|
||||||
// Start periodic updates
|
// Start periodic updates
|
||||||
|
|
@ -325,6 +326,22 @@ function initializeNavigation() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initializeWatchlist() {
|
||||||
|
// Add watchlist button click handler
|
||||||
|
const watchlistButton = document.getElementById('watchlist-button');
|
||||||
|
if (watchlistButton) {
|
||||||
|
watchlistButton.addEventListener('click', showWatchlistModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update watchlist count initially
|
||||||
|
updateWatchlistButtonCount();
|
||||||
|
|
||||||
|
// Update count every 30 seconds
|
||||||
|
setInterval(updateWatchlistButtonCount, 30000);
|
||||||
|
|
||||||
|
console.log('Watchlist system initialized');
|
||||||
|
}
|
||||||
|
|
||||||
function navigateToPage(pageId) {
|
function navigateToPage(pageId) {
|
||||||
if (pageId === currentPage) return;
|
if (pageId === currentPage) return;
|
||||||
|
|
||||||
|
|
@ -10031,6 +10048,9 @@ function displayArtistsResults(query, results) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update watchlist status for all cards
|
||||||
|
updateArtistCardWatchlistStatus();
|
||||||
|
|
||||||
// Add mouse wheel horizontal scrolling
|
// Add mouse wheel horizontal scrolling
|
||||||
container.addEventListener('wheel', (event) => {
|
container.addEventListener('wheel', (event) => {
|
||||||
if (event.deltaY !== 0) {
|
if (event.deltaY !== 0) {
|
||||||
|
|
@ -10068,6 +10088,12 @@ function createArtistCard(artist) {
|
||||||
<span class="popularity-icon">🔥</span>
|
<span class="popularity-icon">🔥</span>
|
||||||
<span>${popularityText}</span>
|
<span>${popularityText}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="artist-card-actions">
|
||||||
|
<button class="watchlist-toggle-btn" data-artist-id="${artist.id}" data-artist-name="${escapeHtml(artist.name)}" onclick="toggleWatchlist(event, '${artist.id}', '${escapeHtml(artist.name)}')">
|
||||||
|
<span class="watchlist-icon">👁️</span>
|
||||||
|
<span class="watchlist-text">Add to Watchlist</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
@ -10678,6 +10704,140 @@ function updateArtistDetailHeader(artist) {
|
||||||
const genres = artist.genres?.slice(0, 4).join(' • ') || 'Various genres';
|
const genres = artist.genres?.slice(0, 4).join(' • ') || 'Various genres';
|
||||||
genresElement.textContent = genres;
|
genresElement.textContent = genres;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize watchlist button
|
||||||
|
initializeArtistDetailWatchlistButton(artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize watchlist button for artist detail page
|
||||||
|
*/
|
||||||
|
async function initializeArtistDetailWatchlistButton(artist) {
|
||||||
|
const button = document.getElementById('artist-detail-watchlist-btn');
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
// Set up click handler
|
||||||
|
button.onclick = (event) => toggleArtistDetailWatchlist(event, artist.id, artist.name);
|
||||||
|
|
||||||
|
// Check and update current status
|
||||||
|
await updateArtistDetailWatchlistButton(artist.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle watchlist status for artist detail page
|
||||||
|
*/
|
||||||
|
async function toggleArtistDetailWatchlist(event, artistId, artistName) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const button = document.getElementById('artist-detail-watchlist-btn');
|
||||||
|
const icon = button.querySelector('.watchlist-icon');
|
||||||
|
const text = button.querySelector('.watchlist-text');
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
const originalText = text.textContent;
|
||||||
|
text.textContent = 'Loading...';
|
||||||
|
button.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check current status
|
||||||
|
const checkResponse = await fetch('/api/watchlist/check', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ artist_id: artistId })
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkData = await checkResponse.json();
|
||||||
|
if (!checkData.success) {
|
||||||
|
throw new Error(checkData.error || 'Failed to check watchlist status');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isWatching = checkData.is_watching;
|
||||||
|
|
||||||
|
// Toggle watchlist status
|
||||||
|
const endpoint = isWatching ? '/api/watchlist/remove' : '/api/watchlist/add';
|
||||||
|
const payload = isWatching ?
|
||||||
|
{ artist_id: artistId } :
|
||||||
|
{ artist_id: artistId, artist_name: artistName };
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.error || 'Failed to update watchlist');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update button appearance
|
||||||
|
if (isWatching) {
|
||||||
|
// Was watching, now removed
|
||||||
|
icon.textContent = '👁️';
|
||||||
|
text.textContent = 'Add to Watchlist';
|
||||||
|
button.classList.remove('watching');
|
||||||
|
console.log(`❌ Removed ${artistName} from watchlist`);
|
||||||
|
} else {
|
||||||
|
// Was not watching, now added
|
||||||
|
icon.textContent = '👁️';
|
||||||
|
text.textContent = 'Remove from Watchlist';
|
||||||
|
button.classList.add('watching');
|
||||||
|
console.log(`✅ Added ${artistName} to watchlist`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update dashboard watchlist count
|
||||||
|
updateWatchlistButtonCount();
|
||||||
|
|
||||||
|
// Update any visible artist cards
|
||||||
|
updateArtistCardWatchlistStatus();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling watchlist:', error);
|
||||||
|
text.textContent = originalText;
|
||||||
|
|
||||||
|
// Show error feedback
|
||||||
|
const originalBackground = button.style.background;
|
||||||
|
button.style.background = 'rgba(255, 59, 48, 0.3)';
|
||||||
|
setTimeout(() => {
|
||||||
|
button.style.background = originalBackground;
|
||||||
|
}, 2000);
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update artist detail watchlist button status
|
||||||
|
*/
|
||||||
|
async function updateArtistDetailWatchlistButton(artistId) {
|
||||||
|
const button = document.getElementById('artist-detail-watchlist-btn');
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/watchlist/check', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ artist_id: artistId })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
const icon = button.querySelector('.watchlist-icon');
|
||||||
|
const text = button.querySelector('.watchlist-text');
|
||||||
|
|
||||||
|
if (data.is_watching) {
|
||||||
|
icon.textContent = '👁️';
|
||||||
|
text.textContent = 'Remove from Watchlist';
|
||||||
|
button.classList.add('watching');
|
||||||
|
} else {
|
||||||
|
icon.textContent = '👁️';
|
||||||
|
text.textContent = 'Add to Watchlist';
|
||||||
|
button.classList.remove('watching');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking watchlist status:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -11820,6 +11980,447 @@ function escapeHtml(text) {
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Watchlist Functions ---
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle an artist's watchlist status
|
||||||
|
*/
|
||||||
|
async function toggleWatchlist(event, artistId, artistName) {
|
||||||
|
// Prevent event bubbling to parent card
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const button = event.currentTarget;
|
||||||
|
const icon = button.querySelector('.watchlist-icon');
|
||||||
|
const text = button.querySelector('.watchlist-text');
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
const originalText = text.textContent;
|
||||||
|
text.textContent = 'Loading...';
|
||||||
|
button.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check current status
|
||||||
|
const checkResponse = await fetch('/api/watchlist/check', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ artist_id: artistId })
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkData = await checkResponse.json();
|
||||||
|
if (!checkData.success) {
|
||||||
|
throw new Error(checkData.error || 'Failed to check watchlist status');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isWatching = checkData.is_watching;
|
||||||
|
|
||||||
|
// Toggle watchlist status
|
||||||
|
const endpoint = isWatching ? '/api/watchlist/remove' : '/api/watchlist/add';
|
||||||
|
const payload = isWatching ?
|
||||||
|
{ artist_id: artistId } :
|
||||||
|
{ artist_id: artistId, artist_name: artistName };
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.error || 'Failed to update watchlist');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update button appearance
|
||||||
|
if (isWatching) {
|
||||||
|
// Was watching, now removed
|
||||||
|
icon.textContent = '👁️';
|
||||||
|
text.textContent = 'Add to Watchlist';
|
||||||
|
button.classList.remove('watching');
|
||||||
|
console.log(`❌ Removed ${artistName} from watchlist`);
|
||||||
|
} else {
|
||||||
|
// Was not watching, now added
|
||||||
|
icon.textContent = '👁️';
|
||||||
|
text.textContent = 'Watching...';
|
||||||
|
button.classList.add('watching');
|
||||||
|
console.log(`✅ Added ${artistName} to watchlist`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update dashboard watchlist count
|
||||||
|
updateWatchlistButtonCount();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling watchlist:', error);
|
||||||
|
text.textContent = originalText;
|
||||||
|
|
||||||
|
// Show error feedback
|
||||||
|
const originalBackground = button.style.background;
|
||||||
|
button.style.background = 'rgba(255, 59, 48, 0.3)';
|
||||||
|
setTimeout(() => {
|
||||||
|
button.style.background = originalBackground;
|
||||||
|
}, 2000);
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the watchlist button count on dashboard
|
||||||
|
*/
|
||||||
|
async function updateWatchlistButtonCount() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/watchlist/count');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const watchlistButton = document.getElementById('watchlist-button');
|
||||||
|
if (watchlistButton) {
|
||||||
|
watchlistButton.textContent = `👁️ Watchlist (${data.count})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating watchlist count:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and update watchlist status for all visible artist cards
|
||||||
|
*/
|
||||||
|
async function updateArtistCardWatchlistStatus() {
|
||||||
|
const artistCards = document.querySelectorAll('.artist-card');
|
||||||
|
|
||||||
|
for (const card of artistCards) {
|
||||||
|
const artistId = card.dataset.artistId;
|
||||||
|
if (!artistId) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/watchlist/check', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ artist_id: artistId })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success) {
|
||||||
|
const button = card.querySelector('.watchlist-toggle-btn');
|
||||||
|
const icon = button.querySelector('.watchlist-icon');
|
||||||
|
const text = button.querySelector('.watchlist-text');
|
||||||
|
|
||||||
|
if (data.is_watching) {
|
||||||
|
icon.textContent = '👁️';
|
||||||
|
text.textContent = 'Watching...';
|
||||||
|
button.classList.add('watching');
|
||||||
|
} else {
|
||||||
|
icon.textContent = '👁️';
|
||||||
|
text.textContent = 'Add to Watchlist';
|
||||||
|
button.classList.remove('watching');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error checking watchlist status for artist ${artistId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show watchlist modal
|
||||||
|
*/
|
||||||
|
async function showWatchlistModal() {
|
||||||
|
try {
|
||||||
|
// Check if watchlist has any artists
|
||||||
|
const countResponse = await fetch('/api/watchlist/count');
|
||||||
|
const countData = await countResponse.json();
|
||||||
|
|
||||||
|
if (!countData.success) {
|
||||||
|
console.error('Error getting watchlist count:', countData.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (countData.count === 0) {
|
||||||
|
// Show empty state message
|
||||||
|
alert('Your watchlist is empty!\n\nAdd artists to your watchlist from the Artists page to monitor them for new releases.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get watchlist artists
|
||||||
|
const artistsResponse = await fetch('/api/watchlist/artists');
|
||||||
|
const artistsData = await artistsResponse.json();
|
||||||
|
|
||||||
|
if (!artistsData.success) {
|
||||||
|
console.error('Error getting watchlist artists:', artistsData.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create modal if it doesn't exist
|
||||||
|
let modal = document.getElementById('watchlist-modal');
|
||||||
|
if (!modal) {
|
||||||
|
modal = document.createElement('div');
|
||||||
|
modal.id = 'watchlist-modal';
|
||||||
|
modal.className = 'modal-overlay';
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get scan status
|
||||||
|
const statusResponse = await fetch('/api/watchlist/scan/status');
|
||||||
|
const statusData = await statusResponse.json();
|
||||||
|
const scanStatus = statusData.success ? statusData.status : 'idle';
|
||||||
|
|
||||||
|
// Build modal content
|
||||||
|
modal.innerHTML = `
|
||||||
|
<div class="modal-container playlist-modal">
|
||||||
|
<div class="playlist-modal-header">
|
||||||
|
<div class="playlist-header-content">
|
||||||
|
<h2>👁️ Watchlist</h2>
|
||||||
|
<div class="playlist-quick-info">
|
||||||
|
<span class="playlist-track-count">${countData.count} artist${countData.count !== 1 ? 's' : ''}</span>
|
||||||
|
</div>
|
||||||
|
<div class="playlist-modal-sync-status" id="watchlist-scan-status" style="display: ${scanStatus !== 'idle' ? 'block' : 'none'};">
|
||||||
|
<div class="scan-status-main">
|
||||||
|
<span class="sync-stat"><span id="scan-status-text">${scanStatus}</span></span>
|
||||||
|
</div>
|
||||||
|
${statusData.summary ? `
|
||||||
|
<div class="scan-status-summary" style="margin-top: 8px; font-size: 13px; opacity: 0.8;">
|
||||||
|
<span class="sync-stat">Artists: ${statusData.summary.total_artists || 0}</span>
|
||||||
|
<span class="sync-separator"> • </span>
|
||||||
|
<span class="sync-stat">New tracks: ${statusData.summary.new_tracks_found || 0}</span>
|
||||||
|
<span class="sync-separator"> • </span>
|
||||||
|
<span class="sync-stat">Added to wishlist: ${statusData.summary.tracks_added_to_wishlist || 0}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span class="playlist-modal-close" onclick="closeWatchlistModal()">×</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="playlist-modal-body">
|
||||||
|
<div class="watchlist-actions" style="margin-bottom: 20px;">
|
||||||
|
<button class="playlist-modal-btn playlist-modal-btn-primary"
|
||||||
|
id="scan-watchlist-btn"
|
||||||
|
onclick="startWatchlistScan()"
|
||||||
|
${scanStatus === 'scanning' ? 'disabled' : ''}>
|
||||||
|
${scanStatus === 'scanning' ? 'Scanning...' : 'Scan for New Releases'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="watchlist-artists-list">
|
||||||
|
${artistsData.artists.map(artist => `
|
||||||
|
<div class="watchlist-artist-item">
|
||||||
|
<div class="watchlist-artist-info">
|
||||||
|
<span class="watchlist-artist-name">${escapeHtml(artist.artist_name)}</span>
|
||||||
|
<span class="watchlist-artist-date">Added ${new Date(artist.date_added).toLocaleDateString()}</span>
|
||||||
|
${artist.last_scan_timestamp ? `
|
||||||
|
<span class="watchlist-artist-scan">Last scanned ${new Date(artist.last_scan_timestamp).toLocaleDateString()}</span>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
<button class="playlist-modal-btn playlist-modal-btn-secondary"
|
||||||
|
onclick="removeFromWatchlistModal('${artist.spotify_artist_id}', '${escapeHtml(artist.artist_name)}')">
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="playlist-modal-footer">
|
||||||
|
<button class="playlist-modal-btn playlist-modal-btn-secondary" onclick="closeWatchlistModal()">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
|
||||||
|
// Start polling for scan status if scanning
|
||||||
|
if (scanStatus === 'scanning') {
|
||||||
|
pollWatchlistScanStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error showing watchlist modal:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close watchlist modal
|
||||||
|
*/
|
||||||
|
function closeWatchlistModal() {
|
||||||
|
const modal = document.getElementById('watchlist-modal');
|
||||||
|
if (modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start watchlist scan
|
||||||
|
*/
|
||||||
|
async function startWatchlistScan() {
|
||||||
|
try {
|
||||||
|
const button = document.getElementById('scan-watchlist-btn');
|
||||||
|
button.disabled = true;
|
||||||
|
button.textContent = 'Starting scan...';
|
||||||
|
|
||||||
|
const response = await fetch('/api/watchlist/scan', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.error || 'Failed to start scan');
|
||||||
|
}
|
||||||
|
|
||||||
|
button.textContent = 'Scanning...';
|
||||||
|
|
||||||
|
// Show scan status
|
||||||
|
const statusDiv = document.getElementById('watchlist-scan-status');
|
||||||
|
if (statusDiv) {
|
||||||
|
statusDiv.style.display = 'flex';
|
||||||
|
document.getElementById('scan-status-text').textContent = 'scanning';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start polling for updates
|
||||||
|
pollWatchlistScanStatus();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error starting watchlist scan:', error);
|
||||||
|
const button = document.getElementById('scan-watchlist-btn');
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = 'Scan for New Releases';
|
||||||
|
alert(`Error starting scan: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll watchlist scan status
|
||||||
|
*/
|
||||||
|
async function pollWatchlistScanStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/watchlist/scan/status');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
const statusText = document.getElementById('scan-status-text');
|
||||||
|
const button = document.getElementById('scan-watchlist-btn');
|
||||||
|
|
||||||
|
if (statusText) {
|
||||||
|
// Show detailed progress if scanning
|
||||||
|
if (data.status === 'scanning' && data.current_artist_name) {
|
||||||
|
const artistProgress = `${data.current_artist_index || 0}/${data.total_artists || 0}`;
|
||||||
|
let detailText = `Scanning ${data.current_artist_name} (${artistProgress})`;
|
||||||
|
|
||||||
|
if (data.current_phase === 'fetching_discography') {
|
||||||
|
detailText += ' - Fetching releases...';
|
||||||
|
} else if (data.current_phase === 'checking_albums' && data.albums_to_check > 0) {
|
||||||
|
const albumProgress = `${data.albums_checked || 0}/${data.albums_to_check}`;
|
||||||
|
detailText += ` - Checking albums (${albumProgress})`;
|
||||||
|
} else if (data.current_phase && data.current_phase.startsWith('checking_album_')) {
|
||||||
|
detailText += ` - "${data.current_album || 'Unknown Album'}"`;
|
||||||
|
} else if (data.current_phase === 'rate_limiting') {
|
||||||
|
detailText += ' - Rate limiting...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add running totals
|
||||||
|
if (data.tracks_found_this_scan > 0 || data.tracks_added_this_scan > 0) {
|
||||||
|
detailText += ` | Found: ${data.tracks_found_this_scan || 0}, Added: ${data.tracks_added_this_scan || 0}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
statusText.textContent = detailText;
|
||||||
|
} else {
|
||||||
|
statusText.textContent = data.status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status === 'completed') {
|
||||||
|
if (button) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = 'Scan for New Releases';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status display with results
|
||||||
|
const statusDiv = document.getElementById('watchlist-scan-status');
|
||||||
|
if (statusDiv && data.summary) {
|
||||||
|
const newTracks = data.summary.new_tracks_found || 0;
|
||||||
|
const addedTracks = data.summary.tracks_added_to_wishlist || 0;
|
||||||
|
const totalArtists = data.summary.total_artists || 0;
|
||||||
|
const successfulScans = data.summary.successful_scans || 0;
|
||||||
|
|
||||||
|
let completionMessage = `Scan completed: ${successfulScans}/${totalArtists} artists scanned`;
|
||||||
|
if (newTracks > 0) {
|
||||||
|
completionMessage += `, found ${newTracks} new track${newTracks !== 1 ? 's' : ''}`;
|
||||||
|
if (addedTracks > 0) {
|
||||||
|
completionMessage += `, added ${addedTracks} to wishlist`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completionMessage += ', no new tracks found';
|
||||||
|
}
|
||||||
|
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<div class="scan-status-main">
|
||||||
|
<span class="sync-stat">${completionMessage}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update watchlist count
|
||||||
|
updateWatchlistButtonCount();
|
||||||
|
|
||||||
|
console.log('Watchlist scan completed:', data.summary);
|
||||||
|
return; // Stop polling
|
||||||
|
|
||||||
|
} else if (data.status === 'error') {
|
||||||
|
if (button) {
|
||||||
|
button.disabled = false;
|
||||||
|
button.textContent = 'Scan for New Releases';
|
||||||
|
}
|
||||||
|
console.error('Watchlist scan error:', data.error);
|
||||||
|
return; // Stop polling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue polling if still scanning
|
||||||
|
if (data.success && data.status === 'scanning') {
|
||||||
|
setTimeout(pollWatchlistScanStatus, 2000); // Poll every 2 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error polling watchlist scan status:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove artist from watchlist via modal
|
||||||
|
*/
|
||||||
|
async function removeFromWatchlistModal(artistId, artistName) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/watchlist/remove', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ artist_id: artistId })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.error || 'Failed to remove from watchlist');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`❌ Removed ${artistName} from watchlist`);
|
||||||
|
|
||||||
|
// Refresh the modal
|
||||||
|
showWatchlistModal();
|
||||||
|
|
||||||
|
// Update button count
|
||||||
|
updateWatchlistButtonCount();
|
||||||
|
|
||||||
|
// Update any visible artist cards
|
||||||
|
updateArtistCardWatchlistStatus();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing from watchlist:', error);
|
||||||
|
alert(`Error removing ${artistName} from watchlist: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Global Cleanup on Page Unload ---
|
// --- Global Cleanup on Page Unload ---
|
||||||
// Note: Automatic wishlist processing now runs server-side and continues even when browser is closed
|
// Note: Automatic wishlist processing now runs server-side and continues even when browser is closed
|
||||||
|
|
@ -5099,6 +5099,66 @@ body {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Watchlist Modal Styles */
|
||||||
|
.watchlist-artists-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 16px 20px 16px 24px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border-color: rgba(29, 185, 84, 0.2);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-date {
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-artist-scan {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(29, 185, 84, 0.7);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
/* Add these styles to the end of style.css */
|
/* Add these styles to the end of style.css */
|
||||||
|
|
||||||
.sync-progress-indicator {
|
.sync-progress-indicator {
|
||||||
|
|
@ -6022,6 +6082,66 @@ body {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Artist Card Actions (Watchlist) */
|
||||||
|
.artist-card-actions {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-toggle-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-toggle-btn:hover:not(:disabled) {
|
||||||
|
background: rgba(29, 185, 84, 0.15);
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: rgba(29, 185, 84, 0.3);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-toggle-btn.watching {
|
||||||
|
background: rgba(255, 193, 7, 0.15);
|
||||||
|
color: #ffc107;
|
||||||
|
border-color: rgba(255, 193, 7, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-toggle-btn.watching:hover:not(:disabled) {
|
||||||
|
background: rgba(255, 193, 7, 0.25);
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: rgba(255, 193, 7, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-toggle-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchlist-text {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* Artist Card Animation States */
|
/* Artist Card Animation States */
|
||||||
.artist-card.loading {
|
.artist-card.loading {
|
||||||
animation: artistCardPulse 2s ease-in-out infinite;
|
animation: artistCardPulse 2s ease-in-out infinite;
|
||||||
|
|
@ -6109,12 +6229,20 @@ body {
|
||||||
.artist-detail-header {
|
.artist-detail-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
padding-bottom: 30px;
|
padding-bottom: 30px;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.artist-detail-header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 30px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.artist-detail-back-button {
|
.artist-detail-back-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -6139,6 +6267,49 @@ body {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.artist-detail-watchlist-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 18px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #e0e0e0;
|
||||||
|
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
outline: none;
|
||||||
|
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-detail-watchlist-btn:hover:not(:disabled) {
|
||||||
|
background: rgba(29, 185, 84, 0.15);
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: rgba(29, 185, 84, 0.3);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-detail-watchlist-btn.watching {
|
||||||
|
background: rgba(255, 193, 7, 0.15);
|
||||||
|
color: #ffc107;
|
||||||
|
border-color: rgba(255, 193, 7, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-detail-watchlist-btn.watching:hover:not(:disabled) {
|
||||||
|
background: rgba(255, 193, 7, 0.25);
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: rgba(255, 193, 7, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.artist-detail-watchlist-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
.artist-detail-info {
|
.artist-detail-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue