From cff48b2498ea9e5fdd1482e2ee3251f7963acfc0 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Wed, 3 Sep 2025 18:40:25 -0700 Subject: [PATCH] artist design --- web_server.py | 42 ++++++--- webui/static/script.js | 209 ++++++++++++++++++++++++++++++++++++++++- webui/static/style.css | 44 +++++++-- 3 files changed, 274 insertions(+), 21 deletions(-) diff --git a/web_server.py b/web_server.py index e665ff59..0dc1facf 100644 --- a/web_server.py +++ b/web_server.py @@ -1596,8 +1596,9 @@ def get_artist_discography(artist_id): print(f"🎤 Fetching discography for artist: {artist_id}") - # Get all albums and singles for the artist - albums = spotify_client.get_artist_albums(artist_id, album_type='album,single,appears_on', limit=50) + # Get artist's albums and singles (temporarily include appears_on for debugging) + albums = spotify_client.get_artist_albums(artist_id, album_type='album,single', limit=50) + print(f"📊 Raw albums returned from Spotify: {len(albums)}") if not albums: return jsonify({ @@ -1618,13 +1619,22 @@ def get_artist_discography(artist_id): continue seen_albums.add(album.id) - # Skip compilations and appears_on that aren't the main artist's work - if hasattr(album, 'album_type') and album.album_type == 'appears_on': - # Only include if the artist is the main artist - if hasattr(album, 'artists') and album.artists: - main_artist_id = album.artists[0].id if hasattr(album.artists[0], 'id') else None - if main_artist_id != artist_id: - continue + # Debug: Check artist information + print(f"🔍 Checking album: {album.name}") + if hasattr(album, 'artists') and album.artists: + primary_artist_id = album.artists[0].id if hasattr(album.artists[0], 'id') else None + primary_artist_name = album.artists[0].name if hasattr(album.artists[0], 'name') else None + print(f" Primary artist: {primary_artist_name} (ID: {primary_artist_id})") + print(f" Requested artist ID: {artist_id}") + + # Skip if the primary artist doesn't match our requested artist + if primary_artist_id and primary_artist_id != artist_id: + print(f"🚫 Skipping '{album.name}' - primary artist mismatch") + continue + elif not primary_artist_id: + print(f"⚠️ No primary artist ID found for '{album.name}' - including anyway") + else: + print(f"⚠️ No artists found for '{album.name}' - including anyway") album_data = { "id": album.id, @@ -1636,11 +1646,15 @@ def get_artist_discography(artist_id): "external_urls": album.external_urls if hasattr(album, 'external_urls') else {} } + # Skip obvious compilation issues but be more lenient for now + if hasattr(album, 'album_type') and album.album_type == 'compilation': + print(f"📀 Found compilation: '{album.name}' - including for now") + # Categorize by album type if hasattr(album, 'album_type'): if album.album_type in ['single', 'ep']: singles_list.append(album_data) - else: # 'album' or 'compilation' + else: # 'album' or approved 'compilation' album_list.append(album_data) else: # Default to album if no type specified @@ -1659,7 +1673,13 @@ def get_artist_discography(artist_id): album_list.sort(key=get_release_year, reverse=True) singles_list.sort(key=get_release_year, reverse=True) - print(f"✅ Found {len(album_list)} albums and {len(singles_list)} singles for artist") + print(f"✅ Found {len(album_list)} albums and {len(singles_list)} singles for artist {artist_id}") + + # Debug: Log the final album list + for album in album_list: + print(f"📀 Album: {album['name']} ({album['album_type']}) - {album['release_date']}") + for single in singles_list: + print(f"🎵 Single/EP: {single['name']} ({single['album_type']}) - {single['release_date']}") return jsonify({ "albums": album_list, diff --git a/webui/static/script.js b/webui/static/script.js index a2d6e389..5c932737 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -59,7 +59,8 @@ let artistsPageState = { }, cache: { searches: {}, // Cache search results by query - discography: {} // Cache discography by artist ID + discography: {}, // Cache discography by artist ID + colors: {} // Cache extracted colors by image URL } }; let artistsSearchTimeout = null; @@ -9293,6 +9294,14 @@ function displayArtistsResults(query, results) { // Add event listeners to cards container.querySelectorAll('.artist-card').forEach((card, index) => { card.addEventListener('click', () => selectArtist(results[index])); + + // Extract colors from artist image for dynamic glow + const artist = results[index]; + if (artist.image_url) { + extractImageColors(artist.image_url, (colors) => { + applyDynamicGlow(card, colors); + }); + } }); // Add mouse wheel horizontal scrolling @@ -9421,6 +9430,16 @@ function displayArtistDiscography(discography) { if (albumsContainer) { if (discography.albums?.length > 0) { albumsContainer.innerHTML = discography.albums.map(album => createAlbumCard(album)).join(''); + + // Add dynamic glow effects to album cards + albumsContainer.querySelectorAll('.album-card').forEach((card, index) => { + const album = discography.albums[index]; + if (album.image_url) { + extractImageColors(album.image_url, (colors) => { + applyDynamicGlow(card, colors); + }); + } + }); } else { albumsContainer.innerHTML = `
@@ -9436,6 +9455,16 @@ function displayArtistDiscography(discography) { if (singlesContainer) { if (discography.singles?.length > 0) { singlesContainer.innerHTML = discography.singles.map(single => createAlbumCard(single)).join(''); + + // Add dynamic glow effects to singles cards + singlesContainer.querySelectorAll('.album-card').forEach((card, index) => { + const single = discography.singles[index]; + if (single.image_url) { + extractImageColors(single.image_url, (colors) => { + applyDynamicGlow(card, colors); + }); + } + }); } else { singlesContainer.innerHTML = `
@@ -9717,6 +9746,184 @@ function showSearchLoadingCards() { container.innerHTML = loadingCardHtml.repeat(6); } +/** + * Extract dominant colors from an image for dynamic glow effects + */ +async function extractImageColors(imageUrl, callback) { + if (!imageUrl) { + callback(['#1db954', '#1ed760']); // Fallback to Spotify green + return; + } + + // Check cache first for performance + if (artistsPageState.cache.colors[imageUrl]) { + callback(artistsPageState.cache.colors[imageUrl]); + return; + } + + try { + // Create a canvas to analyze the image + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const img = new Image(); + + img.crossOrigin = 'anonymous'; + + img.onload = function() { + // Resize to small dimensions for faster processing + const size = 50; + canvas.width = size; + canvas.height = size; + + // Draw image to canvas + ctx.drawImage(img, 0, 0, size, size); + + try { + // Get image data + const imageData = ctx.getImageData(0, 0, size, size); + const data = imageData.data; + + // Extract colors (sample every few pixels for performance) + const colors = []; + for (let i = 0; i < data.length; i += 16) { // Sample every 4th pixel + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + const alpha = data[i + 3]; + + // Skip transparent or very dark pixels + if (alpha > 128 && (r + g + b) > 150) { + colors.push({ r, g, b }); + } + } + + if (colors.length === 0) { + callback(['#1db954', '#1ed760']); // Fallback + return; + } + + // Find dominant colors using a simple clustering approach + const dominantColors = findDominantColors(colors, 2); + + // Convert to CSS hex colors + const hexColors = dominantColors.map(color => + `#${((1 << 24) + (color.r << 16) + (color.g << 8) + color.b).toString(16).slice(1)}` + ); + + // Cache the colors for future use + artistsPageState.cache.colors[imageUrl] = hexColors; + + callback(hexColors); + + } catch (e) { + console.warn('Color extraction failed, using fallback colors:', e); + callback(['#1db954', '#1ed760']); + } + }; + + img.onerror = function() { + callback(['#1db954', '#1ed760']); // Fallback on error + }; + + img.src = imageUrl; + + } catch (error) { + console.warn('Image color extraction error:', error); + callback(['#1db954', '#1ed760']); + } +} + +/** + * Simple color clustering to find dominant colors + */ +function findDominantColors(colors, numColors = 2) { + if (colors.length === 0) return [{ r: 29, g: 185, b: 84 }]; + + // Simple k-means clustering + let centroids = []; + + // Initialize centroids randomly + for (let i = 0; i < numColors; i++) { + centroids.push(colors[Math.floor(Math.random() * colors.length)]); + } + + // Run a few iterations of k-means + for (let iteration = 0; iteration < 5; iteration++) { + const clusters = Array(numColors).fill().map(() => []); + + // Assign each color to nearest centroid + colors.forEach(color => { + let minDistance = Infinity; + let nearestCluster = 0; + + centroids.forEach((centroid, i) => { + const distance = Math.sqrt( + Math.pow(color.r - centroid.r, 2) + + Math.pow(color.g - centroid.g, 2) + + Math.pow(color.b - centroid.b, 2) + ); + + if (distance < minDistance) { + minDistance = distance; + nearestCluster = i; + } + }); + + clusters[nearestCluster].push(color); + }); + + // Update centroids + centroids = clusters.map(cluster => { + if (cluster.length === 0) return centroids[0]; // Fallback + + const avgR = cluster.reduce((sum, c) => sum + c.r, 0) / cluster.length; + const avgG = cluster.reduce((sum, c) => sum + c.g, 0) / cluster.length; + const avgB = cluster.reduce((sum, c) => sum + c.b, 0) / cluster.length; + + return { r: Math.round(avgR), g: Math.round(avgG), b: Math.round(avgB) }; + }); + } + + // Ensure we have vibrant colors by boosting saturation + return centroids.map(color => { + const max = Math.max(color.r, color.g, color.b); + const min = Math.min(color.r, color.g, color.b); + const saturation = max === 0 ? 0 : (max - min) / max; + + // Boost low saturation colors + if (saturation < 0.4) { + const factor = 1.3; + return { + r: Math.min(255, Math.round(color.r * factor)), + g: Math.min(255, Math.round(color.g * factor)), + b: Math.min(255, Math.round(color.b * factor)) + }; + } + + return color; + }); +} + +/** + * Apply dynamic glow effect to a card element + */ +function applyDynamicGlow(cardElement, colors) { + if (!cardElement || colors.length < 2) return; + + const color1 = colors[0]; + const color2 = colors[1]; + + // Add a small delay to make the effect feel more natural + setTimeout(() => { + // Create CSS custom properties for the dynamic colors + cardElement.style.setProperty('--glow-color-1', color1); + cardElement.style.setProperty('--glow-color-2', color2); + cardElement.classList.add('has-dynamic-glow'); + + console.log(`🎨 Applied dynamic glow: ${color1}, ${color2}`); + }, Math.random() * 200 + 100); // Random delay between 100-300ms +} + /** * Utility function to escape HTML */ diff --git a/webui/static/style.css b/webui/static/style.css index 978e9eb4..42d2046e 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -5861,7 +5861,7 @@ body { gap: 20px; overflow-x: auto; overflow-y: visible; - padding: 20px; + padding: 30px; scroll-behavior: smooth; /* Custom scrollbar styling */ @@ -5914,14 +5914,27 @@ body { } .artist-card:hover { - transform: translateY(-8px) scale(1.02); - z-index: 10; - - box-shadow: - 0 20px 50px rgba(0, 0, 0, 0.6), - 0 0 0 1px rgba(29, 185, 84, 0.15), - 0 0 30px rgba(29, 185, 84, 0.1), - inset 0 1px 0 rgba(255, 255, 255, 0.1); + transform: translateY(-5px) scale(1.01); + z-index: 10; + + box-shadow: + 0 12px 15px rgba(0, 0, 0, 0.4), + 0 0 0 1px rgba(29, 185, 84, 0.15), + 0 0 20px rgba(29, 185, 84, 0.08), + inset 0 1px 0 rgba(255, 255, 255, 0.1); +} + +/* Dynamic glow effects based on image colors */ +.artist-card.has-dynamic-glow { + position: relative; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1), + filter 0.5s cubic-bezier(0.4, 0, 0.2, 1); +} + +.artist-card.has-dynamic-glow:hover { + filter: drop-shadow(0 0 8px var(--glow-color-1, #1db954)) + drop-shadow(0 0 16px var(--glow-color-2, #1ed760)); + border-color: var(--glow-color-1, rgba(29, 185, 84, 0.3)); } .artist-card-background { @@ -6259,6 +6272,19 @@ body { inset 0 1px 0 rgba(255, 255, 255, 0.1); } +/* Dynamic glow effects for album cards */ +.album-card.has-dynamic-glow { + position: relative; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1), + filter 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +.album-card.has-dynamic-glow:hover { + filter: drop-shadow(0 0 6px var(--glow-color-1, #1db954)) + drop-shadow(0 0 12px var(--glow-color-2, #1ed760)); + border-color: var(--glow-color-1, rgba(29, 185, 84, 0.3)); +} + .album-card-image { width: 100%; aspect-ratio: 1;