From 90c6af078d09020ba106d5589d12e16fbd2b37cc Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Fri, 26 Sep 2025 07:43:12 -0700 Subject: [PATCH] add watchlist functionality to library page --- artist_bubble_snapshots.json | 42 +++++++++ web_server.py | 22 ++++- webui/index.html | 5 ++ webui/static/script.js | 161 ++++++++++++++++++++++++++++++++++- webui/static/style.css | 48 +++++++++++ 5 files changed, 271 insertions(+), 7 deletions(-) create mode 100644 artist_bubble_snapshots.json diff --git a/artist_bubble_snapshots.json b/artist_bubble_snapshots.json new file mode 100644 index 00000000..f8c3e3c0 --- /dev/null +++ b/artist_bubble_snapshots.json @@ -0,0 +1,42 @@ +{ + "bubbles": { + "6FBDaR13swtiWwGhX1WQsP": { + "artist": { + "id": "6FBDaR13swtiWwGhX1WQsP", + "name": "blink-182", + "image_url": "https://i.scdn.co/image/ab6761610000e5eb5da36f8b98dd965336a1507a", + "genres": [ + "pop punk", + "punk", + "rock", + "skate punk", + "emo" + ], + "popularity": 80, + "confidence": 0.1111111111111111 + }, + "downloads": [ + { + "virtualPlaylistId": "artist_album_6FBDaR13swtiWwGhX1WQsP_00txDYFrU4LjWqwKE8iQJA", + "album": { + "album_type": "album", + "external_urls": { + "spotify": "https://open.spotify.com/album/00txDYFrU4LjWqwKE8iQJA" + }, + "id": "00txDYFrU4LjWqwKE8iQJA", + "image_url": "https://i.scdn.co/image/ab67616d0000b273d07226d60768eb99f38997f8", + "name": "ONE MORE TIME...", + "release_date": "2023-10-27", + "total_tracks": 19 + }, + "albumType": "albums", + "status": "in_progress", + "startTime": "2025-09-26T14:42:42.028Z" + } + ], + "hasCompletedDownloads": false + } + }, + "timestamp": "2025-09-26T07:42:43.033178", + "snapshot_id": "20250926_074243" +} \ No newline at end of file diff --git a/web_server.py b/web_server.py index 7f022605..6adaf4da 100644 --- a/web_server.py +++ b/web_server.py @@ -2852,12 +2852,20 @@ def get_artist_detail(artist_id): single['image_url'] = fix_artist_image_url(single['image_url']) # Get Spotify discography for proper categorization and missing releases + spotify_artist_data = None try: spotify_discography = get_spotify_artist_discography(artist_info['name']) if spotify_discography['success']: print(f"🎵 Spotify discography found - Albums: {len(spotify_discography['albums'])}, EPs: {len(spotify_discography['eps'])}, Singles: {len(spotify_discography['singles'])}") + # Store Spotify artist data for the response + spotify_artist_data = { + 'spotify_artist_id': spotify_discography.get('spotify_artist_id'), + 'spotify_artist_name': spotify_discography.get('spotify_artist_name'), + 'artist_image': spotify_discography.get('artist_image') + } + # Merge owned and Spotify data for complete picture merged_discography = merge_discography_data(owned_releases, spotify_discography) else: @@ -2869,11 +2877,17 @@ def get_artist_detail(artist_id): # Fall back to our database categorization merged_discography = owned_releases - return jsonify({ + response_data = { "success": True, "artist": artist_info, "discography": merged_discography - }) + } + + # Add Spotify artist data if available + if spotify_artist_data: + response_data["spotify_artist"] = spotify_artist_data + + return jsonify(response_data) except Exception as e: print(f"❌ Error in get_artist_detail: {e}") @@ -12649,7 +12663,9 @@ def get_spotify_artist_discography(artist_name): 'albums': albums, 'eps': eps, 'singles': singles, - 'artist_image': artist.image_url if hasattr(artist, 'image_url') else None + 'artist_image': artist.image_url if hasattr(artist, 'image_url') else None, + 'spotify_artist_id': spotify_artist_id, + 'spotify_artist_name': artist.name } except Exception as e: diff --git a/webui/index.html b/webui/index.html index 5f116c57..8cda418a 100644 --- a/webui/index.html +++ b/webui/index.html @@ -737,6 +737,11 @@ + + diff --git a/webui/static/script.js b/webui/static/script.js index 0963e586..18c64485 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -13462,8 +13462,9 @@ async function showWatchlistModal() { Last scanned ${new Date(artist.last_scan_timestamp).toLocaleDateString()} ` : ''} - @@ -13478,7 +13479,16 @@ async function showWatchlistModal() { `; - + + // Add event listeners for remove buttons + modal.querySelectorAll('.watchlist-remove-btn').forEach(button => { + button.addEventListener('click', () => { + const artistId = button.getAttribute('data-artist-id'); + const artistName = button.getAttribute('data-artist-name'); + removeFromWatchlistModal(artistId, artistName); + }); + }); + // Show modal modal.style.display = 'flex'; @@ -14459,6 +14469,12 @@ function populateArtistDetailPage(data) { // Populate discography sections populateDiscographySections(discography); + + // Initialize library watchlist button if it exists (for library page) + const libraryWatchlistBtn = document.getElementById('library-artist-watchlist-btn'); + if (libraryWatchlistBtn && data.spotify_artist && data.spotify_artist.spotify_artist_id) { + initializeLibraryWatchlistButton(data.spotify_artist.spotify_artist_id, data.spotify_artist.spotify_artist_name); + } } function updateArtistDetailImage(imageUrl, artistName) { @@ -14593,7 +14609,7 @@ function updateCategoryStats(category, releases) { const owned = releases.filter(r => r.owned !== false).length; const missing = releases.filter(r => r.owned === false).length; const total = releases.length; - const completion = total > 0 ? Math.round((owned / total) * 100) : 0; + const completion = total > 0 ? Math.round((owned / total) * 100) : 100; console.log(`📊 ${category}: ${owned} owned, ${missing} missing, ${completion}% complete`); @@ -14953,3 +14969,140 @@ function showArtistDetailHero(show) { } } } + +/** + * Initialize the library page watchlist button + */ +async function initializeLibraryWatchlistButton(artistId, artistName) { + const button = document.getElementById('library-artist-watchlist-btn'); + if (!button) return; + + console.log(`🔧 Initializing library watchlist button for: ${artistName} (${artistId})`); + + // Reset button state + button.disabled = false; + button.classList.remove('watching'); + + // Set up click handler + button.onclick = (e) => toggleLibraryWatchlist(e, artistId, artistName); + + // Check and update current status + await updateLibraryWatchlistButtonStatus(artistId); +} + +/** + * Toggle watchlist status for library page + */ +async function toggleLibraryWatchlist(event, artistId, artistName) { + event.preventDefault(); + + const button = document.getElementById('library-artist-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 state based on new status + 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 if function exists + if (typeof updateWatchlistCount === 'function') { + updateWatchlistCount(); + } + + showToast(data.message, 'success'); + + } catch (error) { + console.error('Error toggling library watchlist:', error); + + // Restore button state + text.textContent = originalText; + showToast(`Error: ${error.message}`, 'error'); + + } finally { + button.disabled = false; + } +} + +/** + * Update library watchlist button status based on current state + */ +async function updateLibraryWatchlistButtonStatus(artistId) { + const button = document.getElementById('library-artist-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 = 'Watching...'; + button.classList.add('watching'); + } else { + icon.textContent = '👁️'; + text.textContent = 'Add to Watchlist'; + button.classList.remove('watching'); + } + } + } catch (error) { + console.warn('Failed to check library watchlist status:', error); + } +} diff --git a/webui/static/style.css b/webui/static/style.css index 93304761..52a88b4d 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -876,6 +876,9 @@ body { } .page-header { + display: flex; + justify-content: space-between; + align-items: center; margin-bottom: 30px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding-bottom: 20px; @@ -7279,6 +7282,51 @@ body { transform: none; } +/* Library Artist Watchlist Button */ + +.library-artist-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; +} + +.library-artist-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); +} + +.library-artist-watchlist-btn.watching { + background: rgba(255, 193, 7, 0.15); + color: #ffc107; + border-color: rgba(255, 193, 7, 0.3); +} + +.library-artist-watchlist-btn.watching:hover:not(:disabled) { + background: rgba(255, 193, 7, 0.25); + color: #ffffff; + border-color: rgba(255, 193, 7, 0.5); +} + +.library-artist-watchlist-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + .artist-detail-info { display: flex; align-items: center;