From f521be77209cd73a150366ba741ba171271943f5 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Tue, 26 May 2026 14:48:45 -0700 Subject: [PATCH] LB Sync tab: fix track counts + auto-mirror on discovery complete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups to the LB Sync tab work: 1. **Track counts all showed 0.** The ``/api/discover/listenbrainz/*`` endpoints assemble a JSPF-shaped payload but drop the cached ``track_count`` field from the underlying ``listenbrainz_playlists`` row — the JSON the frontend sees only carries ``title`` / ``creator`` / ``annotation`` / an empty ``track`` array. The Discover-page renderer worked around it by hard-coding a fallback of 50; the Sync-page renderer had no such fallback, so every card displayed "0 tracks". Backend now includes ``track_count`` directly in each playlist payload (it's already in the cached row) so any frontend can render an accurate count without resorting to a default. JS still falls back to ``annotation.track_count`` and then ``track.length`` for older callers. 2. **LB playlists never landed in Mirrored Playlists.** The existing ``/api/listenbrainz/sync/start/`` endpoint runs the converted Spotify tracks through ``_run_sync_task`` — i.e. it pushes them to the user's media server (Plex / Jellyfin / Navidrome / SoulSync) as a server-side playlist. It does NOT call ``database.mirror_playlist``. So no ``mirrored_playlists`` row gets created and the playlist can't be picked up by the Auto-Sync scheduler, can't show up under the Mirrored tab, doesn't participate in pipeline automations — the whole point of the Sync-tab unification. Tidal works because Tidal mirrors on tab load with raw tracks then enriches via discovery. LB tracks only have provider IDs *after* discovery, so the equivalent moment for LB is "discovery complete". Added ``_mirrorListenBrainzAfterDiscovery(mbid)`` that pulls the matched ``spotify_data`` out of ``discovery_results`` and posts to ``/api/mirror-playlist`` via the existing ``mirrorPlaylist`` helper. Hooked into both the WebSocket and HTTP-poll completion handlers of ``startListenBrainzDiscoveryPolling``. UPSERT-keyed on (source, source_playlist_id, profile_id), so re-running discovery is a safe no-op refresh. Result: any LB playlist the user discovers (from either the Discover page or the new Sync tab) now lands in ``mirrored_playlists`` with ``source='listenbrainz'`` + matched tracks carrying canonical ``extra_data`` JSON, ready for the Auto-Sync refresh + sync pipeline wired up in Phase 1a + 1b. --- web_server.py | 1 + webui/static/sync-listenbrainz.js | 4 +- webui/static/sync-services.js | 73 +++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/web_server.py b/web_server.py index d49e238c..31d1b08b 100644 --- a/web_server.py +++ b/web_server.py @@ -29986,6 +29986,7 @@ def _get_lb_discover_playlists(playlist_type): "identifier": f"https://listenbrainz.org/playlist/{playlist['playlist_mbid']}", "title": playlist['title'], "creator": playlist['creator'], + "track_count": playlist.get('track_count', 0), "annotation": playlist.get('annotation', {}), "track": [] } diff --git a/webui/static/sync-listenbrainz.js b/webui/static/sync-listenbrainz.js index 39401423..d025b485 100644 --- a/webui/static/sync-listenbrainz.js +++ b/webui/static/sync-listenbrainz.js @@ -96,7 +96,9 @@ function renderListenBrainzSyncPlaylists() { const title = inner.title || inner.name || 'ListenBrainz Playlist'; const creator = inner.creator || 'ListenBrainz'; let count = 0; - if (inner.annotation && inner.annotation.track_count) { + if (inner.track_count) { + count = inner.track_count; + } else if (inner.annotation && inner.annotation.track_count) { count = inner.annotation.track_count; } else if (Array.isArray(inner.track) && inner.track.length > 0) { count = inner.track.length; diff --git a/webui/static/sync-services.js b/webui/static/sync-services.js index c6a9edd3..085ed157 100644 --- a/webui/static/sync-services.js +++ b/webui/static/sync-services.js @@ -10793,6 +10793,72 @@ async function resetBeatportChart(urlHash) { // LISTENBRAINZ PLAYLIST DISCOVERY & SYNC // ============================================================================ +/** + * Auto-mirror a ListenBrainz playlist into the mirrored_playlists + * table after discovery completes. Pattern parity with Tidal — + * Tidal mirrors on tab load with raw tracks, then discovery enriches; + * LB tracks only have provider IDs after discovery, so we mirror at + * the end. Idempotent (UPSERT on source + source_playlist_id + + * profile_id), so calling it twice is a no-op. + */ +function _mirrorListenBrainzAfterDiscovery(playlistMbid) { + try { + const state = listenbrainzPlaylistStates[playlistMbid]; + if (!state || !state.playlist) return; + if (typeof mirrorPlaylist !== 'function') return; + + const results = state.discovery_results || []; + if (!results.length) return; + + const tracks = results + .filter(r => r && r.spotify_data && r.spotify_data.id) + .map(r => { + const sp = r.spotify_data; + const artistName = Array.isArray(sp.artists) + ? (typeof sp.artists[0] === 'object' ? sp.artists[0].name : sp.artists[0]) + : (sp.artists || ''); + const albumName = (sp.album && typeof sp.album === 'object') + ? sp.album.name : (sp.album || ''); + const albumImage = (sp.album && sp.album.images && sp.album.images[0]) + ? sp.album.images[0].url : (sp.image_url || null); + return { + track_name: sp.name || '', + artist_name: artistName || '', + album_name: albumName || '', + duration_ms: sp.duration_ms || 0, + image_url: albumImage, + source_track_id: sp.id || '', + extra_data: JSON.stringify({ + discovered: true, + provider: sp.source || 'spotify', + confidence: r.confidence || 1.0, + matched_data: sp, + }), + }; + }); + + if (!tracks.length) { + console.warn(`🪞 [LB Mirror] No matched tracks in '${state.playlist.name}', skipping mirror`); + return; + } + + mirrorPlaylist( + 'listenbrainz', + playlistMbid, + state.playlist.name || 'ListenBrainz Playlist', + tracks, + { + owner: state.playlist.creator || 'ListenBrainz', + description: state.playlist.description || '', + image_url: state.playlist.image_url || '', + } + ); + console.log(`🪞 [LB Mirror] Mirrored '${state.playlist.name}' with ${tracks.length} matched tracks`); + } catch (err) { + console.warn('LB mirror-after-discovery failed:', err); + } +} + function startListenBrainzDiscoveryPolling(playlistMbid) { console.log(`🔄 Starting ListenBrainz discovery polling for: ${playlistMbid}`); @@ -10843,6 +10909,10 @@ function startListenBrainzDiscoveryPolling(playlistMbid) { const playlistIdEl = `discover-lb-playlist-${playlistMbid}`; const syncBtn = document.getElementById(`${playlistIdEl}-sync-btn`); if (syncBtn) syncBtn.style.display = 'inline-block'; + // Mirror matched tracks → mirrored_playlists table so the + // playlist participates in Auto-Sync schedules just like + // Tidal / Qobuz / Spotify mirrors do. + _mirrorListenBrainzAfterDiscovery(playlistMbid); showToast('ListenBrainz discovery complete!', 'success'); } }; @@ -10935,6 +11005,9 @@ function startListenBrainzDiscoveryPolling(playlistMbid) { } console.log('✅ ListenBrainz discovery complete:', playlistMbid); + // Mirror matched tracks → mirrored_playlists table so + // the playlist participates in Auto-Sync schedules. + _mirrorListenBrainzAfterDiscovery(playlistMbid); showToast('ListenBrainz discovery complete!', 'success'); }