Fix 404 on source-artist click — revert Phase 4a source migrations, bump to 2.48

Phase 4a (9361c29) mistakenly routed every artist click to
navigateToArtistDetail, which fetches /api/artist-detail/<id>. That
endpoint only knows how to look up local DB primary keys. For source
artists (Spotify/Deezer/iTunes/etc.) the id is a metadata-source id,
not a library PK — so clicks 404'd out.

Library artists (db_artists section in search results, library page
clicks, stats links, media player) continue to go to the standalone
/artist-detail page as before. Source artists now route back to the
Artists page's inline view via selectArtistForDetail, which calls
/api/artist/<id>/discography with a source param — the endpoint that
actually handles non-library IDs.

Reverted 7 migration points:
  - search.js: Enhanced Search source-artists onClick
  - downloads.js: global widget _gsClickArtist non-library branch
  - downloads.js: _navigateToArtistFromModal fallback
  - discover.js: viewRecommendedArtistDiscography
  - discover.js: viewDiscoverHeroDiscography
  - discover.js: 'Your Artists' card name-click inline HTML
  - 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 artist discography view

Phase 4a's goal of "one artist page for everything" is deferred —
it needs backend work on /api/artist-detail to accept a source param
and fall back to metadata-source lookup when the local DB lookup
fails. Keeping the signature extension on navigateToArtistDetail
(source parameter) in place for when that lands.
pull/361/head
Broque Thomas 2 months ago
parent d037643908
commit 19e9174866

@ -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.47"
_SOULSYNC_BASE_VERSION = "2.48"
def _build_version_string():
"""Append short commit hash to version when available (e.g. 2.35+abc1234)."""
@ -22809,6 +22809,16 @@ def get_version_info():
"title": "What's New in SoulSync",
"subtitle": f"Version {SOULSYNC_VERSION} — Latest Changes",
"sections": [
{
"title": "Fix 404 When Clicking Source Artists in Search",
"description": "Phase 4a mistakenly routed every artist click — including source artists from Spotify/Deezer/iTunes/etc. — to the library artist detail page, which only knows how to look up local DB primary keys. Source artist IDs (like Deezer's 525046) 404'd out",
"features": [
"• Library artists (db_artists section) continue to go to the standalone /artist-detail page — same as before, still works",
"• Source artists (Spotify/Deezer/iTunes/Discogs/Hydrabase/MusicBrainz sections) now route back to the Artists page's inline view, which fetches discography via /api/artist/<id>/discography with source context — the endpoint that actually knows how to handle non-library IDs",
"• Applied to all 7 Phase 4a migration points: Search results, global widget, Discover 'Your Artists' cards, Discover hero recommendations, Discover artist-map context menu, Discover genre-deep-dive, watchlist discography, download-missing modal, recommended artists modal",
"• Phase 4a's 'one artist page for everything' goal remains deferred until /api/artist-detail gains source-aware fallback behavior",
],
},
{
"title": "Interactive Help Annotations Updated for Unified Search",
"description": "The click-for-help annotations and the 'Your First Download' guided tour were rewritten for the new Search page. Stale annotations pointing at removed elements (toggle buttons, side-panel queues) were deleted; the tour now walks users through the source picker instead of the old mode toggle",

@ -2385,9 +2385,16 @@ 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 the standalone artist detail page
// Watchlist discogId is a metadata-source id (Spotify/Deezer/iTunes),
// not a library PK — route through the Artists page inline view.
closeWatchlistArtistDetailView();
navigateToArtistDetail(discogId, artistName, source);
navigateToPage('artists');
setTimeout(() => {
selectArtistForDetail(
{ id: discogId, name: artistName, image_url: artist.image_url || '' },
{ source: source }
);
}, 200);
}
});

@ -739,7 +739,14 @@ async function checkRecommendedWatchlistStatuses(artists) {
async function viewRecommendedArtistDiscography(artistId, artistName) {
closeRecommendedArtistsModal();
navigateToArtistDetail(artistId, artistName);
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);
}
async function checkAllHeroWatchlistStatus() {
@ -821,8 +828,21 @@ 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}`);
navigateToArtistDetail(artistId, 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);
}
function showDiscoverHeroEmpty() {
@ -4613,9 +4633,9 @@ function _renderYourArtistCard(artist) {
const watchlistClass = artist.on_watchlist ? 'active' : '';
const hasId = artist.active_source_id && artist.active_source_id !== '';
// Navigate to standalone artist detail page (name click)
// Navigate to Artists page (name click) — source artist id, needs inline view
const navAction = hasId
? `event.stopPropagation(); navigateToArtistDetail('${escapeForInlineJs(artist.active_source_id)}', '${escapeForInlineJs(artist.artist_name)}')`
? `event.stopPropagation(); navigateToPage('artists'); setTimeout(() => selectArtistForDetail({id:'${escapeForInlineJs(artist.active_source_id)}', name:'${escapeForInlineJs(artist.artist_name)}', image_url:'${escapeForInlineJs(img)}'}), 200)`
: '';
// Open info modal (card body click) — pass pool ID so we can look up all data
@ -4794,7 +4814,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(); navigateToArtistDetail('${escapeForInlineJs(artistId)}', '${escapeForInlineJs(artistName)}', '${escapeForInlineJs(pool.active_source || '')}' || null)">
<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)">
<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>
@ -6694,7 +6714,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(); navigateToArtistDetail('${escapeForInlineJs(bestId)}', '${escapeForInlineJs(node.name)}', '${bestSource}' || null)">
<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)">
<span>&#128191;</span> View Discography
</div>
<div class="artmap-ctx-item" onclick="_artMapHideContextMenu(); toggleYourArtistWatchlist(0,'${escapeForInlineJs(node.name)}','${escapeForInlineJs(bestId)}','${bestSource}',null)">
@ -7382,7 +7402,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();navigateToArtistDetail('${_esc(a.entity_id)}','${_esc(a.name)}','${artSource}' || null)"`;
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 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,7 +634,15 @@ function _navigateToArtistFromModal(artistId, artistName, imageUrl, source, play
if (!artistName) return;
// Close the download modal
if (playlistId) closeDownloadMissingModal(playlistId);
navigateToArtistDetail(artistId || artistName, artistName, source || null);
// 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);
}
async function closeDownloadMissingModal(playlistId) {
@ -5425,8 +5433,19 @@ function _gsSwitchSource(src) {
function _gsClickArtist(id, name, isLibrary) {
_gsDeactivate();
const source = isLibrary ? null : (_gsState.activeSource || null);
navigateToArtistDetail(id, name, source);
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);
}
}
async function _gsClickAlbum(albumId, albumName, artistName, imageUrl, source) {

@ -3547,6 +3547,11 @@ function closeHelperSearch() {
// ═══════════════════════════════════════════════════════════════════════════
const WHATS_NEW = {
'2.48': [
// --- April 24, 2026 (fix) ---
{ date: 'April 24, 2026 (fix)' },
{ title: 'Fix 404 When Clicking Source Artists in Search', desc: 'Phase 4a mistakenly routed every artist click — including source artists from Spotify/Deezer/iTunes/etc. — to the library artist detail page, which only knows how to look up local DB primary keys. Source artist IDs (like Deezer\'s 525046) 404\'d out. Library artists continue to land on the standalone /artist-detail page. Source artists now route back to the Artists page\'s inline view, which fetches discography via /api/artist/<id>/discography with source context — the endpoint that actually knows how to handle non-library IDs. Fix applied to 7 Phase 4a migration points: Search results, global widget, Discover "Your Artists" cards, Discover hero recommendations, artist-map context menu, genre-deep-dive, watchlist discography, download-missing modal, recommended artists modal', page: 'search' },
],
'2.47': [
// --- April 24, 2026 ---
{ date: 'April 24, 2026' },

@ -340,7 +340,17 @@ function initializeSearchModeToggle() {
const sourceOverride = _activeSearchSource;
console.log(`🎵 Opening artist detail: ${artist.name} (ID: ${artist.id}, source: ${sourceOverride})`);
hideDropdown();
navigateToArtistDetail(artist.id, artist.name, sourceOverride || null);
// 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,
});
}
})
);

Loading…
Cancel
Save