Route source-artist clicks to standalone /artist-detail page

Part B of the deferred unification cleanup. Now that Part A teaches
/api/artist-detail/<id> to fall back to a metadata-source lookup when
the library DB lookup misses, source-artist clicks can finally land
on the standalone page without 404ing — the goal Phase 4a aimed for
and had to roll back in commit 19e9174.

Re-migrating the seven callsites reverted earlier in this session:
  - search.js enhanced-search source-artist onClick
  - downloads.js _gsClickArtist (global widget non-library branch)
  - downloads.js _navigateToArtistFromModal fallback
  - discover.js viewRecommendedArtistDiscography
  - discover.js viewDiscoverHeroDiscography
  - discover.js 'Your Artists' card navAction inline onclick
  - discover.js 'Your Artists' info-modal 'View All' button
  - discover.js artist-map context menu
  - discover.js genre-deep-dive artist click
  - api-monitor.js watchlist discography view

Each replaces the navigateToPage('artists')+setTimeout+selectArtist-
ForDetail dance with a single navigateToArtistDetail(id, name,
source) call. The third arg seeds artistDetailPageState.currentArtist-
Source, which library.js now reads and forwards as ?source= to the
backend (added in Part A).

Effect: clicking an artist in any of these surfaces now lands on the
standalone /artist-detail page with a stable URL, source context
preserved, and owned-library data merged in when available. Library
artist clicks (unchanged) and media-player / stats links (unchanged)
all continue to use navigateToArtistDetail too, so they now
consistently share one destination.

The inline Artists page (#artists-page + selectArtistForDetail in
artists.js) still exists but has no external callers left — only the
page's own internal search-result click handler references the
function now. Parts D + E will delete the dead inline page and
finally remove artists.js.
pull/361/head
Broque Thomas 1 month ago
parent 89754480be
commit 1c345e4eb5

@ -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);
}
});

@ -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() {
@ -4633,9 +4613,9 @@ function _renderYourArtistCard(artist) {
const watchlistClass = artist.on_watchlist ? 'active' : '';
const hasId = artist.active_source_id && artist.active_source_id !== '';
// Navigate to Artists page (name click) — source artist id, needs inline view
// 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
@ -4814,7 +4794,7 @@ async function openYourArtistInfoModal(poolId) {
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<span>Explore</span>
</button>
<button class="ya-header-btn ya-viewall-btn" onclick="document.getElementById('ya-info-modal-overlay')?.remove(); document.getElementById('your-artists-modal-overlay')?.remove(); navigateToPage('artists'); setTimeout(() => selectArtistForDetail({id:'${escapeForInlineJs(artistId)}', name:'${escapeForInlineJs(artistName)}', image_url:'${escapeForInlineJs(imageUrl)}'}, {source:'${escapeForInlineJs(pool.active_source || '')}'}), 200)">
<button class="ya-header-btn ya-viewall-btn" onclick="document.getElementById('ya-info-modal-overlay')?.remove(); document.getElementById('your-artists-modal-overlay')?.remove(); navigateToArtistDetail('${escapeForInlineJs(artistId)}', '${escapeForInlineJs(artistName)}', '${escapeForInlineJs(pool.active_source || '')}' || null)">
<span>View Discography</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14"/><path d="M12 5l7 7-7 7"/></svg>
</button>
@ -6714,7 +6694,7 @@ function _artMapSetupInteraction(canvas) {
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); ${hasId ? `openYourArtistInfoModal_direct(${JSON.stringify(node).replace(/"/g, '&quot;')})` : ''}">
<span>&#9432;</span> Artist Info
</div>
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); navigateToPage('artists'); setTimeout(() => selectArtistForDetail({id:'${escapeForInlineJs(bestId)}',name:'${escapeForInlineJs(node.name)}',image_url:'${escapeForInlineJs(node.image_url || '')}'},{source:'${bestSource}'}), 200)">
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); navigateToArtistDetail('${escapeForInlineJs(bestId)}', '${escapeForInlineJs(node.name)}', '${bestSource}' || null)">
<span>&#128191;</span> View Discography
</div>
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); toggleYourArtistWatchlist(0,'${escapeForInlineJs(node.name)}','${escapeForInlineJs(bestId)}','${bestSource}',null)">
@ -7402,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 `<div class="genre-dive-artist" ${clickAction}>
<div class="genre-dive-artist-img" style="${a.image_url ? `background-image:url('${_esc(a.image_url)}')` : ''}">

@ -634,15 +634,7 @@ function _navigateToArtistFromModal(artistId, artistName, imageUrl, source, play
if (!artistName) return;
// Close the download modal
if (playlistId) closeDownloadMissingModal(playlistId);
// The id from a download modal is typically a metadata-source id; route via
// the Artists page inline view so the source-aware discography endpoint runs.
navigateToPage('artists');
setTimeout(() => {
selectArtistForDetail(
{ id: artistId || artistName, name: artistName, image_url: imageUrl || '' },
source ? { source: source } : undefined
);
}, 200);
navigateToArtistDetail(artistId || artistName, artistName, source || null);
}
async function closeDownloadMissingModal(playlistId) {
@ -5433,19 +5425,8 @@ function _gsSwitchSource(src) {
function _gsClickArtist(id, name, isLibrary) {
_gsDeactivate();
if (isLibrary) {
// Library artists: id is a local DB PK — use the standalone artist-detail page.
navigateToArtistDetail(id, name);
} else {
// Source artists: id is a Deezer/Spotify/iTunes id — route to the Artists
// page's inline view which fetches discography from the source.
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) {

@ -340,17 +340,7 @@ function initializeSearchModeToggle() {
const sourceOverride = _activeSearchSource;
console.log(`🎵 Opening artist detail: ${artist.name} (ID: ${artist.id}, source: ${sourceOverride})`);
hideDropdown();
// Source artists are NOT library entries — their id is a Deezer/
// Spotify/iTunes id, not a library PK. Route to the Artists page's
// inline selectArtistForDetail which fetches discography from the
// source directly, not the library's /api/artist-detail endpoint.
navigateToPage('artists');
await new Promise(resolve => setTimeout(resolve, 100));
await selectArtistForDetail(artist, {
source: sourceOverride,
plugin: artist.external_urls?.hydrabase_plugin,
});
navigateToArtistDetail(artist.id, artist.name, sourceOverride || null);
}
})
);

Loading…
Cancel
Save