From 269e89d5143e8d880dc1b2726509d59d84c3ca31 Mon Sep 17 00:00:00 2001 From: Francesco Durighetto Date: Thu, 4 Jun 2026 14:01:26 +0200 Subject: [PATCH] Propagate playlist provenance when adding to wishlist from download modal. Manual "Add to Wishlist" from playlist modals now stores playlist_id, playlist_name, and organize_by_playlist in source_context so wishlist requeue can resolve playlist-folder layout instead of defaulting to album. Co-authored-by: Cursor --- tests/wishlist/test_routes.py | 35 ++++++++++++ webui/static/wishlist-tools.js | 100 ++++++++++++++++++++++++++++++--- 2 files changed, 127 insertions(+), 8 deletions(-) diff --git a/tests/wishlist/test_routes.py b/tests/wishlist/test_routes.py index 8292aaba..a74f779b 100644 --- a/tests/wishlist/test_routes.py +++ b/tests/wishlist/test_routes.py @@ -435,6 +435,41 @@ def test_get_wishlist_cycle_returns_stored_value(): assert payload == {"cycle": "singles"} +def test_add_album_track_to_wishlist_preserves_playlist_modal_context(): + runtime, service, _db, _logger, _activity_calls = _build_runtime() + track = { + "id": "track-1", + "name": "Song One", + "artists": [{"name": "Artist One"}], + "duration_ms": 1234, + } + artist = {"id": "artist-1", "name": "Artist One"} + album = {"id": "album-1", "name": "Album One"} + + payload, status = add_album_track_to_wishlist( + runtime, + track=track, + artist=artist, + album=album, + source_type="playlist", + source_context={ + "playlist_id": "37i9dQZF1DX0XUsuxWHRQd", + "playlist_name": "Daily Mix", + "organize_by_playlist": True, + "added_from": "download_modal", + }, + ) + + assert status == 200 + assert payload["success"] is True + add_call = service.add_calls[0] + assert add_call["source_type"] == "playlist" + assert add_call["source_context"]["playlist_id"] == "37i9dQZF1DX0XUsuxWHRQd" + assert add_call["source_context"]["playlist_name"] == "Daily Mix" + assert add_call["source_context"]["organize_by_playlist"] is True + assert add_call["source_context"]["added_from"] == "download_modal" + + def test_add_album_track_to_wishlist_builds_spotify_payload_and_merges_context(): runtime, service, _db, _logger, _activity_calls = _build_runtime() track = { diff --git a/webui/static/wishlist-tools.js b/webui/static/wishlist-tools.js index d2f2db1a..8a0c8a54 100644 --- a/webui/static/wishlist-tools.js +++ b/webui/static/wishlist-tools.js @@ -1315,6 +1315,82 @@ async function handleWishlistDownloadNow() { registerArtistDownload(artist, album, virtualPlaylistId, albumType); } +/** Playlist-modal ids that should NOT receive playlist-folder wishlist provenance. */ +const MODAL_ALBUM_WISHLIST_PREFIXES = [ + 'artist_album_', 'discover_album_', 'enhanced_search_album_', 'seasonal_album_', + 'spotify_library_', 'beatport_release_', 'discover_cache_', +]; +const MODAL_SINGLE_TRACK_WISHLIST_PREFIXES = [ + 'enhanced_search_track_', 'gsearch_track_', +]; +const MODAL_NON_PLAYLIST_WISHLIST_PREFIXES = [ + 'issue_download_', 'library_redownload_', 'redownload_', +]; + +function isModalPlaylistWishlistContext(playlistId) { + const id = String(playlistId || ''); + if (!id || id === 'wishlist' || id.startsWith('wishlist_')) { + return false; + } + if (MODAL_ALBUM_WISHLIST_PREFIXES.some((p) => id.startsWith(p))) { + return false; + } + if (MODAL_SINGLE_TRACK_WISHLIST_PREFIXES.some((p) => id.startsWith(p))) { + return false; + } + if (MODAL_NON_PLAYLIST_WISHLIST_PREFIXES.some((p) => id.startsWith(p))) { + return false; + } + return true; +} + +function resolveModalWishlistSourceType(playlistId) { + return isModalPlaylistWishlistContext(playlistId) ? 'playlist' : 'album'; +} + +/** + * Build source_context for wishlist rows added from a download modal. + * Playlist modals include playlist_id/name so wishlist requeue can use playlist-folder layout. + */ +function buildModalWishlistSourceContext(playlistId, process, trackAlbum, trackArtist, trackAlbumType) { + const timestamp = new Date().toISOString(); + if (!isModalPlaylistWishlistContext(playlistId)) { + return { + album_name: trackAlbum?.name, + artist_name: trackArtist?.name, + album_type: trackAlbumType || 'album', + added_from: 'download_modal', + timestamp, + }; + } + + const playlistName = process.playlist?.name || process.playlistName || 'Unknown Playlist'; + const organizeSource = typeof playlistOrganizeSourceForRef === 'function' + ? playlistOrganizeSourceForRef(playlistId) + : 'spotify'; + const resolveRef = typeof normalizePlaylistOrganizeRef === 'function' + ? normalizePlaylistOrganizeRef(playlistId, organizeSource) + : playlistId; + const organizeEnabled = typeof isPlaylistOrganizeEnabled === 'function' + ? isPlaylistOrganizeEnabled(playlistId) + : false; + + const context = { + playlist_name: playlistName, + playlist_id: resolveRef, + playlist_source: organizeSource, + added_from: 'download_modal', + timestamp, + }; + if (organizeEnabled) { + context.organize_by_playlist = true; + } + if (resolveRef !== playlistId) { + context.ui_playlist_ref = playlistId; + } + return context; +} + /** * Add all tracks from any download modal to the wishlist * Universal handler for all modal types (artist albums, playlists, YouTube, Tidal, etc.) @@ -1351,8 +1427,12 @@ async function addModalTracksToWishlist(playlistId) { // not for playlists, so we must NOT use it as a blanket default. const processArtist = process.artist || null; const album = process.album || process.playlist || { name: 'Playlist', id: playlistId }; + const wishlistSourceType = resolveModalWishlistSourceType(playlistId); - console.log(`🔄 Adding ${tracks.length} tracks from "${album.name}" to wishlist (process artist: ${processArtist?.name || 'per-track'})`); + console.log( + `🔄 Adding ${tracks.length} tracks from "${album.name}" to wishlist ` + + `(source: ${wishlistSourceType}, process artist: ${processArtist?.name || 'per-track'})` + ); // Disable the button to prevent double-clicks const wishlistBtn = document.getElementById(`add-to-wishlist-btn-${playlistId}`); @@ -1394,7 +1474,7 @@ async function addModalTracksToWishlist(playlistId) { } }); } else { - formattedArtists = [{ name: artist.name }]; + formattedArtists = [{ name: 'Unknown Artist' }]; } const formattedTrack = { @@ -1447,6 +1527,14 @@ async function addModalTracksToWishlist(playlistId) { trackArtist = { name: 'Unknown Artist', id: null }; } + const sourceContext = buildModalWishlistSourceContext( + playlistId, + process, + trackAlbum, + trackArtist, + trackAlbumType, + ); + const response = await fetch('/api/add-album-to-wishlist', { method: 'POST', headers: { @@ -1456,12 +1544,8 @@ async function addModalTracksToWishlist(playlistId) { track: formattedTrack, artist: trackArtist, album: trackAlbum, - source_type: 'album', - source_context: { - album_name: trackAlbum.name, - artist_name: trackArtist.name, - album_type: trackAlbumType - } + source_type: wishlistSourceType, + source_context: sourceContext, }) });