Player: unify local+UPnP behind one authoritative cursor (#171) + queue heart button (#1596) #110

Merged
bvandeusen merged 4 commits from dev into main 2026-07-15 16:30:52 -04:00

4 Commits

Author SHA1 Message Date
bvandeusen 41fe76b90c fix(player): identity-locked UPnP cursor + single index writer — #171
android / Build + lint + test (push) Successful in 4m23s
The local ExoPlayer cursor and the Sonos renderer were two competing
sources of truth for "what's playing" during a cast. The delegate cursor
lagged (forward-only, index-based, size-capped sync, skipped during every
load/re-cast window), and TWO writers of PlayerUiState.queueIndex fought:
PlayerController.onEvents (reading the lagging cursor) stomped the
Sonos-derived index the position tick published, so the in-app player
flickered to the pre-cast track and the notification metadata went stale.

Step 1 — MinstrelForwardingPlayer.syncLocalCursorToRemote (replaces
maybeSyncLocalCursor): align the paused delegate cursor to the track the
renderer is actually playing, matched by track-id parsed from the Sonos
TrackURI (/api/tracks/{id}/stream) against delegate MediaItem.mediaId
(== TrackRef.id). Both directions; survives queue-reload index wobble;
nearest-occurrence tiebreak for duplicate tracks; falls back to the Sonos
Track index; suppressed during load and while a user transport is pending
Sonos's ack. The cursor is now the single authoritative "current track"
that both the in-app UI (onEvents) and the notification (getCurrentMediaItem)
read.

Step 2 — PlayerController: the position tick now patches only
position/duration/play-pause/buffer; onEvents is the sole writer of
queueIndex/currentTrack. Removed desiredQueueIndex, the forward-only
trackChanged path, and publishTickIfChanged. One writer, no stomp.

Part of milestone #171 (unify local + UPnP behind one cursor). Fixes the
flicker + stale-notification symptoms; supersedes #1211/#608/#612.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:59:03 -04:00
bvandeusen 6912dadf2b fix(test): order likes mock before component import in queue tests — #1596
test-web / test (push) Successful in 36s
The prior test fix registered the emptyLikesMock stub but imported it
(and the component under test) in the wrong order: importing QueueTrackRow
/ QueueDrawer transitively loads LikeButton → the mocked $lib/api/likes,
whose hoisted factory runs before the emptyLikesMock import initialized —
"Cannot access '__vi_import_N__' before initialization".

Move the emptyLikesMock import above, and the component import below, the
vi.mock call — matching the ArtistMenu/PlayerBar test layout so the
factory's binding is ready when the component graph loads.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 13:12:37 -04:00
bvandeusen 304e06acc8 test(player): stub likes API in queue component tests — #1596
test-web / test (push) Failing after 33s
QueueTrackRow now renders a LikeButton, which reads createLikedIdsQuery
and needs a QueryClient in Svelte context. The QueueTrackRow / QueueDrawer
unit tests render the rows without one, so they failed with "No
QueryClient was found in Svelte context". Mock $lib/api/likes with the
shared emptyLikesMock() helper — the same pattern PlayerBar/TrackMenu and
17 other component tests already use.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 13:08:06 -04:00
bvandeusen 235839b696 feat(player): heart/like button in queue view (web + android) — #1596
test-web / test (push) Failing after 33s
android / Build + lint + test (push) Successful in 4m25s
The full-screen player's queue ("up next") track rows were the one
track-list surface missing the like heart that TrackRow/PlaylistTrackRow
(web) and playlist/album/artist detail (Android) already carried.

Web: render the shared <LikeButton> in QueueTrackRow between the row body
and the remove button (serves both the /now-playing aside and the mobile
QueueDrawer, same component). LikeButton already stops click propagation
so it won't trigger play-on-click.

Android: PlayerViewModel now exposes likedTrackIds (set-based, the same
idiom as the detail VMs) + toggleLikeTrack; QueueScreen threads
liked/onToggleLike through QueueList → QueueRow, which renders the shared
LikeButton after the duration. Liked state stays sourced from
LikesRepository by track.id — no TrackRef data-model change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 13:02:32 -04:00