From 614deba0afc695aff1854ccbc3af57f2880b3ee2 Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Sun, 28 Jun 2026 21:22:30 -0700 Subject: [PATCH] playlist export: Sync to Spotify / Deezer modal options (#945, increment 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds "Sync to Spotify" and "Sync to Deezer" buttons to the mirrored-playlist export modal (#pl-export-modal), alongside the existing ListenBrainz/JSPF options. They POST to the new /export/service/ endpoint; the shared poller now reports service progress ("Pushing to Spotify…"), the matched/unmatched count, and links the created playlist on done. ListenBrainz/JSPF paths unchanged. Static file, no rebuild. This completes the #945 vertical: resolver → Spotify+Deezer write clients → export job + endpoint → modal. Reverse-sync to a service is a clean export (uses the stored per-track service IDs from enrichment), distinct from true bidirectional sync. --- webui/static/stats-automations.js | 34 ++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/webui/static/stats-automations.js b/webui/static/stats-automations.js index e21fb3ce..81e81ca2 100644 --- a/webui/static/stats-automations.js +++ b/webui/static/stats-automations.js @@ -663,11 +663,19 @@ function exportMirroredPlaylist(playlistId, name) {
Sync to ListenBrainz
Create the playlist directly on your ListenBrainz account (needs your LB token).
- -
Each track is matched to its MusicBrainz recording ID (cached → library → file tag → MusicBrainz). Tracks without an ID can't be included — you'll see how many matched.
+ + +
Tracks are matched by ID (MusicBrainz for ListenBrainz/JSPF; the stored Spotify/Deezer ID for those). Tracks without a match can't be included — you'll see how many made it. Renaming/re-syncing can reset play counts on the destination.
`; overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); @@ -684,9 +692,14 @@ function exportMirroredPlaylist(playlistId, name) { async function _startPlaylistExport(playlistId, mode, name) { _setExportStatus(playlistId, `Starting export…`); try { - const resp = await fetch(`/api/playlists/${playlistId}/export/listenbrainz`, { + // Spotify/Deezer go to the service endpoint; ListenBrainz/JSPF keep the LB one. + const isService = (mode === 'spotify' || mode === 'deezer'); + const url = isService + ? `/api/playlists/${playlistId}/export/service/${mode}` + : `/api/playlists/${playlistId}/export/listenbrainz`; + const resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ mode }), + body: isService ? '{}' : JSON.stringify({ mode }), }); const data = await resp.json(); if (!data.success || !data.job_id) { @@ -709,8 +722,19 @@ async function _pollPlaylistExport(jobId, playlistId, mode, name) { const pct = job.total ? Math.round(100 * (job.done || 0) / job.total) : 0; _setExportStatus(playlistId, `Matching ${job.done || 0}/${job.total || 0} (${pct}%)${st.resolved != null ? ` · ${st.resolved} matched` : ''}`); } else if (job.phase === 'pushing') { - _setExportStatus(playlistId, `Pushing to ListenBrainz…`); + const dest = (mode === 'spotify' || mode === 'deezer') ? (mode[0].toUpperCase() + mode.slice(1)) : 'ListenBrainz'; + _setExportStatus(playlistId, `Pushing to ${dest}…`); } else if (job.phase === 'done') { + // Spotify/Deezer export: report added + unmatched and link the new playlist. + if (mode === 'spotify' || mode === 'deezer') { + const dest = mode[0].toUpperCase() + mode.slice(1); + const push = job.push || {}; + const cov = `${st.resolved || 0} added${st.unmatched ? ` · ${st.unmatched} not on ${dest}` : ''}`; + const link = push.url ? ` open` : ''; + _setExportStatus(playlistId, `Exported to ${dest} · ${cov}${link}`, 12000); + if (typeof showToast === 'function') showToast(`Playlist exported to ${dest} (${cov})`, 'success'); + return; + } const sum = job.summary || {}; const cov = `${sum.included || 0}/${sum.total || 0} matched${sum.skipped ? ` · ${sum.skipped} unmatched` : ''}`; if (mode === 'download') {