From 6fe85f2f37076254e5370d5edeb07da55a19d171 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Sun, 10 May 2026 22:52:11 -0700 Subject: [PATCH] Server playlist sync: append mode (preserve user-added tracks) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discord report (CJFC, 2026-04-26): syncing a Spotify playlist to the server overwrote anything manually added to the server-side playlist. The fix adds a per-sync mode picker next to the Sync button on the playlist details modal — Replace (default, current delete-recreate behavior) or Append only (preserves existing tracks, only adds new ones). Useful when the source platform caps playlist size and the user is manually building beyond it on the server. Implementation: * New `append_to_playlist(name, tracks)` method on Plex / Jellyfin / Navidrome clients. Each uses the server's NATIVE append API: - Plex: `existing_playlist.addItems(new_tracks)` - Jellyfin: `POST /Playlists//Items?Ids=...&UserId=...` - Navidrome: Subsonic `updatePlaylist?songIdToAdd=...` Falls back to `create_playlist` when the playlist doesn't exist yet (first sync). No delete-recreate, no backup playlist created (preserves playlist creation date + metadata + non-soulsync-managed tracks). * Dedup-by-server-native-id (ratingKey for Plex, GUID for Jellyfin, song-id for Navidrome) — never re-adds a track already on the playlist. Server-native identity, not fuzzy title+artist match, so it can't false-collide. * `sync_service.sync_playlist` accepts `sync_mode='replace'|'append'` kwarg. Single if/else branch dispatches to `append_to_playlist` or `update_playlist`. Threaded through `core/discovery/sync.run_sync_task` and the `/api/sync/start` HTTP handler. Validation on the API rejects unknown mode strings (defaults to 'replace'). * Frontend: per-playlist ` + // rendered next to the Sync button, else default 'replace' to preserve + // historical behavior for any caller that hasn't been updated yet. + let syncMode = syncModeOverride; + if (!syncMode) { + const modeSelect = document.getElementById(`sync-mode-${playlistId}`); + syncMode = (modeSelect && modeSelect.value) || 'replace'; + } + if (syncMode !== 'replace' && syncMode !== 'append') { + syncMode = 'replace'; + } + console.log(`🚀 [${new Date().toTimeString().split(' ')[0]}] Starting sync for playlist: ${playlistId} (mode: ${syncMode})`); const playlist = spotifyPlaylists.find(p => p.id === playlistId); if (!playlist) { console.error(`❌ Could not find playlist data for ID: ${playlistId}`); @@ -4160,7 +4172,8 @@ async function startPlaylistSync(playlistId) { playlist_id: playlist.id, playlist_name: playlist.name, tracks: tracks, // Send the full track list - image_url: playlist.image_url || '' + image_url: playlist.image_url || '', + sync_mode: syncMode }) }); diff --git a/webui/static/helper.js b/webui/static/helper.js index 7757d242..1850f610 100644 --- a/webui/static/helper.js +++ b/webui/static/helper.js @@ -3413,6 +3413,11 @@ function closeHelperSearch() { // projects that span multiple commits before shipping. Strip the flag at // release time and add a real `date:` line at the top of the version block. const WHATS_NEW = { + '2.6.0': [ + // --- post-release patch work on the 2.6.0 line — entries hidden by _getLatestWhatsNewVersion until the build version bumps --- + { date: 'Unreleased — 2.6.0 patch work' }, + { title: 'Server Playlist Sync: Append Mode (Stop Overwriting User-Added Tracks)', desc: 'discord report (cjfc, 2026-04-26): syncing a spotify playlist to your server overwrote anything you\'d manually added to the server-side playlist. now there\'s a per-sync mode picker next to the Sync button on the playlist details modal: "Replace" (default, current behavior — delete + recreate) or "Append only" (preserve existing, only add tracks not already there). useful when the source platform caps playlist size (spotify 100-track limit) and you\'re manually building beyond it on the server. each server client (plex / jellyfin / navidrome) gets a new `append_to_playlist(name, tracks)` method that uses the server\'s native append api — plex `addItems`, jellyfin `POST /Playlists//Items`, navidrome subsonic `updatePlaylist?songIdToAdd=...`. no delete-recreate, no backup playlist created in append mode (preserves playlist creation date + metadata + non-soulsync-managed tracks). dedup-by-id ensures we never add a track that\'s already on the playlist (matched by ratingKey for plex, jellyfin guid id for jellyfin, song id for navidrome — server-native identity, not fuzzy title+artist match). falls back to `create_playlist` when the playlist doesn\'t exist yet (first sync). sync_service dispatches via the new mode flag through /api/sync/start; soulsync standalone has no playlist methods at all so the dispatch falls back to update_playlist with a warning log when append is requested against it. 15 new tests pin: missing playlist → create delegation, dedup filtering (existing ids skipped), short-circuit on no-new-tracks (no api call), failure paths return False without raising, contract listing for each server client.', page: 'sync' }, + ], '2.5.0': [ // --- May 10, 2026 — 2.5.0 release --- { date: 'May 10, 2026 — 2.5.0 release' }, diff --git a/webui/static/style.css b/webui/static/style.css index 34314341..49bbbe00 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -15537,6 +15537,24 @@ body.helper-mode-active #dashboard-activity-feed:hover { box-shadow: 0 6px 20px rgba(var(--accent-rgb), 0.4); } +/* Sync mode picker — Replace vs Append, sits left of the Sync button. + Sized to match the modal-btn height so it doesn't break footer alignment. */ +.playlist-modal-sync-mode { + background: #2a2a2a; + color: #ffffff; + border: 1px solid #555555; + border-radius: 6px; + padding: 8px 10px; + font-size: 13px; + cursor: pointer; + margin-right: 4px; +} + +.playlist-modal-sync-mode:hover { + border-color: #777777; + background: #333333; +} + .playlist-modal-btn-tertiary { background: #404040; color: #ffffff; diff --git a/webui/static/sync-services.js b/webui/static/sync-services.js index 4dc00ed8..8eeececb 100644 --- a/webui/static/sync-services.js +++ b/webui/static/sync-services.js @@ -1683,6 +1683,10 @@ function showDeezerArlPlaylistDetailsModal(playlist, originalDeezerPlaylistId) { + diff --git a/webui/static/sync-spotify.js b/webui/static/sync-spotify.js index 796d1ce5..ee0eeca9 100644 --- a/webui/static/sync-spotify.js +++ b/webui/static/sync-spotify.js @@ -1935,6 +1935,10 @@ function showPlaylistDetailsModal(playlist) { ? '📊 View Download Results' : '📥 Download Missing Tracks'} +