From 93f194182976cb760fccc90de00af069ed7b2a0b Mon Sep 17 00:00:00 2001
From: Broque Thomas <26755000+Nezreka@users.noreply.github.com>
Date: Wed, 22 Apr 2026 17:00:52 -0700
Subject: [PATCH] Unify artist detail: route source artists to standalone page,
retire inline Artists page
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Completes the artist-detail unification. Source artists now land on
the same /artist-detail page as library artists (with the source-aware
backend endpoint from earlier this session handling the data fetch).
The inline Artists page is gone â artists.js deleted, #artists-page
HTML block removed, /artists URL aliases to /search.
Source-artist callsites re-migrated from selectArtistForDetail to
navigateToArtistDetail (search results, global widget, download
modal, Discover hero / Your Artists cards / artmap context / genre
deep-dive, watchlist artist detail).
Visual upgrade to standalone hero: added .artist-detail-hero-bg +
.artist-detail-hero-overlay (blurred image bg, dark gradient â same
treatment as the inline page). library.js sets the bg image when
loading an artist.
Library-only UI hidden via CSS for source artists (existing rules
from the previous commit cover Enhanced toggle, Status filter,
completion bars, enrichment coverage, Top Tracks sidebar, Radio /
Enhance buttons).
Final 2 helpers (lazyLoadArtistImages used by wishlist-tools,
showCompletionError used by completion checker) moved from
artists.js into shared-helpers.js. The inline-page candidate set
was dropped from _resolveSimilarArtistsTargets.
init.js: 'artists' alias added at top of navigateToPage (same
pattern as the existing 'downloads' alias). 'case artists:' handler
removed from loadPageData. _getPageFromPath now maps artist-detail
to library as its parent (matches the existing nav highlight at
init.js:2161).
tests/test_script_split_integrity.py: artists.js removed from
SPLIT_MODULES; KNOWN_CROSS_FILE_DUPES updated to point escapeHtml
at shared-helpers.js instead of artists.js. 354/354 tests pass.
Net delta: -1700 lines.
Stays at 2.39. Once you've verified end-to-end (library artist ->
hero looks like inline visual; source artist from Search -> same
page, similar artists works, no 404s; /artists URL -> /search), a
follow-up commit bumps to 2.40 with the full WHATS_NEW entry that's
already prepped.
---
tests/test_script_split_integrity.py | 3 +-
webui/index.html | 145 +--
webui/static/api-monitor.js | 10 +-
webui/static/artists.js | 1584 --------------------------
webui/static/discover.js | 32 +-
webui/static/downloads.js | 25 +-
webui/static/init.js | 18 +-
webui/static/library.js | 11 +
webui/static/search.js | 14 +-
webui/static/shared-helpers.js | 127 ++-
webui/static/style.css | 38 +
11 files changed, 153 insertions(+), 1854 deletions(-)
delete mode 100644 webui/static/artists.js
diff --git a/tests/test_script_split_integrity.py b/tests/test_script_split_integrity.py
index d5982c9e..a801f7d1 100644
--- a/tests/test_script_split_integrity.py
+++ b/tests/test_script_split_integrity.py
@@ -39,7 +39,6 @@ SPLIT_MODULES = [
"downloads.js",
"wishlist-tools.js",
"sync-services.js",
- "artists.js",
"api-monitor.js",
"library.js",
"beatport-ui.js",
@@ -57,7 +56,7 @@ NON_SPLIT_JS = {"setup-wizard.js", "docs.js", "helper.js", "particles.js", "work
# In a plain
-
diff --git a/webui/static/api-monitor.js b/webui/static/api-monitor.js
index bc86b223..c900d824 100644
--- a/webui/static/api-monitor.js
+++ b/webui/static/api-monitor.js
@@ -2385,16 +2385,8 @@ async function openWatchlistArtistDetailView(artistId, artistName) {
source = spotify_artist_id ? 'spotify' : discogs_artist_id ? 'discogs' : deezer_artist_id ? 'deezer' : 'itunes';
}
if (discogId) {
- // Watchlist discogId is a metadata-source id (Spotify/Deezer/iTunes),
- // not a library PK â route through the Artists page inline view.
closeWatchlistArtistDetailView();
- navigateToPage('artists');
- setTimeout(() => {
- selectArtistForDetail(
- { id: discogId, name: artistName, image_url: artist.image_url || '' },
- { source: source }
- );
- }, 200);
+ navigateToArtistDetail(discogId, artistName, source);
}
});
diff --git a/webui/static/artists.js b/webui/static/artists.js
deleted file mode 100644
index 0bd23d63..00000000
--- a/webui/static/artists.js
+++ /dev/null
@@ -1,1584 +0,0 @@
-// 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}`;
- 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 = `
-
- `.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
- */
diff --git a/webui/static/discover.js b/webui/static/discover.js
index e0293a60..d322cc81 100644
--- a/webui/static/discover.js
+++ b/webui/static/discover.js
@@ -739,14 +739,7 @@ async function checkRecommendedWatchlistStatuses(artists) {
async function viewRecommendedArtistDiscography(artistId, artistName) {
closeRecommendedArtistsModal();
-
- const artist = { id: artistId, name: artistName };
-
- // Recommended artists come from the metadata source â route through the
- // Artists page's inline view so the source-provided id resolves correctly.
- navigateToPage('artists');
- await new Promise(resolve => setTimeout(resolve, 100));
- await selectArtistForDetail(artist);
+ navigateToArtistDetail(artistId, artistName);
}
async function checkAllHeroWatchlistStatus() {
@@ -828,21 +821,8 @@ async function viewDiscoverHeroDiscography() {
return;
}
- const artist = {
- id: artistId,
- name: artistName,
- image_url: discoverHeroArtists[discoverHeroIndex]?.image_url || '',
- genres: discoverHeroArtists[discoverHeroIndex]?.genres || [],
- popularity: discoverHeroArtists[discoverHeroIndex]?.popularity || 0
- };
-
console.log(`đĩ Navigating to artist detail for: ${artistName}`);
-
- // Hero artists are source-provided recommendations â route through the
- // Artists page's inline view so the source id resolves correctly.
- navigateToPage('artists');
- await new Promise(resolve => setTimeout(resolve, 100));
- await selectArtistForDetail(artist);
+ navigateToArtistDetail(artistId, artistName);
}
function showDiscoverHeroEmpty() {
@@ -4635,7 +4615,7 @@ function _renderYourArtistCard(artist) {
// Navigate to Artists page (name click) â source artist id, needs inline view
const navAction = hasId
- ? `event.stopPropagation(); navigateToPage('artists'); setTimeout(() => selectArtistForDetail({id:'${escapeForInlineJs(artist.active_source_id)}', name:'${escapeForInlineJs(artist.artist_name)}', image_url:'${escapeForInlineJs(img)}'}), 200)`
+ ? `event.stopPropagation(); navigateToArtistDetail('${escapeForInlineJs(artist.active_source_id)}', '${escapeForInlineJs(artist.artist_name)}')`
: '';
// Open info modal (card body click) â pass pool ID so we can look up all data
@@ -4814,7 +4794,7 @@ async function openYourArtistInfoModal(poolId) {
Explore
-