LB Sync tab: fix track counts + auto-mirror on discovery complete

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/<mbid>`` 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.
This commit is contained in:
Broque Thomas 2026-05-26 14:48:45 -07:00
parent 969d5ffc1b
commit f521be7720
3 changed files with 77 additions and 1 deletions

View file

@ -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": []
}

View file

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

View file

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