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 <cursoragent@cursor.com>
This commit is contained in:
Francesco Durighetto 2026-06-04 14:01:26 +02:00
parent 83e3f6b660
commit 269e89d514
2 changed files with 127 additions and 8 deletions

View file

@ -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 = {

View file

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