From 9361c2996578fde60283e933c4373c3db86e8f8d Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Wed, 22 Apr 2026 13:36:36 -0700 Subject: [PATCH] Route all artist-detail callers to the standalone page, bump to 2.45 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4a of the Search/Artists unification. The app had two artist- detail implementations: the standalone page Library navigates to via navigateToArtistDetail (its own route, deep-link support, highlights Library in the sidebar), and an inline state inside the Artists page reached via selectArtistForDetail. They rendered similar content but were separate code paths and kept drifting apart (PR #356 just had to fix source propagation in both). Every external caller of selectArtistForDetail (9 sites across api-monitor.js, discover.js, downloads.js, search.js) now calls navigateToArtistDetail(id, name, source) directly. Removed ~63 lines of the navigate-then-setTimeout-then-select dance. Source context (Spotify/iTunes/Deezer/etc.) carries cleanly through via the new third argument. Artists sidebar entry, its inline search, and selectArtistForDetail all still work — they just have no external callers. Phase 4b will retire the sidebar entry and artists.js. --- web_server.py | 13 +++++++++++- webui/static/api-monitor.js | 11 ++-------- webui/static/discover.js | 40 +++++++------------------------------ webui/static/downloads.js | 25 +++-------------------- webui/static/helper.js | 5 +++++ webui/static/library.js | 6 +++--- webui/static/search.js | 13 +----------- 7 files changed, 33 insertions(+), 80 deletions(-) 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); } }) );