// ARTISTS PAGE FUNCTIONALITY - ELEGANT SEARCH & DISCOVERY // ============================================================================ /** * Initialize the artists page when navigated to (only runs once) */ function initializeArtistsPage() { console.log('đŸŽĩ Initializing Artists Page (first time)'); // Get DOM elements const searchInput = document.getElementById('artists-search-input'); const headerSearchInput = document.getElementById('artists-header-search-input'); const searchStatus = document.getElementById('artists-search-status'); const backButton = document.getElementById('artists-back-button'); const detailBackButton = document.getElementById('artist-detail-back-button'); // Set up event listeners (only need to do this once) if (searchInput) { searchInput.addEventListener('input', handleArtistsSearchInput); searchInput.addEventListener('keypress', handleArtistsSearchKeypress); } if (headerSearchInput) { headerSearchInput.addEventListener('input', handleArtistsHeaderSearchInput); headerSearchInput.addEventListener('keypress', handleArtistsSearchKeypress); } if (backButton) { backButton.addEventListener('click', () => showArtistsSearchState()); } if (detailBackButton) { detailBackButton.addEventListener('click', () => { // If the user searched within the Artists page, back returns to the // results list so they can pick a different artist. if (artistsPageState.searchResults && artistsPageState.searchResults.length > 0) { showArtistsResultsState(); return; } // Otherwise the user reached this detail view from elsewhere (Search, // Discover, watchlist, etc.). The Artists page is no longer a sidebar // entry, so there's nothing useful to fall back to here — let the // browser take them back to wherever they came from, or drop them on // Search (the go-forward way to find another artist). if (window.history.length > 1) { window.history.back(); } else { navigateToPage('search'); } }); } // Initialize tabs (only need to do this once) initializeArtistTabs(); // Mark as initialized artistsPageState.isInitialized = true; // Restore previous state instead of always resetting to search restoreArtistsPageState(); console.log('✅ Artists Page initialized successfully (ready for navigation)'); } /** * Restore the artists page to its previous state */ function restoreArtistsPageState() { console.log(`🔄 Restoring artists page state: ${artistsPageState.currentView}`); switch (artistsPageState.currentView) { case 'results': // Restore search results state if (artistsPageState.searchQuery && artistsPageState.searchResults.length > 0) { console.log(`đŸ“Ļ Restoring search results for: "${artistsPageState.searchQuery}"`); // Restore search input values const searchInput = document.getElementById('artists-search-input'); const headerSearchInput = document.getElementById('artists-header-search-input'); if (searchInput) searchInput.value = artistsPageState.searchQuery; if (headerSearchInput) headerSearchInput.value = artistsPageState.searchQuery; // Display the cached results displayArtistsResults(artistsPageState.searchQuery, artistsPageState.searchResults); } else { // No valid results state, fall back to search showArtistsSearchState(); } break; case 'detail': // Restore artist detail state if (artistsPageState.selectedArtist && artistsPageState.artistDiscography) { console.log(`🎤 Restoring artist detail for: ${artistsPageState.selectedArtist.name}`); // First restore search results if they exist if (artistsPageState.searchQuery && artistsPageState.searchResults.length > 0) { const searchInput = document.getElementById('artists-search-input'); const headerSearchInput = document.getElementById('artists-header-search-input'); if (searchInput) searchInput.value = artistsPageState.searchQuery; if (headerSearchInput) headerSearchInput.value = artistsPageState.searchQuery; } // Show artist detail state showArtistDetailState(); // Update artist info in header updateArtistDetailHeader(artistsPageState.selectedArtist); // Display cached discography if (artistsPageState.artistDiscography.albums || artistsPageState.artistDiscography.singles) { displayArtistDiscography(artistsPageState.artistDiscography); // Restore cached completion data instead of re-scanning restoreCachedCompletionData(artistsPageState.selectedArtist.id); } } else { // No valid detail state, fall back to search or results if (artistsPageState.searchQuery && artistsPageState.searchResults.length > 0) { displayArtistsResults(artistsPageState.searchQuery, artistsPageState.searchResults); } else { showArtistsSearchState(); } } break; default: case 'search': // Show search state (but preserve any existing search query) if (artistsPageState.searchQuery) { const searchInput = document.getElementById('artists-search-input'); if (searchInput) searchInput.value = artistsPageState.searchQuery; } showArtistsSearchState(); break; } } /** * Handle search input with debouncing */ function handleArtistsSearchInput(event) { const query = event.target.value.trim(); updateArtistsSearchStatus('searching'); // Clear existing timeout if (artistsSearchTimeout) { clearTimeout(artistsSearchTimeout); } // Cancel any active search if (artistsSearchController) { artistsSearchController.abort(); } if (query === '') { updateArtistsSearchStatus('default'); return; } // Set up new debounced search artistsSearchTimeout = setTimeout(() => { performArtistsSearch(query); }, 1000); // 1 second debounce } /** * Handle header search input (already in results state) */ function handleArtistsHeaderSearchInput(event) { const query = event.target.value.trim(); // Update main search input to match const mainInput = document.getElementById('artists-search-input'); if (mainInput) { mainInput.value = query; } // Trigger search with same debouncing logic handleArtistsSearchInput(event); } /** * Handle Enter key press in search inputs */ function handleArtistsSearchKeypress(event) { if (event.key === 'Enter') { event.preventDefault(); const query = event.target.value.trim(); if (query && query !== artistsPageState.searchQuery) { // Clear timeout and search immediately if (artistsSearchTimeout) { clearTimeout(artistsSearchTimeout); } performArtistsSearch(query); } } } /** * Perform artist search with API call */ async function performArtistsSearch(query) { console.log(`🔍 Searching for artists: "${query}"`); // Check cache first if (artistsPageState.cache.searches[query]) { console.log('đŸ“Ļ Using cached search results'); displayArtistsResults(query, artistsPageState.cache.searches[query]); return; } // Update status updateArtistsSearchStatus('searching'); // Show loading cards immediately if we're in results view if (artistsPageState.currentView === 'results') { showSearchLoadingCards(); } try { // Set up abort controller artistsSearchController = new AbortController(); const response = await fetch('/api/match/search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: query, context: 'artist' }), signal: artistsSearchController.signal }); if (!response.ok) { throw new Error(`Search failed: ${response.status}`); } const data = await response.json(); console.log(`✅ Found ${data.results?.length || 0} artists`); // Transform the results to flatten the nested artist data const transformedResults = (data.results || []).map(result => { // Extract artist data from the nested structure const artist = result.artist || result; return { id: artist.id, name: artist.name, image_url: artist.image_url, genres: artist.genres, popularity: artist.popularity, confidence: result.confidence || 0 }; }); console.log('🔧 Transformed results:', transformedResults); // Cache the transformed results artistsPageState.cache.searches[query] = transformedResults; // Display results displayArtistsResults(query, transformedResults); } catch (error) { if (error.name !== 'AbortError') { console.error('❌ Artist search failed:', error); // Provide specific error messages based on the error type let errorMessage = 'Search failed. Please try again.'; if (error.message.includes('401') || error.message.includes('authentication')) { errorMessage = 'Spotify not authenticated. Please check your API settings.'; } else if (error.message.includes('network') || error.message.includes('fetch')) { errorMessage = 'Network error. Please check your connection.'; } else if (error.message.includes('timeout')) { errorMessage = 'Search timed out. Please try again.'; } updateArtistsSearchStatus('error', errorMessage); } } finally { artistsSearchController = null; } } /** * Display artist search results */ function displayArtistsResults(query, results) { console.log(`📊 Displaying ${results.length} artist results`); // Update state artistsPageState.searchQuery = query; artistsPageState.searchResults = results; artistsPageState.currentView = 'results'; // Update header search input if different const headerInput = document.getElementById('artists-header-search-input'); if (headerInput && headerInput.value !== query) { headerInput.value = query; } // Show results state showArtistsResultsState(); // Populate results const container = document.getElementById('artists-cards-container'); if (!container) return; if (results.length === 0) { container.innerHTML = `
🔍
No artists found
Try a different search term
`; return; } // Create artist cards container.innerHTML = results.map(result => createArtistCardHTML(result)).join(''); observeLazyBackgrounds(container); // Add event listeners to cards container.querySelectorAll('.artist-card').forEach((card, index) => { card.addEventListener('click', () => selectArtistForDetail(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); }); } }); // Update watchlist status for all cards updateArtistCardWatchlistStatus(); // Lazy load missing artist images console.log('đŸ–ŧī¸ Starting lazy load for artist images on Artists page...'); if (typeof lazyLoadArtistImages === 'function') { lazyLoadArtistImages(container); } else if (typeof window.lazyLoadArtistImages === 'function') { window.lazyLoadArtistImages(container); } else { console.error('❌ lazyLoadArtistImages function not found!'); } // Add mouse wheel horizontal scrolling container.addEventListener('wheel', (event) => { if (event.deltaY !== 0) { event.preventDefault(); container.scrollLeft += event.deltaY; } }); } /** * Lazy load artist images for cards that don't have images yet. * Fetches images asynchronously so search results appear immediately. */ async function lazyLoadArtistImages(container) { if (!container) { console.error('❌ lazyLoadArtistImages: container is null'); return; } // Find all cards that need images const cardsNeedingImages = container.querySelectorAll('[data-needs-image="true"]'); if (cardsNeedingImages.length === 0) { console.log('✅ All artist cards have images'); return; } console.log(`đŸ–ŧī¸ Lazy loading images for ${cardsNeedingImages.length} artist cards`); // Load images in parallel (but with a small batch to avoid overwhelming the server) const batchSize = 5; const cards = Array.from(cardsNeedingImages); for (let i = 0; i < cards.length; i += batchSize) { const batch = cards.slice(i, i + batchSize); await Promise.all(batch.map(async (card) => { const artistId = card.dataset.artistId; if (!artistId) { console.warn('âš ī¸ Card missing artistId:', card); return; } try { console.log(`🔄 Fetching image for artist ${artistId}...`); const response = await fetch(`/api/artist/${artistId}/image`); const data = await response.json(); console.log(`đŸ“Ĩ Got response for ${artistId}:`, data); if (data.success && data.image_url) { // Update the card's background image // Handle both card types (suggestion-card and artist-card) if (card.classList.contains('suggestion-card')) { card.style.backgroundImage = `url(${data.image_url})`; card.style.backgroundSize = 'cover'; card.style.backgroundPosition = 'center'; } else if (card.classList.contains('artist-card')) { const bgElement = card.querySelector('.artist-card-background'); if (bgElement) { // Clear the gradient first, then set the image bgElement.style.cssText = `background-image: url('${data.image_url}'); background-size: cover; background-position: center;`; } } card.dataset.needsImage = 'false'; console.log(`✅ Loaded image for artist ${artistId}`); } } catch (error) { console.error(`❌ Failed to load image for artist ${artistId}:`, error); } })); } console.log('✅ Finished lazy loading artist images'); } // Make function globally accessible window.lazyLoadArtistImages = lazyLoadArtistImages; /** * Create HTML for an artist card */ function createArtistCardHTML(artist) { const imageUrl = artist.image_url || ''; const genres = artist.genres && artist.genres.length > 0 ? artist.genres.slice(0, 3).join(', ') : 'Various genres'; const popularity = artist.popularity || 0; // Use data-bg-src for lazy background loading via IntersectionObserver const backgroundAttr = imageUrl ? `data-bg-src="${imageUrl}"` : `style="background: linear-gradient(135deg, rgba(29, 185, 84, 0.3) 0%, rgba(24, 156, 71, 0.2) 100%);"`; // Format popularity as a percentage for better UX const popularityText = popularity > 0 ? `${popularity}% Popular` : 'Popularity Unknown'; // Track if image needs to be lazy loaded const needsImage = imageUrl ? 'false' : 'true'; // Check for MusicBrainz ID let mbIconHTML = ''; if (artist.musicbrainz_id) { mbIconHTML = `
`; } return `
${mbIconHTML}
${escapeHtml(artist.name)}
${escapeHtml(genres)}
đŸ”Ĩ ${popularityText}
`; } /** * Select an artist and show their discography */ async function selectArtistForDetail(artist, options = {}) { console.log(`🎤 Selected artist: ${artist.name}`); // Cancel any ongoing completion check from previous artist if (artistCompletionController) { console.log('âšī¸ Canceling previous artist completion check'); artistCompletionController.abort(); artistCompletionController = null; } // Cancel any ongoing similar artists stream from previous artist if (similarArtistsController) { console.log('âšī¸ Canceling previous similar artists stream'); similarArtistsController.abort(); similarArtistsController = null; } // Update state artistsPageState.selectedArtist = artist; artistsPageState.currentView = 'detail'; artistsPageState.sourceOverride = options.source || artist.source || null; artistsPageState.pluginOverride = options.plugin || null; // Show detail state showArtistDetailState(); // Update artist info in header updateArtistDetailHeader(artist); // Load discography (pass artist name for cross-source fallback) await loadArtistDiscography(artist.id, artist.name, artistsPageState.sourceOverride, options.plugin); } /** * Load artist's discography from Spotify or iTunes * @param {string} artistId - Artist ID (Spotify or iTunes format) * @param {string} [artistName] - Optional artist name for fallback searches */ async function loadArtistDiscography(artistId, artistName = null, sourceOverride = null, pluginOverride = null) { console.log(`đŸ’ŋ Loading discography for artist: ${artistId} (name: ${artistName}, source: ${sourceOverride || 'auto'})`); // Use source-prefixed cache key to avoid ID collisions between sources const cacheKey = sourceOverride ? `${sourceOverride}:${artistId}` : artistId; // Check cache first if (artistsPageState.cache.discography[cacheKey]) { console.log('đŸ“Ļ Using cached discography'); const cachedDiscography = artistsPageState.cache.discography[cacheKey]; if (artistsPageState.selectedArtist) { artistsPageState.selectedArtist = { ...artistsPageState.selectedArtist, source: cachedDiscography.source || sourceOverride || artistsPageState.selectedArtist.source || null, }; } artistsPageState.sourceOverride = cachedDiscography.source || sourceOverride || artistsPageState.sourceOverride || null; displayArtistDiscography(cachedDiscography); // Load similar artists in parallel (don't wait) — always uses primary source loadSimilarArtists(artistsPageState.selectedArtist?.name).catch(err => { console.error('❌ Error loading similar artists:', err); }); // Still check completion status for cached data await checkDiscographyCompletion(artistId, cachedDiscography); return; } try { // Show loading states showDiscographyLoading(); // Build URL with optional artist name and source override for fallback let url = `/api/artist/${artistId}/discography`; const params = new URLSearchParams(); if (artistName) params.set('artist_name', artistName); if (sourceOverride) params.set('source', sourceOverride); if (pluginOverride) params.set('plugin', pluginOverride); if (params.toString()) url += `?${params.toString()}`; // Call the real API endpoint const response = await fetch(url); if (!response.ok) { if (response.status === 401) { throw new Error('Spotify not authenticated. Please check your API settings.'); } throw new Error(`Failed to load discography: ${response.status}`); } const data = await response.json(); if (data.error) { throw new Error(data.error); } const discography = { albums: data.albums || [], singles: data.singles || [], source: data.source || sourceOverride || null, }; // Keep the resolved metadata source on the selected artist so album clicks // can pass it through to /api/album//tracks. if (artistsPageState.selectedArtist) { artistsPageState.selectedArtist = { ...artistsPageState.selectedArtist, source: discography.source, }; } artistsPageState.sourceOverride = discography.source || artistsPageState.sourceOverride || null; // Update selected artist with full details from backend (includes MusicBrainz ID) if (data.artist) { console.log('✨ Updating artist details with fresh data from backend'); artistsPageState.selectedArtist = { ...artistsPageState.selectedArtist, ...data.artist }; } // Merge artist_info enrichment from discography response if (data.artist_info) { artistsPageState.selectedArtist = { ...artistsPageState.selectedArtist, artist_info: data.artist_info, }; } // Refresh header with all available data updateArtistDetailHeader(artistsPageState.selectedArtist); console.log(`✅ Loaded ${discography.albums.length} albums and ${discography.singles.length} singles`); // Cache the results (use source-prefixed key if source override active) artistsPageState.cache.discography[cacheKey] = discography; artistsPageState.artistDiscography = discography; // Display results displayArtistDiscography(discography); // Load similar artists and check completion in parallel (don't wait) loadSimilarArtists(artistsPageState.selectedArtist?.name).catch(err => { console.error('❌ Error loading similar artists:', err); }); // Check completion status for all albums and singles await checkDiscographyCompletion(artistId, discography); } catch (error) { console.error('❌ Failed to load discography:', error); showDiscographyError(error.message); } } /** * Display artist's discography in tabs */ function displayArtistDiscography(discography) { console.log(`📀 Displaying discography: ${discography.albums?.length || 0} albums, ${discography.singles?.length || 0} singles`); // Show Download Discography button(s) if there are any releases const _totalReleases = (discography.albums?.length || 0) + (discography.eps?.length || 0) + (discography.singles?.length || 0); const _discogWrap = document.getElementById('discog-download-wrap'); if (_discogWrap) _discogWrap.style.display = _totalReleases > 0 ? '' : 'none'; const _discogBtnArtists = document.getElementById('discog-download-btn-artists'); if (_discogBtnArtists) _discogBtnArtists.style.display = _totalReleases > 0 ? '' : 'none'; // Populate albums const albumsContainer = document.getElementById('album-cards-container'); if (albumsContainer) { if (discography.albums?.length > 0) { albumsContainer.innerHTML = discography.albums.map(album => createAlbumCardHTML(album)).join(''); observeLazyBackgrounds(albumsContainer); // Add dynamic glow effects and click handlers 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); }); } // Add click handler for download missing tracks modal card.addEventListener('click', () => handleArtistAlbumClick(album, 'albums')); card.style.cursor = 'pointer'; }); } else { albumsContainer.innerHTML = `
đŸ’ŋ
No albums found
`; } } // Populate singles const singlesContainer = document.getElementById('singles-cards-container'); if (singlesContainer) { if (discography.singles?.length > 0) { singlesContainer.innerHTML = discography.singles.map(single => createAlbumCardHTML(single)).join(''); observeLazyBackgrounds(singlesContainer); // Add dynamic glow effects and click handlers 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); }); } // Add click handler for download missing tracks modal card.addEventListener('click', () => handleArtistAlbumClick(single, 'singles')); card.style.cursor = 'pointer'; }); } else { singlesContainer.innerHTML = `
đŸŽĩ
No singles or EPs found
`; } } // Auto-switch to Singles tab if no albums but has singles if ((!discography.albums || discography.albums.length === 0) && discography.singles && discography.singles.length > 0) { console.log('📀 No albums found, auto-switching to Singles & EPs tab'); // Switch to singles tab const albumsTab = document.getElementById('albums-tab'); const singlesTab = document.getElementById('singles-tab'); const albumsContent = document.getElementById('albums-content'); const singlesContent = document.getElementById('singles-content'); if (albumsTab && singlesTab && albumsContent && singlesContent) { // Remove active from albums albumsTab.classList.remove('active'); albumsContent.classList.remove('active'); // Add active to singles singlesTab.classList.add('active'); singlesContent.classList.add('active'); } } } /** * Load similar artists from MusicMap */ /** * Restore cached completion data without re-scanning the database */ function restoreCachedCompletionData(artistId) { console.log(`đŸ“Ļ Restoring cached completion data for artist: ${artistId}`); const cachedData = artistsPageState.cache.completionData[artistId]; if (!cachedData) { console.log('âš ī¸ No cached completion data found, skipping restoration'); return; } // Restore album completion overlays if (cachedData.albums) { cachedData.albums.forEach(albumCompletion => { updateAlbumCompletionOverlay(albumCompletion, 'albums'); }); console.log(`✅ Restored ${cachedData.albums.length} album completion overlays`); } // Restore singles completion overlays if (cachedData.singles) { cachedData.singles.forEach(singleCompletion => { updateAlbumCompletionOverlay(singleCompletion, 'singles'); }); console.log(`✅ Restored ${cachedData.singles.length} single completion overlays`); } } /** * Check completion status for entire discography with streaming updates */ /** * Show error state on all completion overlays */ function showCompletionError() { const allOverlays = document.querySelectorAll('.completion-overlay.checking'); allOverlays.forEach(overlay => { overlay.classList.remove('checking'); overlay.classList.add('error'); overlay.innerHTML = 'Error'; overlay.title = 'Failed to check completion status'; }); } /** * Create HTML for an album/single card */ function createAlbumCardHTML(album) { const imageUrl = album.image_url || ''; const year = album.release_date ? new Date(album.release_date).getFullYear() : ''; const type = album.album_type === 'album' ? 'Album' : album.album_type === 'single' ? 'Single' : 'EP'; // Use data-bg-src for lazy background loading via IntersectionObserver const backgroundAttr = imageUrl ? `data-bg-src="${imageUrl}"` : `style="background: linear-gradient(135deg, rgba(29, 185, 84, 0.2) 0%, rgba(24, 156, 71, 0.1) 100%);"`; return `
Checking...
${escapeHtml(album.name)}
${year || 'Unknown'}
${type}
`; } /** * Initialize artist detail tabs */ function initializeArtistTabs() { const tabButtons = document.querySelectorAll('.artist-tab'); const tabContents = document.querySelectorAll('.tab-content'); tabButtons.forEach(button => { button.addEventListener('click', () => { const tabName = button.getAttribute('data-tab'); // Update button states tabButtons.forEach(btn => btn.classList.remove('active')); button.classList.add('active'); // Update content states tabContents.forEach(content => { content.classList.remove('active'); if (content.id === `${tabName}-content`) { content.classList.add('active'); } }); console.log(`🔄 Switched to ${tabName} tab`); }); }); } /** * State management functions */ function showArtistsSearchState() { console.log('🔄 Showing search state'); // Cancel any ongoing completion check when navigating back to search if (artistCompletionController) { console.log('âšī¸ Canceling completion check (navigating back to search)'); artistCompletionController.abort(); artistCompletionController = null; } // Cancel any ongoing similar artists stream when navigating back to search if (similarArtistsController) { console.log('âšī¸ Canceling similar artists stream (navigating back to search)'); similarArtistsController.abort(); similarArtistsController = null; } const searchState = document.getElementById('artists-search-state'); const resultsState = document.getElementById('artists-results-state'); const detailState = document.getElementById('artist-detail-state'); if (searchState) { searchState.classList.remove('hidden', 'fade-out'); } if (resultsState) { resultsState.classList.add('hidden'); resultsState.classList.remove('show'); } if (detailState) { detailState.classList.add('hidden'); detailState.classList.remove('show'); } artistsPageState.currentView = 'search'; updateArtistsSearchStatus('default'); // Show artist downloads section if there are active downloads showArtistDownloadsSection(); } function showArtistsResultsState() { console.log('🔄 Showing results state'); // Cancel any ongoing completion check when navigating back if (artistCompletionController) { console.log('âšī¸ Canceling completion check (navigating back to results)'); artistCompletionController.abort(); artistCompletionController = null; } // Cancel any ongoing similar artists stream when navigating back if (similarArtistsController) { console.log('âšī¸ Canceling similar artists stream (navigating back to results)'); similarArtistsController.abort(); similarArtistsController = null; } // Clear artist-specific data when navigating back to results // This ensures that selecting the same artist again will trigger a fresh scan if (artistsPageState.selectedArtist) { const artistId = artistsPageState.selectedArtist.id; console.log(`đŸ—‘ī¸ Clearing cached data for artist: ${artistsPageState.selectedArtist.name}`); // Clear artist-specific cache data delete artistsPageState.cache.completionData[artistId]; delete artistsPageState.cache.discography[artistId]; // Clear artist state artistsPageState.selectedArtist = null; artistsPageState.artistDiscography = { albums: [], singles: [] }; } const searchState = document.getElementById('artists-search-state'); const resultsState = document.getElementById('artists-results-state'); const detailState = document.getElementById('artist-detail-state'); if (searchState) { searchState.classList.add('fade-out'); setTimeout(() => searchState.classList.add('hidden'), 200); } if (resultsState) { resultsState.classList.remove('hidden'); setTimeout(() => resultsState.classList.add('show'), 50); } if (detailState) { detailState.classList.add('hidden'); detailState.classList.remove('show'); } artistsPageState.currentView = 'results'; } function showArtistDetailState() { console.log('🔄 Showing detail state'); const searchState = document.getElementById('artists-search-state'); const resultsState = document.getElementById('artists-results-state'); const detailState = document.getElementById('artist-detail-state'); if (searchState) { searchState.classList.add('hidden', 'fade-out'); } if (resultsState) { resultsState.classList.add('hidden'); resultsState.classList.remove('show'); } if (detailState) { detailState.classList.remove('hidden'); setTimeout(() => detailState.classList.add('show'), 50); } artistsPageState.currentView = 'detail'; } /** * Update search status text and styling */ function updateArtistsSearchStatus(status, message = null) { const statusElement = document.getElementById('artists-search-status'); if (!statusElement) return; // Clear all status classes statusElement.classList.remove('searching', 'error'); switch (status) { case 'default': statusElement.textContent = 'Start typing to search for artists'; break; case 'searching': statusElement.classList.add('searching'); statusElement.textContent = 'Searching for artists...'; break; case 'error': statusElement.classList.add('error'); statusElement.innerHTML = `
${message || 'Search failed. Please try again.'}
`; break; } } /** * Retry the last search query */ function retryLastSearch() { const searchInput = document.getElementById('artists-search-input'); const headerSearchInput = document.getElementById('artists-header-search-input'); // Get the last search query from either input const query = searchInput?.value?.trim() || headerSearchInput?.value?.trim() || artistsPageState.searchQuery; if (query) { console.log(`🔄 Retrying search for: "${query}"`); performArtistsSearch(query); } } /** * Update artist detail header with artist info */ function updateArtistDetailHeader(artist) { const _esc = (s) => (s || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); const info = artist.artist_info || {}; const imageUrl = artist.image_url || info.image_url || ''; // Background blur const heroBg = document.getElementById('artists-hero-bg'); if (heroBg) { heroBg.style.backgroundImage = imageUrl ? `url('${imageUrl}')` : 'none'; } // Artist image const heroImage = document.getElementById('artists-hero-image'); if (heroImage) { if (imageUrl) { heroImage.style.backgroundImage = `url('${imageUrl}')`; heroImage.innerHTML = ''; } else { heroImage.style.backgroundImage = 'none'; heroImage.innerHTML = '🎤'; // Lazy load fetch(`/api/artist/${artist.id}/image`) .then(r => r.json()) .then(d => { if (d.success && d.image_url) { heroImage.style.backgroundImage = `url('${d.image_url}')`; heroImage.innerHTML = ''; if (heroBg) heroBg.style.backgroundImage = `url('${d.image_url}')`; artist.image_url = d.image_url; } }).catch(() => { }); } } // Name const heroName = document.getElementById('artists-hero-name'); if (heroName) heroName.textContent = artist.name || 'Unknown Artist'; // Badges (service links — real logos matching library page) const badgesEl = document.getElementById('artists-hero-badges'); if (badgesEl) { const _hb = (logo, fallback, title, url) => { const inner = logo ? `${fallback}` : `${fallback}`; if (url) return `${inner}`; return `
${inner}
`; }; const badges = []; if (info.spotify_artist_id) badges.push(_hb(SPOTIFY_LOGO_URL, 'SP', 'Spotify', `https://open.spotify.com/artist/${info.spotify_artist_id}`)); if (info.musicbrainz_id || artist.musicbrainz_id) badges.push(_hb(MUSICBRAINZ_LOGO_URL, 'MB', 'MusicBrainz', `https://musicbrainz.org/artist/${info.musicbrainz_id || artist.musicbrainz_id}`)); if (info.deezer_id) badges.push(_hb(DEEZER_LOGO_URL, 'Dz', 'Deezer', `https://www.deezer.com/artist/${info.deezer_id}`)); if (info.itunes_artist_id) badges.push(_hb(ITUNES_LOGO_URL, 'IT', 'Apple Music', `https://music.apple.com/artist/${info.itunes_artist_id}`)); if (info.lastfm_url) badges.push(_hb(LASTFM_LOGO_URL, 'LFM', 'Last.fm', info.lastfm_url)); if (info.genius_url) badges.push(_hb(GENIUS_LOGO_URL, 'GEN', 'Genius', info.genius_url)); if (info.tidal_id) badges.push(_hb(TIDAL_LOGO_URL, 'TD', 'Tidal', `https://tidal.com/browse/artist/${info.tidal_id}`)); if (info.qobuz_id) badges.push(_hb(QOBUZ_LOGO_URL, 'Qz', 'Qobuz', `https://www.qobuz.com/artist/${info.qobuz_id}`)); if (info.discogs_id) badges.push(_hb(DISCOGS_LOGO_URL, 'DC', 'Discogs', `https://www.discogs.com/artist/${info.discogs_id}`)); badgesEl.innerHTML = badges.join(''); } // Genres (pill tags — merge with Last.fm tags, deduplicated) const genresEl = document.getElementById('artists-hero-genres'); if (genresEl) { let genres = info.genres || artist.genres || []; // Merge Last.fm tags const lfmTags = info.lastfm_tags || []; if (Array.isArray(lfmTags) && lfmTags.length > 0) { const existing = new Set(genres.map(g => g.toLowerCase())); const newTags = lfmTags.filter(t => !existing.has(t.toLowerCase())); genres = [...genres, ...newTags]; } if (genres.length > 0) { genresEl.innerHTML = genres.slice(0, 8).map(g => `${_esc(g)}` ).join(''); } else { genresEl.innerHTML = ''; } } // Bio (Last.fm bio or summary fallback — matching library page pattern) const bioEl = document.getElementById('artists-hero-bio'); if (bioEl) { const bio = info.lastfm_bio || info.bio || ''; if (bio) { // Strip HTML tags and "Read more on Last.fm" links let cleanBio = bio.replace(/]*>.*?<\/a>/gi, '').replace(/<[^>]+>/g, '').trim(); if (cleanBio) { bioEl.innerHTML = `${_esc(cleanBio)} Read more`; bioEl.style.display = ''; } else { bioEl.style.display = 'none'; } } else { bioEl.style.display = 'none'; } } // Stats (Last.fm listeners + playcount, with followers fallback) const statsEl = document.getElementById('artists-hero-stats'); if (statsEl) { const _fmtNum = (n) => { if (!n || n <= 0) return '0'; if (n >= 1000000) return (n / 1000000).toFixed(1).replace(/\.0$/, '') + 'M'; if (n >= 1000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'K'; return n.toLocaleString(); }; let stats = ''; if (info.lastfm_listeners) { stats += `${_fmtNum(info.lastfm_listeners)} listeners`; } if (info.lastfm_playcount) { stats += `${_fmtNum(info.lastfm_playcount)} plays`; } if (!stats && info.followers) { stats += `${_fmtNum(info.followers)} followers`; } statsEl.innerHTML = stats; } // Also update old hidden elements for any JS that references them const oldImage = document.getElementById('search-artist-detail-image'); if (oldImage && imageUrl) oldImage.style.backgroundImage = `url('${imageUrl}')`; const oldName = document.getElementById('search-artist-detail-name'); if (oldName) oldName.textContent = artist.name; // Initialize watchlist button initializeArtistDetailWatchlistButton(artist); } /** * Initialize watchlist button for artist detail page */ async function initializeArtistDetailWatchlistButton(artist) { const button = document.getElementById('artist-detail-watchlist-btn'); if (!button) return; console.log(`🔧 Initializing watchlist button for artist: ${artist.name} (${artist.id})`); // Store artist info on the button for settings gear access button.dataset.artistId = artist.id; button.dataset.artistName = artist.name; // Reset button state completely button.disabled = false; button.classList.remove('watching'); button.style.background = ''; button.style.cursor = ''; // Remove any existing click handlers to prevent duplicates button.onclick = null; // Set up new click handler button.onclick = (event) => toggleArtistDetailWatchlist(event, artist.id, artist.name); // Check and update current status await updateArtistDetailWatchlistButton(artist.id, artist.name); } /** * Toggle watchlist status for artist detail page */ async function toggleArtistDetailWatchlist(event, artistId, artistName) { event.preventDefault(); const button = document.getElementById('artist-detail-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 appearance 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 = 'Remove from Watchlist'; button.classList.add('watching'); console.log(`✅ Added ${artistName} to watchlist`); } // Show/hide watchlist settings gear const settingsBtn = document.getElementById('artist-detail-watchlist-settings-btn'); if (settingsBtn) { if (!isWatching) { // Just added to watchlist — show gear settingsBtn.classList.remove('hidden'); settingsBtn.onclick = () => openWatchlistArtistConfigModal(artistId, artistName); } else { // Just removed from watchlist — hide gear settingsBtn.classList.add('hidden'); settingsBtn.onclick = null; } } // Update dashboard watchlist count updateWatchlistButtonCount(); // Update any visible artist cards updateArtistCardWatchlistStatus(); } catch (error) { console.error('Error toggling watchlist:', error); text.textContent = originalText; // Show error feedback const originalBackground = button.style.background; button.style.background = 'rgba(255, 59, 48, 0.3)'; setTimeout(() => { button.style.background = originalBackground; }, 2000); } finally { button.disabled = false; } } /** * Update artist detail watchlist button status */ async function updateArtistDetailWatchlistButton(artistId, artistName) { const button = document.getElementById('artist-detail-watchlist-btn'); if (!button) { console.warn('âš ī¸ Artist detail watchlist button not found'); return; } // Use passed name or fall back to stored data attribute const name = artistName || button.dataset.artistName || ''; try { console.log(`🔍 Checking watchlist status for artist: ${artistId}`); 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'); console.log(`📊 Watchlist status for ${artistId}: ${data.is_watching ? 'WATCHING' : 'NOT WATCHING'}`); // Ensure button is enabled button.disabled = false; // Show/hide watchlist settings gear const settingsBtn = document.getElementById('artist-detail-watchlist-settings-btn'); if (settingsBtn) { if (data.is_watching) { settingsBtn.classList.remove('hidden'); settingsBtn.onclick = () => openWatchlistArtistConfigModal(artistId, name); } else { settingsBtn.classList.add('hidden'); settingsBtn.onclick = null; } } if (data.is_watching) { icon.textContent = 'đŸ‘ī¸'; text.textContent = 'Remove from Watchlist'; button.classList.add('watching'); } else { icon.textContent = 'đŸ‘ī¸'; text.textContent = 'Add to Watchlist'; button.classList.remove('watching'); } } else { console.error('❌ Failed to check watchlist status:', data.error); } } catch (error) { console.error('❌ Error checking watchlist status:', error); // Ensure button doesn't get stuck in bad state button.disabled = false; } } /** * Show loading state for discography */ function showDiscographyLoading() { const albumsContainer = document.getElementById('album-cards-container'); const singlesContainer = document.getElementById('singles-cards-container'); const loadingHtml = `
Loading...
-
-
`.repeat(4); if (albumsContainer) albumsContainer.innerHTML = loadingHtml; if (singlesContainer) singlesContainer.innerHTML = loadingHtml; } /** * Show error state for discography */ function showDiscographyError(message = 'Failed to load discography') { const albumsContainer = document.getElementById('album-cards-container'); const singlesContainer = document.getElementById('singles-cards-container'); const errorHtml = `
âš ī¸
Failed to load discography
${escapeHtml(message)}
`; if (albumsContainer) albumsContainer.innerHTML = errorHtml; if (singlesContainer) singlesContainer.innerHTML = errorHtml; } /** * Show loading cards while searching */ function showSearchLoadingCards() { const container = document.getElementById('artists-cards-container'); if (!container) return; const loadingCardHtml = `
Loading...
Fetching data...
âŗ Loading...
`; // Show 6 loading cards container.innerHTML = loadingCardHtml.repeat(6); } // =============================== // ARTIST ALBUM DOWNLOAD MISSING TRACKS INTEGRATION // =============================== /** * Get the completion status of an album from cached data or DOM * @param {string} albumId - The album ID * @param {string} albumType - The album type ('albums' or 'singles') * @returns {Object|null} - Completion status object or null */ function getAlbumCompletionStatus(albumId, albumType) { try { // First, check cached completion data const artistId = artistsPageState.selectedArtist?.id; if (artistId && artistsPageState.cache.completionData[artistId]) { const cachedData = artistsPageState.cache.completionData[artistId]; const dataArray = albumType === 'albums' ? cachedData.albums : cachedData.singles; if (dataArray) { const completionData = dataArray.find(item => item.album_id === albumId || item.id === albumId); if (completionData) { console.log(`📊 Found cached completion data for album ${albumId}:`, completionData); return completionData; } } } // Fallback: Check DOM completion overlay const containerId = albumType === 'albums' ? 'album-cards-container' : 'singles-cards-container'; const container = document.getElementById(containerId); if (container) { const albumCard = container.querySelector(`[data-album-id="${albumId}"]`); if (albumCard) { const overlay = albumCard.querySelector('.completion-overlay'); if (overlay) { // Extract status from overlay classes const classList = Array.from(overlay.classList); const statusClasses = ['completed', 'nearly_complete', 'partial', 'missing', 'downloading', 'downloaded', 'error']; const status = statusClasses.find(cls => classList.includes(cls)); if (status) { console.log(`📊 Found DOM completion status for album ${albumId}: ${status}`); return { status, completion_percentage: status === 'completed' ? 100 : 0 }; } } } } console.warn(`âš ī¸ No completion status found for album ${albumId}`); return null; } catch (error) { console.error(`❌ Error getting album completion status for ${albumId}:`, error); return null; } } /** * Handle album/single/EP click to open download missing tracks modal */ async function handleArtistAlbumClick(album, albumType) { console.log(`đŸŽĩ Album clicked: ${album.name} (${album.album_type}) from artist: ${artistsPageState.selectedArtist?.name}`); if (!artistsPageState.selectedArtist) { console.error('❌ No selected artist found'); showToast('Error: No artist selected', 'error'); return; } showLoadingOverlay('Loading album...'); try { // Check completion status of the album const completionStatus = getAlbumCompletionStatus(album.id, albumType); console.log(`📊 Album completion status: ${completionStatus?.status || 'unknown'} (${completionStatus?.completion_percentage || 0}%)`); // For Artists page, always use Download Missing Tracks modal to analyze and download console.log(`🔄 Opening download missing tracks modal for album analysis`); // Create virtual playlist ID const virtualPlaylistId = `artist_album_${artistsPageState.selectedArtist.id}_${album.id}`; // Check if modal already exists and show it if (activeDownloadProcesses[virtualPlaylistId]) { console.log(`📱 Reopening existing modal for ${album.name}`); const process = activeDownloadProcesses[virtualPlaylistId]; if (process.modalElement) { if (process.status === 'complete') { showToast('Showing previous results. Close this modal to start a new analysis.', 'info'); } process.modalElement.style.display = 'flex'; hideLoadingOverlay(); return; } } // Create virtual playlist and open modal // Note: Don't hide loading overlay here - let the flow continue through to the modal await createArtistAlbumVirtualPlaylist(album, albumType); } catch (error) { hideLoadingOverlay(); console.error('❌ Error handling album click:', error); showToast(`Error opening download modal: ${error.message}`, 'error'); } } /** * Create virtual playlist for artist album and open download missing tracks modal */ async function createArtistAlbumVirtualPlaylist(album, albumType) { const artist = artistsPageState.selectedArtist; const virtualPlaylistId = `artist_album_${artist.id}_${album.id}`; console.log(`đŸŽĩ Creating virtual playlist for: ${artist.name} - ${album.name}`); try { // Loading overlay already shown by handleArtistAlbumClick // Fetch album tracks from backend (pass name/artist for Hydrabase support) const _aat1 = new URLSearchParams({ name: album.name || '', artist: artist.name || '' }); const albumSource = artistsPageState.sourceOverride || album.source || artist.source || artistsPageState.artistDiscography?.source || null; if (albumSource) { _aat1.set('source', albumSource); } if (artistsPageState.pluginOverride) { _aat1.set('plugin', artistsPageState.pluginOverride); } const response = await fetch(`/api/album/${album.id}/tracks?${_aat1}`); if (!response.ok) { if (response.status === 401) { throw new Error('Spotify not authenticated. Please check your API settings.'); } const errData = await response.json().catch(() => ({})); throw new Error(errData.error || `Failed to load album tracks: ${response.status}`); } const data = await response.json(); if (!data.success || !data.tracks || data.tracks.length === 0) { throw new Error('No tracks found for this album'); } console.log(`✅ Loaded ${data.tracks.length} tracks for ${data.album.name}`); // Use album data from API response (has complete data including images array) const fullAlbumData = data.album; // Format playlist name with artist and album info const playlistName = `[${artist.name}] ${fullAlbumData.name}`; // Open download missing tracks modal with formatted tracks // Pass false for showLoadingOverlay since we already have one from handleArtistAlbumClick // Use fullAlbumData from API response instead of album parameter await openDownloadMissingModalForArtistAlbum(virtualPlaylistId, playlistName, data.tracks, fullAlbumData, artist, false); // Track this download for artist bubble management registerArtistDownload(artist, album, virtualPlaylistId, albumType); } catch (error) { console.error('❌ Error creating virtual playlist:', error); showToast(`Failed to load album: ${error.message}`, 'error'); throw error; } } /** * Open download missing tracks modal specifically for artist albums * Similar to openDownloadMissingModalForYouTube but for artist albums */