diff --git a/webui/index.html b/webui/index.html
index 5ddbcb2a..4bcfba98 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -2769,11 +2769,11 @@
diff --git a/webui/src/platform/shell/globals.d.ts b/webui/src/platform/shell/globals.d.ts
index 2db2c644..65d5b471 100644
--- a/webui/src/platform/shell/globals.d.ts
+++ b/webui/src/platform/shell/globals.d.ts
@@ -43,14 +43,6 @@ declare global {
skipRouteChange?: boolean;
},
) => void;
- navigateToArtistDetailPage: (
- artistId: string | number,
- artistName: string,
- sourceOverride?: string | null,
- options?: {
- replace?: boolean;
- },
- ) => void;
cancelSimilarArtistsLoad: () => void;
showReactHost: (pageId: ShellPageId) => void;
};
diff --git a/webui/static/api-monitor.js b/webui/static/api-monitor.js
index aa356e8b..d53d0667 100644
--- a/webui/static/api-monitor.js
+++ b/webui/static/api-monitor.js
@@ -2316,6 +2316,25 @@ async function openWatchlistArtistDetailView(artistId, artistName) {
if (artist.mood) metaTags.push(`${escapeHtml(artist.mood)}`);
if (artist.label) metaTags.push(`${escapeHtml(artist.label)}`);
+ let discogId = null;
+ let discogSource = null;
+ const activeSrc = (currentMusicSourceName || '').toLowerCase();
+ if (activeSrc.includes('spotify') && spotify_artist_id) {
+ discogId = spotify_artist_id; discogSource = 'spotify';
+ } else if (activeSrc.includes('discogs') && discogs_artist_id) {
+ discogId = discogs_artist_id; discogSource = 'discogs';
+ } else if (activeSrc.includes('deezer') && deezer_artist_id) {
+ discogId = deezer_artist_id; discogSource = 'deezer';
+ } else if (activeSrc.includes('musicbrainz') && musicbrainz_artist_id) {
+ discogId = musicbrainz_artist_id; discogSource = 'musicbrainz';
+ } else if (itunes_artist_id) {
+ discogId = itunes_artist_id; discogSource = 'itunes';
+ } else {
+ discogId = spotify_artist_id || discogs_artist_id || deezer_artist_id || musicbrainz_artist_id || itunes_artist_id;
+ discogSource = spotify_artist_id ? 'spotify' : discogs_artist_id ? 'discogs' : deezer_artist_id ? 'deezer' : musicbrainz_artist_id ? 'musicbrainz' : 'itunes';
+ }
+ const discogHref = discogId ? buildArtistDetailPath(discogId, discogSource) : '#';
+
overlay.innerHTML = `
${artist.banner_url ? `
@@ -2398,7 +2417,7 @@ async function openWatchlistArtistDetailView(artistId, artistName) {
@@ -2410,30 +2429,6 @@ async function openWatchlistArtistDetailView(artistId, artistName) {
closeWatchlistArtistDetailView();
});
- overlay.querySelector('.watchlist-detail-discog-action').addEventListener('click', () => {
- // Use the ID matching the active metadata source
- let discogId, source;
- const activeSrc = (currentMusicSourceName || '').toLowerCase();
- if (activeSrc.includes('spotify') && spotify_artist_id) {
- discogId = spotify_artist_id; source = 'spotify';
- } else if (activeSrc.includes('discogs') && discogs_artist_id) {
- discogId = discogs_artist_id; source = 'discogs';
- } else if (activeSrc.includes('deezer') && deezer_artist_id) {
- discogId = deezer_artist_id; source = 'deezer';
- } else if (activeSrc.includes('musicbrainz') && musicbrainz_artist_id) {
- discogId = musicbrainz_artist_id; source = 'musicbrainz';
- } else if (itunes_artist_id) {
- discogId = itunes_artist_id; source = 'itunes';
- } else {
- discogId = spotify_artist_id || discogs_artist_id || deezer_artist_id || musicbrainz_artist_id || itunes_artist_id;
- source = spotify_artist_id ? 'spotify' : discogs_artist_id ? 'discogs' : deezer_artist_id ? 'deezer' : musicbrainz_artist_id ? 'musicbrainz' : 'itunes';
- }
- if (discogId) {
- closeWatchlistArtistDetailView();
- navigateToArtistDetailPage(discogId, artistName, source);
- }
- });
-
overlay.querySelector('.watchlist-detail-settings-action').addEventListener('click', () => {
// Remove overlay immediately so it doesn't block the config modal
const detailOverlay = document.querySelector('.watchlist-artist-detail-overlay');
diff --git a/webui/static/discover.js b/webui/static/discover.js
index 99191459..e1d05c12 100644
--- a/webui/static/discover.js
+++ b/webui/static/discover.js
@@ -269,10 +269,9 @@ function displayDiscoverHeroArtist(artist) {
if (discographyBtn && artistId) {
discographyBtn.setAttribute('data-artist-id', artistId);
discographyBtn.setAttribute('data-artist-name', artist.artist_name);
- // Source the click handler will pass to navigateToArtistDetail. Without
- // this, source-only hero artists (which is the typical case โ they
- // come from discover similar-artists, not the library) get looked up
- // as library IDs and 404. Backend always includes artist.source.
+ discographyBtn.href = buildArtistDetailPath(artistId, artist.source || null);
+ // Keep the source on the link so source-only hero artists resolve to
+ // the correct artist-detail URL instead of being treated as library IDs.
if (artist.source) discographyBtn.setAttribute('data-source', artist.source);
else discographyBtn.removeAttribute('data-source');
// Also store both IDs for cross-source operations
@@ -557,7 +556,7 @@ function renderRecommendedArtistsModal(modal, artists, source = null) {
const similarText = artist.occurrence_count > 1
? `Similar to ${artist.occurrence_count} in your watchlist`
: 'Similar to an artist in your watchlist';
- const artistSource = artist.source || source || _recommendedArtistsSource || '';
+ const artistSource = artist.source || source || _recommendedArtistsSource || '';
return `
`;
}).join('')}
@@ -599,16 +602,6 @@ function renderRecommendedArtistsModal(modal, artists, source = null) {
if (watchlistBtn) {
e.stopPropagation();
toggleRecommendedWatchlist(watchlistBtn);
- return;
- }
-
- const card = e.target.closest('.recommended-artist-card');
- if (card) {
- const artistId = card.getAttribute('data-artist-id');
- const artistSource = card.getAttribute('data-artist-source') || _recommendedArtistsSource || null;
- const nameEl = card.querySelector('.recommended-card-name');
- const artistName = nameEl ? nameEl.textContent : '';
- viewRecommendedArtistDiscography(artistId, artistName, artistSource);
}
});
}
@@ -739,11 +732,6 @@ async function checkRecommendedWatchlistStatuses(artists) {
}
}
-async function viewRecommendedArtistDiscography(artistId, artistName, source = null) {
- closeRecommendedArtistsModal();
- navigateToArtistDetailPage(artistId, artistName, source || null);
-}
-
async function checkAllHeroWatchlistStatus() {
const btn = document.getElementById('discover-hero-watch-all');
if (!btn || !discoverHeroArtists || discoverHeroArtists.length === 0) return;
@@ -811,26 +799,6 @@ function jumpToDiscoverHeroSlide(index) {
updateDiscoverHeroIndicators();
}
-async function viewDiscoverHeroDiscography() {
- const button = document.getElementById('discover-hero-discography');
- if (!button) return;
-
- const artistId = button.getAttribute('data-artist-id');
- const artistName = button.getAttribute('data-artist-name');
- // Pass the source so /api/artist-detail knows to synthesize from that
- // metadata provider instead of doing a local DB lookup. Hero similar
- // artists are almost always source-only (not in the library).
- const source = button.getAttribute('data-source') || null;
-
- if (!artistId || !artistName) {
- console.error('No artist data found for discography view');
- return;
- }
-
- console.log(`๐ต Navigating to artist detail for: ${artistName} (source: ${source || 'library'})`);
- navigateToArtistDetailPage(artistId, artistName, source);
-}
-
function showDiscoverHeroEmpty() {
const titleEl = document.getElementById('discover-hero-title');
const subtitleEl = document.getElementById('discover-hero-subtitle');
@@ -4514,10 +4482,7 @@ function _renderYourArtistCard(artist) {
const detailSource = _pickArtistDetailSource(artist);
const hasId = detailSource.id && detailSource.id !== '';
- // Navigate to Artists page (name click) โ source artist id, needs inline view
- const navAction = hasId
- ? `event.stopPropagation(); navigateToArtistDetailPage('${escapeForInlineJs(detailSource.id)}', '${escapeForInlineJs(artist.artist_name)}', '${escapeForInlineJs(detailSource.source)}' || null)`
- : '';
+ const detailHref = hasId ? buildArtistDetailPath(detailSource.id, detailSource.source) : '';
// Open info modal (card body click) โ pass pool ID so we can look up all data
const infoAction = hasId
@@ -4545,7 +4510,9 @@ function _renderYourArtistCard(artist) {
- ${_esc(artist.artist_name)}
+ ${hasId
+ ? `${_esc(artist.artist_name)}`
+ : `${_esc(artist.artist_name)}
`}
`;
@@ -4695,10 +4662,10 @@ async function openYourArtistInfoModal(poolId) {
Explore
-
+
`;
}
} catch (err) {
@@ -6595,9 +6562,9 @@ function _artMapSetupInteraction(canvas) {
ⓘ Artist Info
-
+
👁 ${node.type === 'watchlist' ? 'On Watchlist' : 'Add to Watchlist'}
@@ -7275,9 +7242,9 @@ 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();navigateToArtistDetailPage('${_esc(a.entity_id)}','${_esc(a.name)}','${artSource}' || null)"`;
+ const detailHref = a.entity_id ? buildArtistDetailPath(a.entity_id, a.source || null) : '#';
const srcClass = (a.source || '').toLowerCase();
- return ``;
+ `;
}).join('')}
`;
diff --git a/webui/static/downloads.js b/webui/static/downloads.js
index 5e5cad38..678c1796 100644
--- a/webui/static/downloads.js
+++ b/webui/static/downloads.js
@@ -633,48 +633,6 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam
hideLoadingOverlay();
}
-function _navigateToArtistFromModal(artistId, artistName, imageUrl, source, playlistId) {
- if (!artistName) return;
- // Close the download modal
- const process = playlistId ? activeDownloadProcesses[playlistId] : 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);
- if (!resolvedArtistId || !resolvedSource) {
- showToast(`Artist details are not available for ${artistName}`, 'warning');
- return;
- }
- navigateToArtistDetailPage(resolvedArtistId, artistName, resolvedSource);
-}
-
async function closeDownloadMissingModal(playlistId) {
const process = activeDownloadProcesses[playlistId];
if (!process) {
@@ -5698,13 +5656,13 @@ function _gsRenderFromState(state) {
if (dbArtists.length) {
h += '';
}
if (artists.length) {
h += ``;
- h += artists.map(a => `
${a.image_url ? `

` : '๐ค'}
`).join('');
+ h += artists.map(a => `
${a.image_url ? `` : '๐ค
'}`).join('');
h += '
';
}
@@ -5778,13 +5736,6 @@ async function _gsLazyLoadArtistImages() {
}
}
-function _gsClickArtist(id, name, isLibrary) {
- _gsDeactivate();
- const activeSource = _gsController && _gsController.state.activeSource;
- const source = isLibrary ? null : (activeSource || null);
- navigateToArtistDetailPage(id, name, source);
-}
-
async function _gsClickAlbum(albumId, albumName, artistName, imageUrl, source) {
_gsDeactivate();
// Same flow as handleEnhancedSearchAlbumClick โ fetch album, open download modal
diff --git a/webui/static/library.js b/webui/static/library.js
index 0ef9f1f2..f915d73a 100644
--- a/webui/static/library.js
+++ b/webui/static/library.js
@@ -218,6 +218,7 @@ function displayLibraryArtists(artists) {
// Ignore clicks on badge icons (they open external links / toggle watchlist)
const badge = e.target.closest('.source-card-icon');
if (badge) {
+ e.preventDefault();
e.stopPropagation();
const url = badge.dataset.url;
if (url) { window.open(url, '_blank'); return; }
@@ -233,10 +234,6 @@ function displayLibraryArtists(artists) {
}
return;
}
- const card = e.target.closest('.library-artist-card');
- if (card) {
- navigateToArtistDetailPage(card.dataset.artistId, card.dataset.artistName);
- }
};
}
@@ -299,14 +296,14 @@ function buildLibraryArtistCardHTML(artist, index) {
// Track stats
const trackStat = artist.track_count > 0 ? `${artist.track_count} track${artist.track_count !== 1 ? 's' : ''}` : '';
- return ``;
+ `;
}
function updateLibraryPagination(pagination) {
@@ -780,27 +777,6 @@ if (typeof window !== 'undefined') {
}
-// Public navigation entrypoint for artist detail. Callers should use this so
-// artist-detail navigations stay URL-driven, while the renderer handoff below
-// remains the legacy implementation detail.
-function navigateToArtistDetailPage(artistId, artistName, sourceOverride = null, options = {}) {
- const normalizedSource = sourceOverride || null;
-
- if (!artistId) return false;
- if (currentPage === 'artist-detail' &&
- String(artistId) === String(artistDetailPageState.currentArtistId) &&
- String(normalizedSource || '') === String(artistDetailPageState.currentArtistSource || '')) {
- return true;
- }
-
- return navigateToPage('artist-detail', {
- artistId,
- artistSource: normalizedSource,
- forceReload: true,
- replace: options.replace === true,
- });
-}
-
function navigateToArtistDetail(artistId, artistName, sourceOverride = null, options = {}) {
const normalizedSource = sourceOverride || null;
diff --git a/webui/static/media-player.js b/webui/static/media-player.js
index a6889aa4..b300de5a 100644
--- a/webui/static/media-player.js
+++ b/webui/static/media-player.js
@@ -107,6 +107,21 @@ function setTrackInfo(track) {
document.getElementById('no-track-message').classList.add('hidden');
document.getElementById('media-player').classList.remove('idle');
+ const gotoArtistBtn = document.getElementById('np-goto-artist');
+ if (gotoArtistBtn) {
+ if (track.artist_id) {
+ gotoArtistBtn.href = buildArtistDetailPath(track.artist_id, track.artist_source || null);
+ gotoArtistBtn.style.pointerEvents = '';
+ gotoArtistBtn.setAttribute('aria-disabled', 'false');
+ gotoArtistBtn.tabIndex = 0;
+ } else {
+ gotoArtistBtn.href = '#';
+ gotoArtistBtn.style.pointerEvents = 'none';
+ gotoArtistBtn.setAttribute('aria-disabled', 'true');
+ gotoArtistBtn.tabIndex = -1;
+ }
+ }
+
// Sync expanded player and media session
updateNpTrackInfo();
updateMediaSessionMetadata();
@@ -184,6 +199,14 @@ function clearTrack() {
document.getElementById('no-track-message').classList.remove('hidden');
document.getElementById('media-player').classList.add('idle');
+ const gotoArtistBtn = document.getElementById('np-goto-artist');
+ if (gotoArtistBtn) {
+ gotoArtistBtn.href = '#';
+ gotoArtistBtn.style.pointerEvents = 'none';
+ gotoArtistBtn.setAttribute('aria-disabled', 'true');
+ gotoArtistBtn.tabIndex = -1;
+ }
+
// Reset queue state
npQueue = [];
npQueueIndex = -1;
@@ -1231,15 +1254,11 @@ function initExpandedPlayer() {
});
}
- // Action button (Go to Artist)
+ // Action link (Go to Artist)
const gotoArtistBtn = document.getElementById('np-goto-artist');
if (gotoArtistBtn) {
- gotoArtistBtn.addEventListener('click', () => {
- if (currentTrack && currentTrack.artist_id) {
- closeNowPlayingModal();
- navigateToArtistDetailPage(currentTrack.artist_id, currentTrack.artist || '');
- }
- });
+ gotoArtistBtn.style.textDecoration = 'none';
+ gotoArtistBtn.style.color = 'inherit';
}
// Buffering state listeners on audioPlayer
if (audioPlayer) {
diff --git a/webui/static/search.js b/webui/static/search.js
index 8cedc3c1..4974fad9 100644
--- a/webui/static/search.js
+++ b/webui/static/search.js
@@ -317,11 +317,7 @@ function initializeSearchModeToggle() {
name: artist.name,
meta: 'In Your Library',
badge: { text: 'Library', class: 'enh-badge-library' },
- onClick: () => {
- console.log(`๐ต Opening library artist detail: ${artist.name} (ID: ${artist.id})`);
- hideDropdown();
- navigateToArtistDetailPage(artist.id, artist.name);
- }
+ href: buildArtistDetailPath(artist.id),
})
);
@@ -337,12 +333,7 @@ function initializeSearchModeToggle() {
name: artist.name,
meta: 'Artist',
badge: sourceBadge,
- onClick: () => {
- const sourceOverride = searchController.state.activeSource;
- console.log(`๐ต Opening artist detail: ${artist.name} (ID: ${artist.id}, source: ${sourceOverride})`);
- hideDropdown();
- navigateToArtistDetailPage(artist.id, artist.name, sourceOverride || null);
- }
+ href: buildArtistDetailPath(artist.id, searchController.state.activeSource || null),
})
);
diff --git a/webui/static/shared-helpers.js b/webui/static/shared-helpers.js
index bbc9ca0c..26745e78 100644
--- a/webui/static/shared-helpers.js
+++ b/webui/static/shared-helpers.js
@@ -545,7 +545,8 @@ function renderCompactSection(sectionId, listId, countId, items, mapItem) {
items.forEach(item => {
const config = mapItem(item);
- const elem = document.createElement('div');
+ const isLink = isArtist && !!config.href;
+ const elem = document.createElement(isLink ? 'a' : 'div');
// Add appropriate card class
if (isArtist) {
@@ -566,6 +567,13 @@ function renderCompactSection(sectionId, listId, countId, items, mapItem) {
elem.className = 'enh-compact-item track-item';
}
+ if (isLink) {
+ elem.href = config.href;
+ elem.style.color = 'inherit';
+ elem.style.textDecoration = 'none';
+ elem.setAttribute('aria-label', config.name || 'Artist');
+ }
+
// Build image HTML with type-specific classes
let imageClass = 'enh-item-image';
let placeholderClass = 'enh-item-image-placeholder';
@@ -612,7 +620,9 @@ function renderCompactSection(sectionId, listId, countId, items, mapItem) {
${badgeHtml}
`;
- elem.addEventListener('click', config.onClick);
+ if (config.onClick) {
+ elem.addEventListener('click', config.onClick);
+ }
// Add play button handler for tracks
if (isTrack && config.onPlay) {
@@ -3823,8 +3833,12 @@ function displaySimilarArtists(artists) {
*/
function createSimilarArtistBubble(artist) {
// Create bubble container
- const bubble = document.createElement('div');
+ const bubble = document.createElement('a');
bubble.className = 'similar-artist-bubble';
+ bubble.href = buildArtistDetailPath(artist.id, artist.source || null);
+ bubble.style.color = 'inherit';
+ bubble.style.textDecoration = 'none';
+ bubble.setAttribute('aria-label', artist.name);
bubble.setAttribute('data-artist-id', artist.id);
bubble.setAttribute('data-artist-source', artist.source || '');
if (artist.plugin) {
@@ -3877,13 +3891,6 @@ function createSimilarArtistBubble(artist) {
bubble.appendChild(genres);
}
- // Click โ navigate to the standalone artist-detail page. Works for both
- // library and source artists thanks to the source-aware backend endpoint.
- bubble.addEventListener('click', () => {
- console.log(`๐ต Clicked similar artist: ${artist.name} (ID: ${artist.id})`);
- navigateToArtistDetailPage(artist.id, artist.name, artist.source || null);
- });
-
return bubble;
}
diff --git a/webui/static/shell-bridge.js b/webui/static/shell-bridge.js
index 16167032..66127869 100644
--- a/webui/static/shell-bridge.js
+++ b/webui/static/shell-bridge.js
@@ -165,7 +165,6 @@ window.SoulSyncWebShellBridge = {
activateLegacyPath(pathname) {
activateLegacyPath(pathname);
},
- navigateToArtistDetailPage,
navigateToArtistDetail,
cancelSimilarArtistsLoad() {
if (typeof cancelSimilarArtistsLoad === 'function') {
@@ -177,5 +176,47 @@ window.SoulSyncWebShellBridge = {
},
};
+function _handleShellLinkClick(event) {
+ if (event.defaultPrevented || event.button !== 0 || _isModifiedLinkClick(event)) return;
+
+ const anchor = event.target?.closest?.('a[href]');
+ if (!anchor || (anchor.target && anchor.target !== '_self')) return;
+
+ const href = anchor.getAttribute('href');
+ if (!href || href === '#' || href.startsWith('javascript:')) return;
+
+ const router = getWebRouter();
+ if (!router?.navigateToPage) return;
+
+ const pathname = anchor.pathname || new URL(anchor.href, window.location.href).pathname;
+
+ if (pathname.startsWith('/artist-detail/')) {
+ _handleArtistDetailLinkClick(event, pathname, router);
+ return;
+ }
+}
+
+function _handleArtistDetailLinkClick(event, pathname, router) {
+ const parts = pathname.split('/').filter(Boolean);
+ if (parts.length < 3) return;
+
+ // Keep the semantic link, but hand the click back to TanStack so artist
+ // detail navigations stay in the SPA when the router is available.
+ const source = decodeURIComponent(parts[1] || '');
+ const artistId = decodeURIComponent(parts.slice(2).join('/'));
+ if (!source || !artistId) return;
+
+ event.preventDefault();
+ void router.navigateToPage('artist-detail', {
+ artistId,
+ artistSource: source,
+ });
+}
+
+function _isModifiedLinkClick(event) {
+ return event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
+}
+
window.addEventListener('popstate', syncActivePageFromLocation);
+document.addEventListener('click', _handleShellLinkClick, true);
window.dispatchEvent(new CustomEvent(SHELL_BRIDGE_READY_EVENT));
diff --git a/webui/static/stats-automations.js b/webui/static/stats-automations.js
index 46304405..8e5e5271 100644
--- a/webui/static/stats-automations.js
+++ b/webui/static/stats-automations.js
@@ -162,7 +162,7 @@ async function loadStatsData() {
${i + 1}
${item.image_url ? `
` : ''}
-
${item.id ? `
${_esc(item.name)}` : _esc(item.name)}${item.soul_id && !String(item.soul_id).startsWith('soul_unnamed_') ? '

' : ''}
+
${item.id ? `
${_esc(item.name)}` : _esc(item.name)}${item.soul_id && !String(item.soul_id).startsWith('soul_unnamed_') ? '

' : ''}
${item.global_listeners ? _fmt(item.global_listeners) + ' global listeners' : ''}
${_fmt(item.play_count)} plays
@@ -176,7 +176,7 @@ async function loadStatsData() {
${item.image_url ? `
` : ''}
${_fmt(item.play_count)} plays
@@ -189,7 +189,7 @@ async function loadStatsData() {
${item.image_url ? `
` : ''}
${_fmt(item.play_count)} plays
@@ -231,7 +231,7 @@ function _renderTopArtistsVisual(artists) {
${top5.map((a, i) => {
const pct = Math.round((a.play_count / maxPlays) * 100);
const size = 44 + (4 - i) * 6; // Largest first: 68, 62, 56, 50, 44
- return `
${_esc(a.name)}
${_fmt(a.play_count)}
- `;
+ `;
}).join('')}
`;
}
diff --git a/webui/static/style.css b/webui/static/style.css
index a17d2abd..a6f7619c 100644
--- a/webui/static/style.css
+++ b/webui/static/style.css
@@ -16876,7 +16876,7 @@ body.helper-mode-active #dashboard-activity-feed:hover {
margin-top: 32px;
}
-.watchlist-detail-actions button {
+.watchlist-detail-actions > * {
flex: 1;
padding: 12px 20px;
border-radius: 8px;
@@ -16885,9 +16885,14 @@ body.helper-mode-active #dashboard-activity-feed:hover {
font-weight: 600;
cursor: pointer;
transition: background 0.15s ease, transform 0.1s ease;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ text-decoration: none;
+ box-sizing: border-box;
}
-.watchlist-detail-actions button:active {
+.watchlist-detail-actions > *:active {
transform: scale(0.98);
}
diff --git a/webui/static/sync-spotify.js b/webui/static/sync-spotify.js
index e86fe83b..ad8ba041 100644
--- a/webui/static/sync-spotify.js
+++ b/webui/static/sync-spotify.js
@@ -1993,6 +1993,28 @@ function generateDownloadModalHeroSection(context) {
const artistImage = artist?.image_url || artist?.images?.[0]?.url;
const albumImage = album?.image_url || album?.images?.[0]?.url;
const artistSource = artist?.source || album?.source || context.source || '';
+ const sourceKey = (artistSource || '').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 detailArtistId = artist?.id || artist?.artist_id || '';
+ for (const field of (sourceIdFields[sourceKey] || ['artist_id', 'id'])) {
+ const candidate = artist?.[field];
+ if (candidate) {
+ detailArtistId = candidate;
+ break;
+ }
+ }
+ if (detailArtistId && String(detailArtistId).toLowerCase() === String(artist?.name || '').toLowerCase()) {
+ detailArtistId = '';
+ }
+ const artistHref = detailArtistId ? buildArtistDetailPath(detailArtistId, artistSource || null) : '#';
// Use album image as background if available
if (albumImage) {
@@ -2007,7 +2029,7 @@ function generateDownloadModalHeroSection(context) {