From ec4439da0039b3885fa8589b41357c51d06db233 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Mon, 29 Jun 2026 15:17:07 -0700 Subject: [PATCH] discover: Last.fm Radio + ListenBrainz -> mix cards; remove Browse-by-Genre MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit buildListenBrainzPlaylistsHtml (shared by both sections) now emits a grid of mix cards instead of full-width track-table subsections — each playlist is a card that opens its tracks (lazy-loaded from /api/discover/listenbrainz/playlist/{id}) + Download/Sync in the shared modal. ListenBrainz sync uses its own -sync-total/-sync-matched status spans, so the mix carries a matching statusHtml; the sync button id (discover-lb-playlist-{id}-sync-btn) lines up so startListenBrainzPlaylistSync drives it. The old eager loadTracksForPlaylists calls are now harmless no-ops (their -playlist containers are gone, so loadListenBrainzPlaylistTracks early-returns). Wing-It is omitted from the card modal for now (its dropdown needs modal-aware positioning). Browse by Genre removed (section markup + the loadGenreBrowserTabs call): it was empty (/api/discover/genres/available returns nothing) and redundant with the Genre Explorer pills. 64 script-integrity tests green. --- webui/index.html | 20 +------ webui/static/discover.js | 112 +++++++++++++++------------------------ 2 files changed, 46 insertions(+), 86 deletions(-) diff --git a/webui/index.html b/webui/index.html index 58db7919..630c6145 100644 --- a/webui/index.html +++ b/webui/index.html @@ -3797,24 +3797,8 @@
- -
-
-

🎵 Browse by Genre

-

Discover music by your favorite genres

-
- - -
-
-
-

Loading genres...

-
-
- - -
-
+ diff --git a/webui/static/discover.js b/webui/static/discover.js index 86b64721..3d9b6b15 100644 --- a/webui/static/discover.js +++ b/webui/static/discover.js @@ -50,7 +50,7 @@ async function loadDiscoverPage() { initializeLastfmRadioSection(), // Last.fm Radio section (gated on API key) initializeListenBrainzTabs(), // ListenBrainz playlists (tabbed) loadDecadeBrowserTabs(), // Time Machine (tabbed by decade) - loadGenreBrowserTabs(), // Browse by Genre (tabbed by genre) + // loadGenreBrowserTabs(), // REMOVED (#discover redesign): empty + redundant with Genre Explorer loadListenBrainzPlaylistsFromBackend(), // Load ListenBrainz playlist states for persistence loadDiscoveryBlacklist() // Blocked artists list ]); @@ -3304,84 +3304,58 @@ function groupListenBrainzPlaylists(playlists) { } function buildListenBrainzPlaylistsHtml(playlists, tabId) { - let html = ''; - playlists.forEach((playlist, index) => { + // #discover redesign: each playlist is a mix card (opens its tracks + actions in the shared + // modal) instead of a full-width track-table subsection. Shared by Last.fm Radio + ListenBrainz. + const icon = tabId === 'lastfm_radio' ? '\U0001F4FB' : '\U0001F3A7'; + const mixes = playlists.map(playlist => { const playlistData = playlist.playlist || playlist; const identifier = playlistData.identifier?.split('/').pop() || ''; - console.log(`📋 Playlist ${index}:`, { - title: playlistData.title, - fullIdentifier: playlistData.identifier, - extractedIdentifier: identifier - }); const title = playlistData.title || 'Untitled Playlist'; const creator = playlistData.creator || 'ListenBrainz'; - let trackCount = 50; if (playlistData.annotation?.track_count && playlistData.annotation.track_count > 0) { trackCount = playlistData.annotation.track_count; } else if (playlistData.track && Array.isArray(playlistData.track) && playlistData.track.length > 0) { trackCount = playlistData.track.length; } - - const playlistId = `discover-lb-playlist-${identifier}`; // Use consistent MBID-based ID - const virtualPlaylistId = `discover_lb_${tabId}_${identifier}`; - - html += ` -
-
-
-

${title}

- -
-
- - - - - + const escTitle = escapeForInlineJs(title); + // playlistId is the status base startListenBrainzPlaylistSync targets; LB uses its own + // -sync-total/-sync-matched spans, so we hand the modal a matching statusHtml block. + const playlistId = `discover-lb-playlist-${identifier}`; + const statusHtml = ` + - `; +
`; + return { + key: `lb-${tabId}-${identifier}`, + title, subtitle: `by ${creator}`, trackCount, + coverHtml: `
${icon}
`, + statusBase: playlistId, + statusHtml, + actions: [ + { label: 'Download', closeFirst: true, onclick: `openDownloadModalForListenBrainzPlaylist('${identifier}', '${escTitle}')` }, + { label: 'Sync', primary: true, isSync: true, onclick: `startListenBrainzPlaylistSync('${identifier}')` }, + ], + // Tracks load lazily on open; cache them where displayListenBrainzTracks would so reuse works. + fetchTracks: () => fetch(`/api/discover/listenbrainz/playlist/${identifier}`).then(r => r.json()).then(d => { + const tracks = (d && d.tracks) || []; + listenbrainzTracksCache[identifier] = tracks; + return tracks; + }), + }; }); - return html; + mixes.forEach(m => { _discoverMixRegistry[m.key] = m; }); + return `
${mixes.map(_buildMixCard).join('')}
`; } function loadTracksForPlaylists(playlists) { @@ -4550,7 +4524,9 @@ function openMixModal(mix) { const oc = (a.closeFirst ? closeFirst : '') + a.onclick; return ``; }).join(''); - const syncStatus = base ? ` + // A section can supply its own status markup (e.g. ListenBrainz uses -sync-total/-sync-matched + // spans instead of the generic -sync-completed/-sync-pending). Otherwise use the generic block. + const syncStatus = mix.statusHtml || (base ? ` -
` : ''; +
` : ''); overlay.innerHTML = `