diff --git a/core/seasonal_discovery.py b/core/seasonal_discovery.py index 159776f7..b813717a 100644 --- a/core/seasonal_discovery.py +++ b/core/seasonal_discovery.py @@ -707,7 +707,8 @@ class SeasonalDiscoveryService: artist_name, album_cover_url, release_date, - popularity + popularity, + source FROM seasonal_albums WHERE season_key = ? AND source = ? ORDER BY popularity DESC, album_name ASC diff --git a/webui/static/discover.js b/webui/static/discover.js index aa22b3aa..af8fdd91 100644 --- a/webui/static/discover.js +++ b/webui/static/discover.js @@ -3945,6 +3945,35 @@ function hideSeasonalSections() { } } +function _buildDiscoverArtistContext(source, artistName, sourceData = {}, albumData = {}) { + const normalizedSource = (source || '').toString().toLowerCase(); + const albumArtist = Array.isArray(albumData.artists) ? albumData.artists[0] : null; + const activeSource = (source || sourceData.active_source || sourceData.source || '').toString().toLowerCase(); + const context = { + ...sourceData, + id: sourceData.active_source_id || sourceData.artist_id || albumArtist?.id || '', + name: artistName || sourceData.artist_name || sourceData.name || albumArtist?.name || '', + source: normalizedSource || activeSource || '', + spotify_artist_id: sourceData.spotify_artist_id || sourceData.artist_spotify_id || ((normalizedSource || activeSource) === 'spotify' ? albumArtist?.id : '') || '', + itunes_artist_id: sourceData.itunes_artist_id || sourceData.artist_itunes_id || ((normalizedSource || activeSource) === 'itunes' ? albumArtist?.id : '') || '', + deezer_artist_id: sourceData.deezer_artist_id || sourceData.artist_deezer_id || sourceData.deezer_id || ((normalizedSource || activeSource) === 'deezer' ? albumArtist?.id : '') || '', + discogs_artist_id: sourceData.discogs_artist_id || sourceData.artist_discogs_id || sourceData.discogs_id || '', + amazon_artist_id: sourceData.amazon_artist_id || sourceData.artist_amazon_id || sourceData.amazon_id || '', + soul_id: sourceData.soul_id || sourceData.hydrabase_artist_id || '', + }; + + const sourceIdBySource = { + spotify: context.spotify_artist_id, + itunes: context.itunes_artist_id, + deezer: context.deezer_artist_id, + discogs: context.discogs_artist_id, + amazon: context.amazon_artist_id, + hydrabase: context.soul_id, + }; + context.id = sourceIdBySource[normalizedSource || activeSource] || context.id; + return context; +} + async function openDownloadModalForSeasonalAlbum(albumIndex) { const album = discoverSeasonalAlbums[albumIndex]; if (!album) { @@ -4004,14 +4033,12 @@ async function openDownloadModalForSeasonalAlbum(albumIndex) { const virtualPlaylistId = `seasonal_album_${albumId}`; // Pass proper artist/album context for album download (1 worker + source reuse) - const artistContext = { - name: album.artist_name, - source: source - }; + const artistContext = _buildDiscoverArtistContext(source, album.artist_name, album, albumData); const albumContext = { id: albumData.id, name: albumData.name, + source: source, album_type: albumData.album_type || 'album', total_tracks: albumData.total_tracks || 0, release_date: albumData.release_date || '', @@ -6986,7 +7013,7 @@ async function openCacheDiscoverAlbum(sectionKey, index) { duration_ms: track.duration_ms || 0, track_number: track.track_number || 0, }; }); - const artistContext = { id: albumData.artists?.[0]?.id || '', name: artistName, source: resolvedSource }; + const artistContext = _buildDiscoverArtistContext(resolvedSource, artistName, item, albumData); const albumContext = { id: albumData.id, name: albumData.name, album_type: albumData.album_type || 'album', total_tracks: albumData.total_tracks || 0, release_date: albumData.release_date || '', images: albumData.images || [] }; await openDownloadMissingModalForYouTube(`discover_cache_${resolvedId}`, albumData.name, spotifyTracks, artistContext, albumContext); hideLoadingOverlay(); @@ -7046,11 +7073,7 @@ async function openCacheDiscoverAlbum(sectionKey, index) { }; }); - const artistContext = { - id: albumData.artists?.[0]?.id || '', - name: item.artist_name || albumData.artists?.[0]?.name || '', - source: source, - }; + const artistContext = _buildDiscoverArtistContext(source, item.artist_name || albumData.artists?.[0]?.name || '', item, albumData); const albumContext = { id: albumData.id, name: albumData.name, @@ -7970,11 +7993,7 @@ async function openDownloadModalForRecentAlbum(albumIndex) { const virtualPlaylistId = `discover_album_${albumId}`; // CRITICAL FIX: Pass proper artist/album context for modal display - const artistContext = { - id: source === 'spotify' ? album.artist_spotify_id : source === 'deezer' ? album.artist_deezer_id : album.artist_itunes_id, - name: album.artist_name, - source: source - }; + const artistContext = _buildDiscoverArtistContext(source, album.artist_name, album, albumData); const albumContext = { id: albumData.id, diff --git a/webui/static/downloads.js b/webui/static/downloads.js index 8edca77e..7374753d 100644 --- a/webui/static/downloads.js +++ b/webui/static/downloads.js @@ -501,7 +501,10 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam const heroContext = isDiscoverAlbum && album && artist ? { type: 'album', artist: { + ...artist, name: artist.name, + id: artist.id || artist.artist_id || null, + source: artist.source || album.source || null, image_url: artist.image_url || null }, album: { @@ -634,9 +637,42 @@ function _navigateToArtistFromModal(artistId, artistName, imageUrl, source, play if (!artistName) return; // Close the download modal const process = playlistId ? activeDownloadProcesses[playlistId] : null; - const resolvedSource = source || process?.artist?.source || process?.album?.source || process?.source || null; + const artistContext = process?.artist || {}; + const inferredSource = artistContext.spotify_artist_id ? 'spotify' + : artistContext.itunes_artist_id ? 'itunes' + : (artistContext.deezer_artist_id || artistContext.deezer_id) ? 'deezer' + : (artistContext.discogs_artist_id || artistContext.discogs_id) ? 'discogs' + : (artistContext.amazon_artist_id || artistContext.amazon_id) ? 'amazon' + : (artistContext.soul_id || artistContext.hydrabase_artist_id) ? 'hydrabase' + : null; + const resolvedSource = source || process?.artist?.source || process?.album?.source || process?.source || inferredSource; + const sourceKey = (resolvedSource || '').toString().toLowerCase(); + const sourceIdFields = { + spotify: ['spotify_artist_id', 'id', 'artist_id'], + itunes: ['itunes_artist_id', 'artist_id', 'id'], + deezer: ['deezer_artist_id', 'deezer_id', 'artist_id', 'id'], + discogs: ['discogs_artist_id', 'discogs_id', 'artist_id', 'id'], + amazon: ['amazon_artist_id', 'amazon_id', 'artist_id', 'id'], + hydrabase: ['soul_id', 'hydrabase_artist_id', 'artist_id', 'id'], + musicbrainz: ['musicbrainz_id', 'artist_id', 'id'], + }; + let resolvedArtistId = artistId; + for (const field of (sourceIdFields[sourceKey] || ['artist_id', 'id'])) { + const candidate = artistContext?.[field]; + if (candidate) { + resolvedArtistId = candidate; + break; + } + } + if (resolvedArtistId && String(resolvedArtistId).toLowerCase() === String(artistName).toLowerCase()) { + resolvedArtistId = null; + } if (playlistId) closeDownloadMissingModal(playlistId); - navigateToArtistDetail(artistId || artistName, artistName, resolvedSource || null); + if (!resolvedArtistId || !resolvedSource) { + showToast(`Artist details are not available for ${artistName}`, 'warning'); + return; + } + navigateToArtistDetail(resolvedArtistId, artistName, resolvedSource); } async function closeDownloadMissingModal(playlistId) { diff --git a/webui/static/library.js b/webui/static/library.js index 8e5b6a62..e1ea24d3 100644 --- a/webui/static/library.js +++ b/webui/static/library.js @@ -1042,6 +1042,17 @@ async function loadArtistDetailData(artistId, artistName) { throw new Error(data.error || 'Failed to load artist data'); } + const isSourceOnlyArtist = !data.artist?.server_source; + if (isSourceOnlyArtist && data.discography) { + for (const bucket of ['albums', 'eps', 'singles']) { + for (const release of (data.discography[bucket] || [])) { + if (release.owned === null || typeof release.owned === 'undefined') { + release.owned = false; + } + } + } + } + console.log(`✅ Loaded artist detail data:`, data); // Hide loading and show all content @@ -1077,7 +1088,7 @@ async function loadArtistDetailData(artistId, artistName) { renderArtistEnrichmentCoverage(data.enrichment_coverage); // Start streaming ownership checks if we have Spotify discography with checking state - if (data.discography && data.discography.albums) { + if (!isSourceOnlyArtist && data.discography && data.discography.albums) { const hasChecking = [...(data.discography.albums || []), ...(data.discography.eps || []), ...(data.discography.singles || [])] .some(r => r.owned === null); if (hasChecking) { @@ -1091,7 +1102,9 @@ async function loadArtistDetailData(artistId, artistName) { // Use currentArtistId (not the closure arg) because the library-upgrade // branch above may have rewritten it from the source ID to the library PK, // and /api/library/artist//quality-analysis only works on library PKs. - checkArtistEnhanceEligibility(artistDetailPageState.currentArtistId); + if (!isSourceOnlyArtist) { + checkArtistEnhanceEligibility(artistDetailPageState.currentArtistId); + } } catch (error) { console.error(`❌ Error loading artist detail data:`, error);