diff --git a/web_server.py b/web_server.py index 920bbab1..2d848064 100644 --- a/web_server.py +++ b/web_server.py @@ -21480,6 +21480,67 @@ def remove_from_watchlist(): print(f"Error removing from watchlist: {e}") return jsonify({"success": False, "error": str(e)}), 500 +@app.route('/api/watchlist/add-batch', methods=['POST']) +def add_batch_to_watchlist(): + """Add multiple artists to the watchlist at once""" + try: + data = request.get_json() + artists = data.get('artists', []) + + if not artists or not isinstance(artists, list): + return jsonify({"success": False, "error": "Missing or invalid artists list"}), 400 + + database = get_database() + added = 0 + skipped = 0 + + for artist in artists: + artist_id = artist.get('artist_id') + artist_name = artist.get('artist_name') + if not artist_id or not artist_name: + continue + + # Check if already watched + if database.is_artist_in_watchlist(artist_id): + skipped += 1 + continue + + success = database.add_artist_to_watchlist(artist_id, artist_name) + if success: + added += 1 + # Cache artist image + try: + is_itunes_id = artist_id.isdigit() + if is_itunes_id: + itunes_url = f"https://itunes.apple.com/lookup?id={artist_id}&entity=album&limit=5" + resp = requests.get(itunes_url, timeout=5) + if resp.status_code == 200: + results = resp.json().get('results', []) + for res in results: + if 'artworkUrl100' in res: + image_url = res['artworkUrl100'].replace('100x100', '600x600') + database.update_watchlist_artist_image(artist_id, image_url) + break + elif spotify_client and spotify_client.is_authenticated(): + artist_data = spotify_client.get_artist(artist_id) + if artist_data and 'images' in artist_data and artist_data['images']: + image_url = artist_data['images'][1]['url'] if len(artist_data['images']) > 1 else artist_data['images'][0]['url'] + if image_url: + database.update_watchlist_artist_image(artist_id, image_url) + except Exception as img_error: + print(f"⚠️ Could not fetch artist image for {artist_name}: {img_error}") + + return jsonify({ + "success": True, + "added": added, + "skipped": skipped, + "message": f"Added {added} artist{'s' if added != 1 else ''} to watchlist ({skipped} already watched)" + }) + + except Exception as e: + print(f"Error batch adding to watchlist: {e}") + return jsonify({"success": False, "error": str(e)}), 500 + @app.route('/api/watchlist/remove-batch', methods=['POST']) def remove_batch_from_watchlist(): """Remove multiple artists from the watchlist""" diff --git a/webui/index.html b/webui/index.html index bad8ea61..252f8ff0 100644 --- a/webui/index.html +++ b/webui/index.html @@ -2189,6 +2189,12 @@
+ + + diff --git a/webui/static/script.js b/webui/static/script.js index e4dc8610..a282e485 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -33393,6 +33393,9 @@ async function loadDiscoverHero() { }, 8000); } + // Check if all hero artists are already watched + checkAllHeroWatchlistStatus(); + } catch (error) { console.error('Error loading discover hero:', error); showDiscoverHeroEmpty(); @@ -33543,6 +33546,90 @@ function toggleDiscoverHeroWatchlist(event) { toggleWatchlist(event, artistId, artistName); } +async function watchAllHeroArtists(btn) { + if (!discoverHeroArtists || discoverHeroArtists.length === 0) return; + if (btn.classList.contains('all-watched')) return; + + const textEl = btn.querySelector('.watch-all-text'); + const originalText = textEl ? textEl.textContent : ''; + + // Loading state + btn.disabled = true; + if (textEl) textEl.textContent = 'Adding...'; + + try { + const artists = discoverHeroArtists.map(a => ({ + artist_id: a.artist_id, + artist_name: a.artist_name + })); + + const response = await fetch('/api/watchlist/add-batch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ artists }) + }); + + const data = await response.json(); + if (data.success) { + if (textEl) textEl.textContent = 'All Watched'; + btn.classList.add('all-watched'); + btn.disabled = true; + + // Sync the per-slide watchlist button for current artist + const currentArtist = discoverHeroArtists[discoverHeroIndex]; + if (currentArtist) { + checkAndUpdateDiscoverHeroWatchlistButton(currentArtist.artist_id); + } + + // Update watchlist count badge + if (typeof updateWatchlistButtonCount === 'function') { + updateWatchlistButtonCount(); + } + } else { + if (textEl) textEl.textContent = originalText; + btn.disabled = false; + } + } catch (error) { + console.error('Error watching all hero artists:', error); + if (textEl) textEl.textContent = originalText; + btn.disabled = false; + } +} + +async function checkAllHeroWatchlistStatus() { + const btn = document.getElementById('discover-hero-watch-all'); + if (!btn || !discoverHeroArtists || discoverHeroArtists.length === 0) return; + + try { + let allWatched = true; + for (const artist of discoverHeroArtists) { + const response = await fetch('/api/watchlist/check', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ artist_id: artist.artist_id }) + }); + const data = await response.json(); + if (!data.success || !data.is_watching) { + allWatched = false; + break; + } + } + + const textEl = btn.querySelector('.watch-all-text'); + if (allWatched) { + if (textEl) textEl.textContent = 'All Watched'; + btn.classList.add('all-watched'); + btn.disabled = true; + } else { + if (textEl) textEl.textContent = 'Watch All'; + btn.classList.remove('all-watched'); + btn.disabled = false; + } + } catch (error) { + console.error('Error checking hero watchlist status:', error); + } +} + function navigateDiscoverHero(direction) { if (!discoverHeroArtists || discoverHeroArtists.length === 0) return; diff --git a/webui/static/style.css b/webui/static/style.css index a4fdf58d..549b5356 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -20179,6 +20179,48 @@ body { border-radius: 4px; } +/* Watch All Button */ +.discover-hero-watch-all { + position: absolute; + bottom: 24px; + right: 24px; + z-index: 10; + display: flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + background: rgba(0, 0, 0, 0.3); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 20px; + color: rgba(255, 255, 255, 0.9); + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: background 0.2s ease, border-color 0.2s ease; +} + +.discover-hero-watch-all:hover { + background: rgba(0, 0, 0, 0.5); + border-color: rgba(255, 255, 255, 0.3); +} + +.discover-hero-watch-all .watch-all-icon { + font-size: 14px; +} + +.discover-hero-watch-all.all-watched { + background: rgba(var(--accent-rgb), 0.15); + border-color: rgba(var(--accent-rgb), 0.4); + color: rgb(var(--accent-rgb)); + cursor: default; +} + +.discover-hero-watch-all:disabled:not(.all-watched) { + opacity: 0.6; + cursor: wait; +} + /* Discover Sections */ .discover-section { margin-bottom: 50px;