diff --git a/web_server.py b/web_server.py index 7b01e306..8865c21f 100644 --- a/web_server.py +++ b/web_server.py @@ -37,7 +37,7 @@ _log_dir = Path(_log_path).parent logger = setup_logging(_log_level, _log_path) # App version — single source of truth for backup metadata, version-info endpoint, etc. -_SOULSYNC_BASE_VERSION = "2.44" +_SOULSYNC_BASE_VERSION = "2.45" def _build_version_string(): """Append short commit hash to version when available (e.g. 2.35+abc1234).""" @@ -22809,6 +22809,17 @@ def get_version_info(): "title": "What's New in SoulSync", "subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes", "sections": [ + { + "title": "Artist Links Everywhere Go to the Same Page", + "description": "Clicking an artist result in Search, Discover, the API Monitor, or anywhere else now lands on the standalone artist detail page Library already uses — instead of swapping into the Artists page's inline detail view", + "features": [ + "• One artist detail page, not two — less navigation surprise and a stable URL (/artist-detail) for deep links", + "• Source context (Spotify/iTunes/Deezer/etc.) now carries cleanly into the destination so non-Spotify albums load from the right provider", + "• Removed the navigate-then-setTimeout dance at 9 callsites (api-monitor, discover, downloads, search, library recommendations)", + "• Artists sidebar entry and its inline search still work for now — next phase retires that page entirely", + "• Phase 4a of the Search/Artists unification project", + ], + }, { "title": "Remove Embedded Download Manager from Search Page", "description": "The Search page used to carry a second copy of the Download Manager (active + finished queues, clear/cancel-all buttons) that was hidden by default and duplicated the dedicated Downloads page. That duplicate is gone", diff --git a/webui/static/api-monitor.js b/webui/static/api-monitor.js index 3784e28c..3dcbd1b7 100644 --- a/webui/static/api-monitor.js +++ b/webui/static/api-monitor.js @@ -2382,16 +2382,9 @@ async function openWatchlistArtistDetailView(artistId, artistName) { source = spotify_artist_id ? 'spotify' : discogs_artist_id ? 'discogs' : deezer_artist_id ? 'deezer' : 'itunes'; } if (discogId) { - // Close detail overlay and navigate to Artists page + // Close detail overlay and navigate to the standalone artist detail page closeWatchlistArtistDetailView(); - // Navigate to Artists page and load discography - 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/discover.js b/webui/static/discover.js index 6763054f..e70e3b65 100644 --- a/webui/static/discover.js +++ b/webui/static/discover.js @@ -739,16 +739,7 @@ async function checkRecommendedWatchlistStatuses(artists) { async function viewRecommendedArtistDiscography(artistId, artistName) { closeRecommendedArtistsModal(); - - const artist = { - id: artistId, - name: artistName - }; - - // Use same navigation pattern as hero slider - navigateToPage('artists'); - await new Promise(resolve => setTimeout(resolve, 100)); - await selectArtistForDetail(artist); + navigateToArtistDetail(artistId, artistName); } async function checkAllHeroWatchlistStatus() { @@ -830,25 +821,8 @@ async function viewDiscoverHeroDiscography() { return; } - // Create artist object matching the expected format - 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}`); - - // Navigate to Artists page - navigateToPage('artists'); - - // Small delay to let the page load - await new Promise(resolve => setTimeout(resolve, 100)); - - // Load the artist details - await selectArtistForDetail(artist); + navigateToArtistDetail(artistId, artistName); } function showDiscoverHeroEmpty() { @@ -4639,9 +4613,9 @@ function _renderYourArtistCard(artist) { const watchlistClass = artist.on_watchlist ? 'active' : ''; const hasId = artist.active_source_id && artist.active_source_id !== ''; - // Navigate to artist page (name click) + // Navigate to standalone artist detail page (name click) 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 @@ -4820,7 +4794,7 @@ async function openYourArtistInfoModal(poolId) { Explore - @@ -6720,7 +6694,7 @@ function _artMapSetupInteraction(canvas) {
Artist Info
-
+
💿 View Discography
@@ -7408,7 +7382,7 @@ async function openGenreDeepDive(genre) { // Always open on Artists page with discography — pass source for correct routing const imgUrl = _esc(a.image_url || ''); const artSource = _esc(a.source || ''); - const clickAction = `onclick="document.getElementById('genre-deep-dive-modal').remove();navigateToPage('artists');setTimeout(()=>selectArtistForDetail({id:'${_esc(a.entity_id)}',name:'${_esc(a.name)}',image_url:'${imgUrl}'},{source:'${artSource}'}),300)"`; + const clickAction = `onclick="document.getElementById('genre-deep-dive-modal').remove();navigateToArtistDetail('${_esc(a.entity_id)}','${_esc(a.name)}','${artSource}' || null)"`; const srcClass = (a.source || '').toLowerCase(); return `
diff --git a/webui/static/downloads.js b/webui/static/downloads.js index 7348f4ca..f13bfb2e 100644 --- a/webui/static/downloads.js +++ b/webui/static/downloads.js @@ -634,16 +634,7 @@ function _navigateToArtistFromModal(artistId, artistName, imageUrl, source, play if (!artistName) return; // Close the download modal if (playlistId) closeDownloadMissingModal(playlistId); - // Navigate to Artists page and load discography - navigateToPage('artists'); - setTimeout(() => { - // If we have an artist ID, use it directly - // If not, search by name — selectArtistForDetail handles both - selectArtistForDetail( - { id: artistId || artistName, name: artistName, image_url: imageUrl || '' }, - source ? { source: source } : undefined - ); - }, 200); + navigateToArtistDetail(artistId || artistName, artistName, source || null); } async function closeDownloadMissingModal(playlistId) { @@ -5434,18 +5425,8 @@ function _gsSwitchSource(src) { function _gsClickArtist(id, name, isLibrary) { _gsDeactivate(); - if (isLibrary) { - // Same as enhanced search: navigateToArtistDetail - navigateToArtistDetail(id, name); - } else { - // Same as enhanced search: navigate to Artists page + selectArtistForDetail - navigateToPage('artists'); - setTimeout(() => { - selectArtistForDetail({ id, name, image_url: '' }, { - source: _gsState.activeSource || '', - }); - }, 150); - } + const source = isLibrary ? null : (_gsState.activeSource || null); + navigateToArtistDetail(id, name, source); } async function _gsClickAlbum(albumId, albumName, artistName, imageUrl, source) { diff --git a/webui/static/helper.js b/webui/static/helper.js index fe201416..6be8d1bb 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3599,6 +3599,11 @@ function closeHelperSearch() { // ═══════════════════════════════════════════════════════════════════════════ const WHATS_NEW = { + '2.45': [ + // --- April 23, 2026 (night) --- + { date: 'April 23, 2026 (night)' }, + { title: 'Artist Links Everywhere Go to the Same Page', desc: 'Clicking an artist result in Search, Discover, the API Monitor, or anywhere else now lands on the standalone artist detail page that Library already uses — instead of swapping into the Artists page\'s inline detail view. One artist detail page, not two: less navigation surprise, a stable /artist-detail URL for deep links, and source context (Spotify/iTunes/Deezer/etc.) now carries cleanly into the destination so non-Spotify albums load from the right provider. Removed the navigate-then-setTimeout dance at 9 callsites. Artists sidebar entry and its inline search still work for now — next phase retires that page entirely. Phase 4a of the Search/Artists unification project', page: 'library' }, + ], '2.44': [ // --- April 23, 2026 (evening) --- { date: 'April 23, 2026 (evening)' }, diff --git a/webui/static/library.js b/webui/static/library.js index 675187b8..3f7fb359 100644 --- a/webui/static/library.js +++ b/webui/static/library.js @@ -655,8 +655,8 @@ let discographyFilterState = { ownership: 'all' // 'all', 'owned', 'missing' }; -function navigateToArtistDetail(artistId, artistName) { - console.log(`🎵 Navigating to artist detail: ${artistName} (ID: ${artistId})`); +function navigateToArtistDetail(artistId, artistName, sourceOverride = null) { + console.log(`🎵 Navigating to artist detail: ${artistName} (ID: ${artistId}${sourceOverride ? `, source: ${sourceOverride}` : ''})`); // Abort any in-progress completion stream if (artistDetailPageState.completionController) { @@ -672,7 +672,7 @@ function navigateToArtistDetail(artistId, artistName) { // Store current artist info and reset enhanced view state artistDetailPageState.currentArtistId = artistId; artistDetailPageState.currentArtistName = artistName; - artistDetailPageState.currentArtistSource = null; + artistDetailPageState.currentArtistSource = sourceOverride || null; artistDetailPageState.enhancedData = null; artistDetailPageState.expandedAlbums = new Set(); artistDetailPageState.selectedTracks = new Set(); diff --git a/webui/static/search.js b/webui/static/search.js index 4dad56aa..be9147c5 100644 --- a/webui/static/search.js +++ b/webui/static/search.js @@ -340,18 +340,7 @@ function initializeSearchModeToggle() { const sourceOverride = _activeSearchSource; console.log(`🎵 Opening artist detail: ${artist.name} (ID: ${artist.id}, source: ${sourceOverride})`); hideDropdown(); - - // Navigate to Artists page - navigateToPage('artists'); - - // Small delay to let the page load - await new Promise(resolve => setTimeout(resolve, 100)); - - // Load the artist details with source context - await selectArtistForDetail(artist, { - source: sourceOverride, - plugin: artist.external_urls?.hydrabase_plugin, - }); + navigateToArtistDetail(artist.id, artist.name, sourceOverride || null); } }) );