library page update

This commit is contained in:
Broque Thomas 2025-09-23 14:19:11 -07:00
parent 9485dfb03b
commit 03b0b8c6c5
4 changed files with 1583 additions and 9 deletions

View file

@ -2687,6 +2687,96 @@ def get_library_artists():
}
}), 500
@app.route('/api/test-artist/<artist_id>')
def test_artist_endpoint(artist_id):
"""Simple test endpoint"""
return jsonify({
"success": True,
"message": f"Test endpoint working for artist ID: {artist_id}"
})
@app.route('/api/artist-detail/<artist_id>')
def get_artist_detail(artist_id):
"""Get artist detail data"""
try:
print(f"🎵 Getting artist detail for ID: {artist_id}")
# Get database instance
database = get_database()
# Get artist discography from database
db_result = database.get_artist_discography(artist_id)
if not db_result.get('success'):
print(f"❌ Database returned error: {db_result}")
return jsonify({
"success": False,
"error": db_result.get('error', 'Artist not found')
}), 404
artist_info = db_result['artist']
owned_releases = db_result['owned_releases']
print(f"✅ Found artist: {artist_info['name']} with {len(owned_releases['albums'])} albums")
# Fix artist image URL
print(f"🖼️ Artist image before fix: '{artist_info.get('image_url')}'")
if artist_info.get('image_url'):
artist_info['image_url'] = fix_artist_image_url(artist_info['image_url'])
print(f"🖼️ Artist image after fix: '{artist_info['image_url']}'")
else:
print(f"🖼️ No artist image URL found for {artist_info['name']}")
# Debug final artist data being sent
print(f"🖼️ Final artist data being sent: {artist_info}")
# Fix image URLs for all albums
for album in owned_releases['albums']:
if album.get('image_url'):
album['image_url'] = fix_artist_image_url(album['image_url'])
# Fix image URLs for EPs and singles (currently empty but for future use)
for ep in owned_releases['eps']:
if ep.get('image_url'):
ep['image_url'] = fix_artist_image_url(ep['image_url'])
for single in owned_releases['singles']:
if single.get('image_url'):
single['image_url'] = fix_artist_image_url(single['image_url'])
# Get Spotify discography for proper categorization and missing releases
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'])}")
# Merge owned and Spotify data for complete picture
merged_discography = merge_discography_data(owned_releases, spotify_discography)
else:
print(f"⚠️ Spotify discography not found: {spotify_discography.get('error', 'Unknown error')}")
# Fall back to our database categorization
merged_discography = owned_releases
except Exception as spotify_error:
print(f"⚠️ Error fetching Spotify data: {spotify_error}")
# Fall back to our database categorization
merged_discography = owned_releases
return jsonify({
"success": True,
"artist": artist_info,
"discography": merged_discography
})
except Exception as e:
print(f"❌ Error in get_artist_detail: {e}")
import traceback
traceback.print_exc()
return jsonify({
"success": False,
"error": str(e)
}), 500
@app.route('/api/library/debug-photos')
def debug_library_photos():
"""Debug endpoint to check artist photo URLs"""
@ -12210,6 +12300,217 @@ def start_oauth_callback_servers():
print("✅ OAuth callback servers started")
# ===============================================
# Artist Detail Spotify Integration Functions
# ===============================================
def get_spotify_artist_discography(artist_name):
"""Get complete artist discography from Spotify"""
try:
from core.spotify_client import SpotifyClient
print(f"🎵 Searching Spotify for artist: {artist_name}")
# Initialize Spotify client
spotify_client = SpotifyClient()
# Search for the artist
artists = spotify_client.search_artists(artist_name, limit=1)
if not artists:
return {
'success': False,
'error': f'Artist "{artist_name}" not found on Spotify'
}
artist = artists[0]
spotify_artist_id = artist.id
print(f"🎵 Found Spotify artist: {artist.name} (ID: {spotify_artist_id})")
# Get all albums (albums, singles, and compilations)
all_albums = spotify_client.get_artist_albums(spotify_artist_id, album_type='album,single,compilation', limit=50)
if not all_albums:
return {
'success': False,
'error': f'No albums found for artist "{artist_name}"'
}
print(f"📀 Found {len(all_albums)} releases on Spotify")
# Categorize releases
albums = []
eps = []
singles = []
for album in all_albums:
# Use the Album object properties
track_count = album.total_tracks
release_data = {
'title': album.name,
'year': album.release_date[:4] if album.release_date else None,
'image_url': album.image_url,
'spotify_id': album.id,
'owned': False, # Will be updated when merging with owned data
'track_count': track_count,
'album_type': album.album_type.lower()
}
# Categorize based on album type and track count
album_type = album.album_type.lower()
if album_type == 'single' or track_count <= 3:
singles.append(release_data)
elif album_type == 'compilation' or (track_count <= 8 and track_count > 3):
eps.append(release_data)
else:
albums.append(release_data)
print(f"📀 Categorized Spotify releases - Albums: {len(albums)}, EPs: {len(eps)}, Singles: {len(singles)}")
return {
'success': True,
'albums': albums,
'eps': eps,
'singles': singles,
'artist_image': artist.image_url if hasattr(artist, 'image_url') else None
}
except Exception as e:
print(f"❌ Error getting Spotify discography for {artist_name}: {e}")
return {
'success': False,
'error': str(e)
}
def merge_discography_data(owned_releases, spotify_discography):
"""Build discography using Spotify as source of truth, checking if we own each release"""
try:
print("🔄 Building discography using Spotify categorization...")
def normalize_title(title):
"""Normalize title for comparison"""
import re
normalized = title.lower().strip()
normalized = re.sub(r'[^\w\s]', '', normalized)
normalized = re.sub(r'\s+', ' ', normalized)
return normalized.strip()
def normalize_year(year):
"""Normalize year to integer for comparison"""
if year is None:
return None
try:
return int(year)
except (ValueError, TypeError):
return None
# Create a flat map of ALL owned releases (regardless of category)
all_owned = []
all_owned.extend(owned_releases['albums'])
all_owned.extend(owned_releases['eps'])
all_owned.extend(owned_releases['singles'])
owned_map = {}
for owned in all_owned:
key = (normalize_title(owned['title']), normalize_year(owned.get('year')))
if key not in owned_map:
owned_map[key] = []
owned_map[key].append(owned)
print(f"📀 Created lookup map for {len(all_owned)} owned releases")
def build_category(spotify_category, category_name):
"""Build cards for a category using Spotify as source of truth"""
cards = []
print(f"📀 Building {category_name} category with {len(spotify_category)} Spotify releases")
for spotify_release in spotify_category:
spotify_key = (normalize_title(spotify_release['title']), normalize_year(spotify_release.get('year')))
# Debug logging for Bad Hair Day specifically
if 'bad hair day' in spotify_release['title'].lower():
print(f"🔍 DEBUG: Spotify 'Bad Hair Day' - Title: '{spotify_release['title']}', Year: {spotify_release.get('year')}")
print(f"🔍 DEBUG: Normalized key: {spotify_key}")
print(f"🔍 DEBUG: Available owned keys: {list(owned_map.keys())}")
# Check if we own this release (exact match first)
owned_release = None
matched_key = None
if spotify_key in owned_map and owned_map[spotify_key]:
owned_release = owned_map[spotify_key].pop(0).copy()
matched_key = spotify_key
else:
# Fallback: try matching by title only (ignore year)
title_only = normalize_title(spotify_release['title'])
for key, releases in owned_map.items():
if key[0] == title_only and releases: # key[0] is the normalized title
owned_release = releases.pop(0).copy()
matched_key = key
print(f"🔄 Year mismatch fallback: '{spotify_release['title']}' matched by title only")
break
if owned_release:
# We own it - use owned data with Spotify enhancements
# Add Spotify metadata
owned_release['spotify_id'] = spotify_release['spotify_id']
owned_release['owned'] = True
# Image priority: owned first, then Spotify fallback
if not owned_release.get('image_url') and spotify_release.get('image_url'):
owned_release['image_url'] = spotify_release['image_url']
cards.append(owned_release)
print(f"{category_name}: '{spotify_release['title']}' - OWNED")
# Remove empty lists from map (use the key that actually matched)
if matched_key and not owned_map[matched_key]:
del owned_map[matched_key]
else:
# We don't own it - create missing card
missing_release = spotify_release.copy()
missing_release['owned'] = False
missing_release['track_completion'] = 0
cards.append(missing_release)
print(f"{category_name}: '{spotify_release['title']}' - MISSING")
return cards
# Build each category using Spotify as the source of truth
albums = build_category(spotify_discography['albums'], 'Albums')
eps = build_category(spotify_discography['eps'], 'EPs')
singles = build_category(spotify_discography['singles'], 'Singles')
# Report any owned releases that didn't match Spotify (rare)
remaining_owned = sum(len(owned_list) for owned_list in owned_map.values())
if remaining_owned > 0:
print(f"📀 Note: {remaining_owned} owned releases not found on Spotify (compilations, bootlegs, etc.)")
print(f"✅ Built discography - Albums: {len(albums)}, EPs: {len(eps)}, Singles: {len(singles)}")
return {
'success': True,
'albums': albums,
'eps': eps,
'singles': singles
}
except Exception as e:
print(f"❌ Error building discography: {e}")
import traceback
traceback.print_exc()
return {
'success': False,
'error': str(e),
'albums': [],
'eps': [],
'singles': []
}
if __name__ == '__main__':
# Initialize logging for web server
from utils.logging_config import setup_logging

View file

@ -588,10 +588,10 @@
</button>
<div class="artist-detail-info">
<div class="artist-detail-image" id="artist-detail-image"></div>
<div class="artist-detail-image" id="search-artist-detail-image"></div>
<div class="artist-detail-text">
<h2 class="artist-detail-name" id="artist-detail-name">Artist Name</h2>
<p class="artist-detail-genres" id="artist-detail-genres">Genres</p>
<h2 class="artist-detail-name" id="search-artist-detail-name">Artist Name</h2>
<p class="artist-detail-genres" id="search-artist-detail-genres">Genres</p>
</div>
</div>
</div>
@ -729,6 +729,135 @@
</div>
</div>
<!-- Artist Detail Page -->
<div class="page" id="artist-detail-page">
<div class="page-header">
<button class="back-btn" id="artist-detail-back-btn">
<span>← Back to Library</span>
</button>
</div>
<!-- Artist Hero Section -->
<div class="artist-hero-section">
<div class="artist-hero-content">
<div class="artist-image-container">
<img class="artist-image" id="artist-detail-image" src="" alt="Artist Image" />
<div class="artist-image-fallback" id="artist-detail-image-fallback">🎵</div>
</div>
<div class="artist-info">
<h1 class="artist-name" id="artist-detail-name">Artist Name</h1>
<div class="collection-overview">
<div class="collection-category">
<div class="category-header">
<span class="category-label">Albums</span>
<span class="category-stats" id="albums-stats">0 owned, 0 missing</span>
</div>
<div class="completion-section">
<div class="completion-bar">
<div class="completion-fill" id="albums-completion-fill" style="width: 0%"></div>
</div>
<span class="completion-text" id="albums-completion-text">0%</span>
</div>
</div>
<div class="collection-category">
<div class="category-header">
<span class="category-label">EPs</span>
<span class="category-stats" id="eps-stats">0 owned, 0 missing</span>
</div>
<div class="completion-section">
<div class="completion-bar">
<div class="completion-fill" id="eps-completion-fill" style="width: 0%"></div>
</div>
<span class="completion-text" id="eps-completion-text">0%</span>
</div>
</div>
<div class="collection-category">
<div class="category-header">
<span class="category-label">Singles</span>
<span class="category-stats" id="singles-stats">0 owned, 0 missing</span>
</div>
<div class="completion-section">
<div class="completion-bar">
<div class="completion-fill" id="singles-completion-fill" style="width: 0%"></div>
</div>
<span class="completion-text" id="singles-completion-text">0%</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="artist-detail-content">
<!-- Loading State -->
<div class="artist-detail-loading hidden" id="artist-detail-loading">
<div class="loading-spinner"></div>
<p>Loading artist discography...</p>
</div>
<!-- Error State -->
<div class="artist-detail-error hidden" id="artist-detail-error">
<div class="error-icon">⚠️</div>
<h3>Failed to load artist details</h3>
<p id="artist-detail-error-message">An error occurred while loading the artist's discography.</p>
<button class="retry-btn" id="artist-detail-retry-btn">Retry</button>
</div>
<!-- Main Content -->
<div class="artist-detail-main" id="artist-detail-main">
<!-- Discography Sections -->
<div class="discography-sections">
<!-- Albums Section -->
<div class="discography-section" id="albums-section">
<div class="section-header">
<h3>Albums</h3>
<div class="section-stats">
<span id="albums-owned-count">0 owned</span>
<span id="albums-missing-count">0 missing</span>
</div>
</div>
<div class="releases-grid" id="albums-grid">
<!-- Album cards will be populated here -->
</div>
</div>
<!-- EPs Section -->
<div class="discography-section" id="eps-section">
<div class="section-header">
<h3>EPs</h3>
<div class="section-stats">
<span id="eps-owned-count">0 owned</span>
<span id="eps-missing-count">0 missing</span>
</div>
</div>
<div class="releases-grid" id="eps-grid">
<!-- EP cards will be populated here -->
</div>
</div>
<!-- Singles Section -->
<div class="discography-section" id="singles-section">
<div class="section-header">
<h3>Singles</h3>
<div class="section-stats">
<span id="singles-owned-count">0 owned</span>
<span id="singles-missing-count">0 missing</span>
</div>
</div>
<div class="releases-grid" id="singles-grid">
<!-- Single cards will be populated here -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Settings Page -->
<div class="page" id="settings-page">
<div class="page-header">

View file

@ -346,11 +346,14 @@ function initializeWatchlist() {
function navigateToPage(pageId) {
if (pageId === currentPage) return;
// Update navigation buttons
// Update navigation buttons (only if there's a nav button for this page)
document.querySelectorAll('.nav-button').forEach(btn => {
btn.classList.remove('active');
});
document.querySelector(`[data-page="${pageId}"]`).classList.add('active');
const navButton = document.querySelector(`[data-page="${pageId}"]`);
if (navButton) {
navButton.classList.add('active');
}
// Update pages
document.querySelectorAll('.page').forEach(page => {
@ -405,6 +408,9 @@ async function loadPageData(pageId) {
await loadLibraryArtists();
}
break;
case 'artist-detail':
// Artist detail page is handled separately by navigateToArtistDetail()
break;
case 'settings':
initializeSettings();
await loadSettingsData();
@ -13824,11 +13830,10 @@ function createLibraryArtistCard(artist) {
card.appendChild(imageContainer);
card.appendChild(info);
// Add click handler (for future functionality)
// Add click handler to navigate to artist detail page
card.addEventListener("click", () => {
// TODO: Implement artist detail view
console.log(`Clicked on artist: ${artist.name}`);
showToast(`Artist details coming soon!`, "info");
console.log(`🎵 Opening artist detail for: ${artist.name} (ID: ${artist.id})`);
navigateToArtistDetail(artist.id, artist.name);
});
return card;
@ -13892,3 +13897,475 @@ function showLibraryEmpty(show) {
}
}
}
// ===============================================
// Artist Detail Page Functions
// ===============================================
// Artist detail page state
let artistDetailPageState = {
isInitialized: false,
currentArtistId: null,
currentArtistName: null
};
function navigateToArtistDetail(artistId, artistName) {
console.log(`🎵 Navigating to artist detail: ${artistName} (ID: ${artistId})`);
// Store current artist info
artistDetailPageState.currentArtistId = artistId;
artistDetailPageState.currentArtistName = artistName;
// Navigate to artist detail page
navigateToPage('artist-detail');
// Initialize if needed and load data
if (!artistDetailPageState.isInitialized) {
initializeArtistDetailPage();
}
// Load artist data
loadArtistDetailData(artistId, artistName);
}
function initializeArtistDetailPage() {
console.log("🔧 Initializing Artist Detail page...");
// Initialize back button
const backBtn = document.getElementById("artist-detail-back-btn");
if (backBtn) {
backBtn.addEventListener("click", () => {
console.log("🔙 Returning to Library page");
navigateToPage('library');
});
}
// Initialize retry button
const retryBtn = document.getElementById("artist-detail-retry-btn");
if (retryBtn) {
retryBtn.addEventListener("click", () => {
if (artistDetailPageState.currentArtistId && artistDetailPageState.currentArtistName) {
loadArtistDetailData(artistDetailPageState.currentArtistId, artistDetailPageState.currentArtistName);
}
});
}
artistDetailPageState.isInitialized = true;
console.log("✅ Artist Detail page initialized successfully");
}
async function loadArtistDetailData(artistId, artistName) {
console.log(`🔄 Loading artist detail data for: ${artistName} (ID: ${artistId})`);
// Show loading state
showArtistDetailLoading(true);
showArtistDetailError(false);
showArtistDetailMain(false);
// Update header with artist name
updateArtistDetailHeader(artistName);
try {
// Call API to get artist discography data
const response = await fetch(`/api/artist-detail/${artistId}`);
if (!response.ok) {
throw new Error(`Failed to load artist data: ${response.statusText}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Failed to load artist data');
}
console.log(`✅ Loaded artist detail data:`, data);
// Hide loading and show main content
showArtistDetailLoading(false);
showArtistDetailMain(true);
console.log(`🎨 Main content visibility:`, document.getElementById('artist-detail-main'));
console.log(`🎨 Albums section:`, document.getElementById('albums-section'));
// Populate the page with data
populateArtistDetailPage(data);
} catch (error) {
console.error(`❌ Error loading artist detail data:`, error);
// Show error state
showArtistDetailLoading(false);
showArtistDetailError(true, error.message);
showToast(`Failed to load artist details: ${error.message}`, "error");
}
}
function updateArtistDetailHeader(artistName) {
// Update header title
const headerTitle = document.getElementById("artist-detail-name");
if (headerTitle) {
headerTitle.textContent = artistName;
}
// Update main artist name
const mainTitle = document.getElementById("artist-info-name");
if (mainTitle) {
mainTitle.textContent = artistName;
}
}
function populateArtistDetailPage(data) {
const artist = data.artist;
const discography = data.discography;
console.log(`🎨 Populating artist detail page for: ${artist.name}`);
console.log(`📀 Discography data:`, discography);
console.log(`📀 Albums:`, discography.albums);
console.log(`📀 EPs:`, discography.eps);
console.log(`📀 Singles:`, discography.singles);
// Update hero section with image, name, and stats
updateArtistHeroSection(artist, discography);
// Update genres (if element exists)
updateArtistGenres(artist.genres);
// Update summary stats (if element exists)
updateArtistSummaryStats(discography);
// Populate discography sections
populateDiscographySections(discography);
}
function updateArtistDetailImage(imageUrl, artistName) {
const imageElement = document.getElementById("artist-detail-image");
const fallbackElement = document.getElementById("artist-image-fallback");
if (imageUrl && imageUrl.trim() !== "") {
imageElement.src = imageUrl;
imageElement.alt = artistName;
imageElement.classList.remove("hidden");
fallbackElement.classList.add("hidden");
imageElement.onerror = () => {
console.log(`Failed to load artist image for ${artistName}: ${imageUrl}`);
// Replace with fallback on error
imageElement.classList.add("hidden");
fallbackElement.classList.remove("hidden");
};
imageElement.onload = () => {
console.log(`Successfully loaded artist image for ${artistName}: ${imageUrl}`);
};
} else {
console.log(`No image URL for ${artistName}: '${imageUrl}'`);
imageElement.classList.add("hidden");
fallbackElement.classList.remove("hidden");
}
}
function updateArtistGenres(genres) {
const genresContainer = document.getElementById("artist-genres");
if (!genresContainer) return;
genresContainer.innerHTML = "";
if (genres && genres.length > 0) {
genres.forEach(genre => {
const genreTag = document.createElement("span");
genreTag.className = "genre-tag";
genreTag.textContent = genre;
genresContainer.appendChild(genreTag);
});
}
}
function updateArtistSummaryStats(discography) {
// Calculate stats
const ownedAlbums = discography.albums.filter(album => album.owned).length;
const missingAlbums = discography.albums.filter(album => !album.owned).length;
const totalAlbums = discography.albums.length;
const completionPercentage = totalAlbums > 0 ? Math.round((ownedAlbums / totalAlbums) * 100) : 0;
// Update owned albums count
const ownedElement = document.getElementById("owned-albums-count");
if (ownedElement) {
ownedElement.textContent = ownedAlbums;
}
// Update missing albums count
const missingElement = document.getElementById("missing-albums-count");
if (missingElement) {
missingElement.textContent = missingAlbums;
}
// Update completion percentage
const completionElement = document.getElementById("completion-percentage");
if (completionElement) {
completionElement.textContent = `${completionPercentage}%`;
}
}
function updateArtistHeaderStats(albumCount, trackCount) {
// This function is deprecated - now using updateArtistHeroSection
console.log("📊 Using new hero section instead of old header stats");
}
function updateArtistHeroSection(artist, discography) {
console.log("🖼️ Updating artist hero section");
// Update artist image with detailed debugging
const imageElement = document.getElementById("artist-detail-image");
const fallbackElement = document.getElementById("artist-detail-image-fallback");
console.log(`🖼️ Debug Artist image info:`);
console.log(` - URL: '${artist.image_url}'`);
console.log(` - Type: ${typeof artist.image_url}`);
console.log(` - Full artist object:`, artist);
console.log(` - Image element:`, imageElement);
console.log(` - Fallback element:`, fallbackElement);
if (artist.image_url && artist.image_url.trim() !== "" && artist.image_url !== "null") {
console.log(`✅ Setting image src to: ${artist.image_url}`);
imageElement.src = artist.image_url;
imageElement.alt = artist.name;
imageElement.style.display = "block";
if (fallbackElement) {
fallbackElement.style.display = "none";
}
imageElement.onload = () => {
console.log(`✅ Successfully loaded artist image: ${artist.image_url}`);
};
imageElement.onerror = () => {
console.error(`❌ Failed to load artist image: ${artist.image_url}`);
imageElement.style.display = "none";
if (fallbackElement) {
fallbackElement.style.display = "flex";
}
};
} else {
console.log(`🖼️ No valid image URL - showing fallback for ${artist.name}`);
imageElement.style.display = "none";
if (fallbackElement) {
fallbackElement.style.display = "flex";
}
}
// Update artist name
const nameElement = document.getElementById("artist-detail-name");
if (nameElement) {
nameElement.textContent = artist.name;
}
// Calculate and update stats for each category
updateCategoryStats('albums', discography.albums);
updateCategoryStats('eps', discography.eps);
updateCategoryStats('singles', discography.singles);
}
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;
console.log(`📊 ${category}: ${owned} owned, ${missing} missing, ${completion}% complete`);
// Update stats text
const statsElement = document.getElementById(`${category}-stats`);
if (statsElement) {
statsElement.textContent = `${owned} owned, ${missing} missing`;
}
// Update completion bar
const fillElement = document.getElementById(`${category}-completion-fill`);
if (fillElement) {
fillElement.style.width = `${completion}%`;
}
// Update completion text
const textElement = document.getElementById(`${category}-completion-text`);
if (textElement) {
textElement.textContent = `${completion}%`;
}
}
function populateDiscographySections(discography) {
// Populate albums
populateReleaseSection('albums', discography.albums);
// Populate EPs
populateReleaseSection('eps', discography.eps);
// Populate singles
populateReleaseSection('singles', discography.singles);
}
function populateReleaseSection(sectionType, releases) {
const gridId = `${sectionType}-grid`;
const ownedCountId = `${sectionType}-owned-count`;
const missingCountId = `${sectionType}-missing-count`;
const grid = document.getElementById(gridId);
if (!grid) return;
// Clear existing content
grid.innerHTML = "";
// Calculate stats
const ownedCount = releases.filter(release => release.owned).length;
const missingCount = releases.filter(release => !release.owned).length;
// Update section stats
const ownedElement = document.getElementById(ownedCountId);
const missingElement = document.getElementById(missingCountId);
if (ownedElement) {
ownedElement.textContent = `${ownedCount} owned`;
}
if (missingElement) {
missingElement.textContent = `${missingCount} missing`;
}
// Create release cards
releases.forEach((release, index) => {
console.log(`📀 Creating card ${index + 1} for: ${release.title}`);
const card = createReleaseCard(release);
grid.appendChild(card);
console.log(`📀 Added card to grid:`, card);
});
console.log(`📀 Populated ${sectionType} section: ${ownedCount} owned, ${missingCount} missing`);
console.log(`📀 Grid element:`, grid);
console.log(`📀 Grid children count:`, grid.children.length);
}
function createReleaseCard(release) {
const card = document.createElement("div");
card.className = `release-card${release.owned ? "" : " missing"}`;
card.setAttribute("data-release-id", release.id || "");
card.setAttribute("data-spotify-id", release.spotify_id || "");
// Create image
const imageContainer = document.createElement("div");
if (release.image_url && release.image_url.trim() !== "") {
const img = document.createElement("img");
img.src = release.image_url;
img.alt = release.title;
img.className = "release-image";
img.onerror = () => {
imageContainer.innerHTML = `<div class="release-image-fallback">💿</div>`;
};
imageContainer.appendChild(img);
} else {
imageContainer.innerHTML = `<div class="release-image-fallback">💿</div>`;
}
// Create title
const title = document.createElement("h4");
title.className = "release-title";
title.textContent = release.title;
title.title = release.title;
// Create year
const year = document.createElement("div");
year.className = "release-year";
year.textContent = release.year || "Unknown Year";
// Create completion info
const completion = document.createElement("div");
completion.className = "release-completion";
const completionText = document.createElement("span");
const completionBar = document.createElement("div");
completionBar.className = "completion-bar";
const completionFill = document.createElement("div");
completionFill.className = "completion-fill";
if (release.owned) {
const percentage = release.track_completion || 100;
completionFill.style.width = `${percentage}%`;
if (percentage === 100) {
completionText.textContent = "Complete";
completionText.className = "completion-text complete";
completionFill.className += " complete";
} else {
completionText.textContent = `${percentage}%`;
completionText.className = "completion-text partial";
completionFill.className += " partial";
}
} else {
completionText.textContent = "Missing";
completionText.className = "completion-text missing";
completionFill.className += " missing";
completionFill.style.width = "0%";
}
completionBar.appendChild(completionFill);
completion.appendChild(completionText);
completion.appendChild(completionBar);
// Assemble card
card.appendChild(imageContainer);
card.appendChild(title);
card.appendChild(year);
card.appendChild(completion);
// Add click handler for future functionality
card.addEventListener("click", () => {
console.log(`Clicked on release: ${release.title} (Owned: ${release.owned})`);
if (release.owned) {
showToast(`Album details coming soon!`, "info");
} else {
showToast(`Add to download queue coming soon!`, "info");
}
});
return card;
}
// UI state management functions
function showArtistDetailLoading(show) {
const loadingElement = document.getElementById("artist-detail-loading");
if (loadingElement) {
if (show) {
loadingElement.classList.remove("hidden");
} else {
loadingElement.classList.add("hidden");
}
}
}
function showArtistDetailError(show, message = "") {
const errorElement = document.getElementById("artist-detail-error");
const errorMessageElement = document.getElementById("artist-detail-error-message");
if (errorElement) {
if (show) {
errorElement.classList.remove("hidden");
if (errorMessageElement && message) {
errorMessageElement.textContent = message;
}
} else {
errorElement.classList.add("hidden");
}
}
}
function showArtistDetailMain(show) {
const mainElement = document.getElementById("artist-detail-main");
if (mainElement) {
if (show) {
mainElement.classList.remove("hidden");
} else {
mainElement.classList.add("hidden");
}
}
}

View file

@ -8870,3 +8870,670 @@ body {
font-size: 12px;
}
}
/* ========================================
Artist Detail Page Styles
======================================== */
.artist-detail-header {
display: flex;
align-items: center;
gap: 20px;
width: 100%;
}
.back-btn {
background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%);
backdrop-filter: blur(12px) saturate(1.1);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 12px 20px;
color: #ffffff;
font-family: 'SF Pro Text', -apple-system, sans-serif;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);
text-decoration: none;
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.back-btn:hover {
background: linear-gradient(135deg, rgba(40, 40, 40, 0.95) 0%, rgba(30, 30, 30, 0.98) 100%);
border-color: rgba(255, 255, 255, 0.15);
transform: translateY(-1px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
}
.artist-detail-title {
flex-grow: 1;
min-width: 0;
}
.artist-detail-title h2 {
font-size: 28px;
font-weight: 700;
color: #ffffff;
margin: 0 0 8px 0;
line-height: 1.2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.artist-detail-stats {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
.artist-stat {
color: #b3b3b3;
font-size: 14px;
font-weight: 500;
}
/* Artist Detail Content */
.artist-detail-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.artist-detail-loading,
.artist-detail-error {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
text-align: center;
padding: 40px 20px;
}
.artist-detail-loading .loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-top: 3px solid #1db954;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
.artist-detail-error .error-icon {
font-size: 48px;
margin-bottom: 16px;
}
.artist-detail-error h3 {
color: #ffffff;
font-size: 20px;
margin: 0 0 8px 0;
}
.artist-detail-error p {
color: #b3b3b3;
margin: 0 0 20px 0;
}
.retry-btn {
background: linear-gradient(135deg, #1db954 0%, #1ed760 100%);
border: none;
border-radius: 12px;
padding: 12px 24px;
color: #ffffff;
font-family: 'SF Pro Text', -apple-system, sans-serif;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);
}
.retry-btn:hover {
background: linear-gradient(135deg, #1ed760 0%, #1db954 100%);
transform: translateY(-1px);
box-shadow: 0 8px 25px rgba(29, 185, 84, 0.3);
}
/* Artist Info Section */
.artist-info-section {
display: flex;
gap: 30px;
padding: 30px 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 30px;
background: linear-gradient(135deg, rgba(26, 26, 26, 0.3) 0%, rgba(18, 18, 18, 0.5) 100%);
border-radius: 20px;
padding: 32px;
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.artist-image-container {
position: relative;
flex-shrink: 0;
}
.artist-detail-image {
width: 200px;
height: 200px;
border-radius: 16px;
object-fit: cover;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.artist-image-fallback {
width: 200px;
height: 200px;
border-radius: 16px;
background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%);
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 64px;
}
.artist-info {
flex: 1;
min-width: 0;
}
.artist-info h1 {
font-size: 48px;
font-weight: 800;
color: #ffffff;
margin: 0 0 12px 0;
line-height: 1.1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.artist-genres {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 24px;
}
.genre-tag {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 20px;
padding: 6px 14px;
color: #ffffff;
font-size: 12px;
font-weight: 500;
text-transform: capitalize;
}
.artist-summary-stats {
display: flex;
gap: 40px;
flex-wrap: wrap;
}
.summary-stat {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.stat-number {
font-size: 32px;
font-weight: 800;
color: #1db954;
line-height: 1;
margin-bottom: 6px;
}
.stat-label {
font-size: 14px;
color: #b3b3b3;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Discography Sections */
.discography-sections {
display: flex;
flex-direction: column;
gap: 40px;
}
.discography-section {
opacity: 1; /* Show immediately instead of animation for now */
}
/* Animation keyframes for future use */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.section-header h3 {
font-size: 24px;
font-weight: 700;
color: #ffffff;
margin: 0;
}
.section-stats {
display: flex;
gap: 16px;
color: #b3b3b3;
font-size: 14px;
font-weight: 500;
}
.releases-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 16px;
padding: 8px 0;
}
/* Release Cards */
.release-card {
background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%);
backdrop-filter: blur(12px) saturate(1.1);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 14px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
height: 280px; /* Fixed height for consistent layout */
}
.release-card:hover {
background: linear-gradient(135deg, rgba(40, 40, 40, 0.95) 0%, rgba(30, 30, 30, 0.98) 100%);
border-color: rgba(255, 255, 255, 0.15);
transform: translateY(-4px);
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.4);
}
.release-card.missing {
opacity: 0.6;
background: linear-gradient(135deg, rgba(60, 60, 60, 0.3) 0%, rgba(40, 40, 40, 0.5) 100%);
border-color: rgba(255, 255, 255, 0.05);
}
.release-card.missing:hover {
opacity: 0.8;
background: linear-gradient(135deg, rgba(70, 70, 70, 0.4) 0%, rgba(50, 50, 50, 0.6) 100%);
}
.release-image {
width: 100%;
aspect-ratio: 1;
border-radius: 12px;
object-fit: cover;
margin-bottom: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
}
.release-image-fallback {
width: 100%;
aspect-ratio: 1;
border-radius: 12px;
background: linear-gradient(135deg, rgba(60, 60, 60, 0.6) 0%, rgba(40, 40, 40, 0.8) 100%);
border: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
margin-bottom: 12px;
flex-shrink: 0;
color: rgba(255, 255, 255, 0.6);
}
.release-title {
font-size: 16px;
font-weight: 600;
color: #ffffff;
margin: 0 0 6px 0;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.release-year {
font-size: 14px;
color: #b3b3b3;
margin: 0 0 8px 0;
}
.release-completion {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 11px;
margin-top: auto;
padding-top: 8px;
border-top: 1px solid rgba(255, 255, 255, 0.05);
}
.completion-text {
color: #b3b3b3;
}
.completion-text.complete {
color: #1db954;
font-weight: 600;
}
.completion-text.partial {
color: #ffa500;
font-weight: 600;
}
.completion-text.missing {
color: #ff6b6b;
font-weight: 600;
}
.completion-bar {
width: 60px;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
overflow: hidden;
}
.completion-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
}
.completion-fill.complete {
background: linear-gradient(90deg, #1db954, #1ed760);
}
.completion-fill.partial {
background: linear-gradient(90deg, #ffa500, #ffb347);
}
.completion-fill.missing {
background: linear-gradient(90deg, #ff6b6b, #ff8e8e);
width: 0%;
}
/* Responsive Design */
@media (max-width: 768px) {
.artist-detail-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.artist-detail-title h2 {
font-size: 24px;
white-space: normal;
}
.artist-info-section {
flex-direction: column;
align-items: center;
text-align: center;
gap: 20px;
}
.artist-detail-image,
.artist-image-fallback {
width: 150px;
height: 150px;
}
.artist-info h1 {
font-size: 36px;
white-space: normal;
text-align: center;
}
.artist-summary-stats {
justify-content: center;
gap: 30px;
}
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.releases-grid {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 16px;
}
.release-card {
padding: 12px;
}
}
/* ===============================
ARTIST HERO SECTION (Modal-inspired design)
=============================== */
.artist-hero-section {
background: linear-gradient(135deg, rgba(26, 26, 26, 0.95) 0%, rgba(18, 18, 18, 0.98) 100%);
backdrop-filter: blur(12px) saturate(1.1);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
margin: 20px 20px 20px 20px;
padding: 20px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
}
.artist-hero-content {
display: flex;
align-items: center;
gap: 20px;
}
.artist-image-container {
position: relative;
flex-shrink: 0;
}
.artist-image {
width: 100px;
height: 100px;
border-radius: 12px;
object-fit: cover;
border: 2px solid rgba(29, 185, 84, 0.3);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
display: block;
}
.artist-image-fallback {
width: 100px;
height: 100px;
border-radius: 12px;
background: linear-gradient(135deg, rgba(29, 185, 84, 0.2) 0%, rgba(30, 215, 96, 0.1) 100%);
border: 2px solid rgba(29, 185, 84, 0.3);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
color: rgba(255, 255, 255, 0.6);
position: absolute;
top: 0;
left: 0;
}
.artist-image:not([src=""]):not([src]) + .artist-image-fallback,
.artist-image[src]:not([src=""]) + .artist-image-fallback {
display: none;
}
.artist-info {
flex: 1;
min-width: 0;
}
.artist-name {
font-size: 32px;
font-weight: 700;
color: #ffffff;
margin: 0 0 16px 0;
line-height: 1.1;
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
letter-spacing: -0.3px;
}
.collection-overview {
display: flex;
flex-direction: column;
gap: 12px;
}
.collection-category {
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 12px 16px;
transition: all 0.3s ease;
}
.collection-category:hover {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(29, 185, 84, 0.2);
}
.category-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.category-label {
color: #ffffff;
font-weight: 700;
font-size: 16px;
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
}
.category-stats {
color: #b3b3b3;
font-size: 14px;
font-weight: 500;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
}
.completion-section {
display: flex;
align-items: center;
gap: 16px;
}
.completion-bar {
flex: 1;
height: 8px;
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
overflow: hidden;
position: relative;
}
.completion-fill {
height: 100%;
background: linear-gradient(90deg, #1db954 0%, #1ed760 100%);
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 4px;
position: relative;
}
.completion-fill::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.2) 50%, transparent 100%);
animation: shimmer 2s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
.completion-text {
color: #ffffff;
font-size: 16px;
font-weight: 700;
min-width: 45px;
text-align: right;
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.artist-hero-section {
margin: 15px;
padding: 20px;
}
.artist-hero-content {
flex-direction: column;
align-items: center;
text-align: center;
gap: 20px;
}
.artist-image,
.artist-image-fallback {
width: 120px;
height: 120px;
}
.artist-name {
font-size: 32px;
text-align: center;
}
.category-header {
flex-direction: column;
align-items: flex-start;
gap: 4px;
margin-bottom: 8px;
}
.completion-section {
gap: 12px;
}
}