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;