feat(web/playlists): For-You tile play button refreshes and plays

Click on the For-You tile's play button now triggers a synchronous
refresh, then enqueues + auto-plays the freshly-built playlist.
Two-click-target tile pattern: tile BODY click still navigates to
the playlist detail page (existing behavior); the play button is
the new generate-and-play action.

Behavior is gated on playlist.system_variant === 'for_you' — every
other playlist (user, Discover, Songs-like-X) keeps the existing
"fetch detail, enqueue, play" flow. The atomic-replace BuildSystem-
Playlists creates a new playlist_id, so the play handler uses the
refresh response's id for the follow-up getPlaylist call rather
than the stale tile prop's id.

Empty-library response (playlist_id: null) is a no-op — clicking
play in a degenerate-empty library doesn't error, it just doesn't
start playback. Caches are invalidated post-refresh so the home
view re-fetches and the tile re-renders with the new id, avoiding
the corner case of a click-then-tile-body 404.

Tests cover the refresh-and-play happy path, the empty-library
no-op, and that non-For-You playlists keep the existing flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 10:40:23 -04:00
parent 54c40c18be
commit e48d84cde1
4 changed files with 161 additions and 3 deletions
+17 -2
View File
@@ -3,7 +3,7 @@
import { useQueryClient } from '@tanstack/svelte-query';
import type { Playlist, PlaylistTrack, TrackRef } from '$lib/api/types';
import { user } from '$lib/auth/store.svelte';
import { getPlaylist, refreshDiscover } from '$lib/api/playlists';
import { getPlaylist, refreshDiscover, refreshForYou } from '$lib/api/playlists';
import { qk } from '$lib/api/queries';
import { playQueue } from '$lib/player/store.svelte';
@@ -60,7 +60,22 @@
if (starting) return;
starting = true;
try {
const detail = await getPlaylist(playlist.id);
let detailID = playlist.id;
// For-You: refresh first so click means "give me a fresh mix
// and play it." Atomic replace creates a new playlist row, so
// the response's playlist_id is what we follow up with.
if (playlist.system_variant === 'for_you') {
const refreshed = await refreshForYou();
if (!refreshed.playlist_id) {
// Empty library — nothing to play.
return;
}
detailID = refreshed.playlist_id;
// Invalidate caches so the home + detail views see the new
// playlist on their next read.
await queryClient.invalidateQueries({ queryKey: qk.playlists() });
}
const detail = await getPlaylist(detailID);
const refs = toTrackRefs(detail.tracks);
if (refs.length > 0) {
playQueue(refs, 0);