Skip to content

feat: list the play order in GET /player/queue, and step back without rewinding - #421

Merged
devgianlu merged 3 commits into
devgianlu:masterfrom
leodurandfr:feat/play-order-window
Nov 11, 2026
Merged

devgianlu merged 3 commits into
devgianlu:masterfrom
leodurandfr:feat/play-order-window

Conversation

@leodurandfr

@leodurandfr leodurandfr commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Lets a client show the tracks before and after the current one, like the queue view and the mini player swipe in the Spotify apps, and go back to the track it showed.

The daemon already holds that play order: the window Spotify Connect publishes, up to 32 tracks on each side with the user's queue and shuffling applied, and the metadata cache already prefetches it. Only next_track is exposed today. The Web API's GET /me/player/queue is not an option: with the daemon's token it answered 429.

Change

  • GET /player/queue returns {prev_tracks, track, next_tracks}, each entry {uri, uid, provider, track}:

    • provider is queue, context or autoplay, which is how the apps split their queue view.
    • uid tells two copies of a track apart, and is null where Spotify gives none, as for an album's tracks.
    • track has the /status shape, or is null until its metadata is cached (always null without metadata.enabled). The current entry carries the playing position, the others 0.

    prev_tracks is the play order, not a history. Without a session the endpoint answers 204.

  • A queue event, with no data, says the response changed: the order moved, entries got their metadata, or the session ended. The position alone raises none.

  • POST /player/prev takes an optional {"allow_seeking": bool}, the option of Connect's own skip_prev. false skips back from a track played past three seconds instead of rewinding it. The default is true, and a body that doesn't parse gets it too, since prev used to ignore its body.

  • The first commit, independent of the others, makes track descriptions tolerate incomplete cached metadata: a cached track missing its album, an artist's name or its gid panicked the daemon on a /status read.

GET /status is unchanged: listing the window there would make every read about twenty times larger.

While a queued track plays, prev_tracks misses the context track played just before it: GoNext enters the queue without moving the context position, so a prev from the queued track lands one context track earlier and the queued track stays queued. Connect receives the same window. The PR doesn't change this; the /player/prev description now says where prev lands.

Tested

  • gofmt, go vet and go test -count=1 -tags "test_unit test_integration" ./daemon/... ./spclient/... on each commit; oapi-codegen v2.5.0 regenerates daemon/api_gen.go without a diff.
  • TestApiPlayerQueueWireFormat and TestPlayerQueue* cover the response (wire format, positions, local files, no metadata). TestQueueEvent* cover every emit site, the session end included: removing any one call fails a test. TestQueueKeepsItsEntriesNamed covers eviction during sweeps and listings, TestApiPrev the /player/prev body, and TestApiTrackToleratesIncompleteMetadata incomplete track and episode metadata. The allow_seeking dispatch has no unit test, since the three-second check needs a player with an output; the device run below covers it.
  • On a Raspberry Pi 5 with the iOS app, on the reviewed revision (f64f57e): playlist entries are context with a uid and album entries have none; queued tracks are queue (q1, q2, …, two uids for a track queued twice) and stay queued when a new context starts; an album running out with autoplay on continued as a station with 32 autoplay entries; 32 to 37 named entries are 21 to 25 KB, read in 1 to 5 ms, against about 1 KB for /status; {"allow_seeking": false} 7 s into a track went to the previous one. On the current revision (150b9c0 on master 9085364), going back from a carousel with {"allow_seeking": false} went to the previous track rather than rewinding the current one. The queue event wiring as changed by the review has not been re-checked case by case on the device.

Not in this PR: playing or removing an entry by uid, and reordering the queue.

🤖 Generated with Claude Code

@leodurandfr
leodurandfr marked this pull request as draft October 7, 2026 17:29
@leodurandfr
leodurandfr force-pushed the feat/play-order-window branch from 4048efa to 3483411 Compare October 7, 2026 22:11
@leodurandfr leodurandfr changed the title feat: list the play-order window in /status, and step back without rewinding feat: list the play order in GET /player/queue, and step back without rewinding Oct 7, 2026
@leodurandfr
leodurandfr marked this pull request as ready for review October 7, 2026 22:14
@leodurandfr
leodurandfr force-pushed the feat/play-order-window branch from 3483411 to b0c5f51 Compare October 7, 2026 23:12
leodurandfr added a commit to leodurandfr/Milo that referenced this pull request Oct 7, 2026
The fork's window moved out of /status (devgianlu/go-librespot#421, reworked
for any client): /status is back to upstream's shape, and the play order is
GET /player/queue, {prev_tracks, track, next_tracks}, entries
{uri, uid, provider, track}. The `queue` event now says when that answer
changed.

SpotifySource reads it in the burst that carries `queue` or a connection
(the event is raised only on change, so one missed while /events was down
is never said again), and reads it even when /status fails in that burst.
A 204 or a 404 (stock 0.10.3) lists nothing; any other failed read keeps
the last order (E10). details.queue keeps its shape: uid, provider and
artwork have no consumer yet (the carousel slides text only).

The fake daemon raises `queue` ahead of the events it comes with whenever
its answer changes, as go-librespot compares the answer itself, and its
docstring records the provider/uid values measured on 2026-10-07.

Smoke, cut to Spotify (no shared layer touched): item 1 (no failed unit,
without a reboot), item 3 (iPhone handoff, a track queued from the phone,
the phone's mini-bar swipe both ways), item 9 (no new ERROR/WARNING of the
backend's own). Item 6 (radio -> spotify -> radio) was NOT run: music was
playing. Unit binary: the fork's feat/status-queue (#420 + #421) at 8a92995.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@leodurandfr
leodurandfr force-pushed the feat/play-order-window branch 2 times, most recently from b268b5e to 17dc5ad Compare October 10, 2026 15:05
@leodurandfr
leodurandfr force-pushed the feat/play-order-window branch 2 times, most recently from 30315ab to f64f57e Compare October 16, 2026 08:35

@devgianlu devgianlu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 This review was generated by Claude (Claude Code), not written by hand. Please verify before acting on it.

None of these crash the daemon or play the wrong thing. The ones I'd fix before merging are the null entries after LRU eviction, the missed "gone" event, and the POST /player/prev 400 on bodies that aren't JSON. The API.md note is a quick docs fix.

Checked and fine: the req.Data.(ApiPrev) assertion can't panic (only built in PlayerPrev), provider values are always queue/context/autoplay, and peek handles a nil cache when metadata is disabled.

Comment thread daemon/player_queue.go Resolved
Comment thread daemon/player_queue.go Resolved
Comment thread daemon/api_server.go Resolved
Comment thread API.md Resolved
Comment thread daemon/player_queue.go Resolved
Comment thread daemon/api_server.go Resolved
Comment thread daemon/player.go Resolved
Comment thread daemon/player_queue.go Resolved
Comment thread daemon/player_queue.go Resolved
Comment thread daemon/player_queue.go Resolved
leodurandfr and others added 3 commits November 9, 2026 11:38
newApiResponseStatusMedia dereferenced every metadata field. The playing
track always has them, but next_track and /context/tracks describe
whatever the metadata cache holds, and a cached track missing its album,
an artist's name or its gid panicked the daemon on a /status read.
GET /player/queue, later in this series, describes up to 65 of them.

Read the fields through the generated nil-safe getters, so that missing
metadata is described as empty, a missing album date included. An
episode without a named show lists no artist instead of one blank name.
A track keeps a nameless artist's "" so that artist_names stays paired
with artist_uris, and a track without artists lists [] instead of null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /player/prev rewinds a track played past three seconds. That suits
a previous button, but not a client that already shows the previous
track and wants to go to it, such as a swipe over a carousel. Spotify
Connect's skip_prev command has an option for this, which the daemon
already honors.

Accept an optional {"allow_seeking": bool} body, defaulting to true.
With false, prev goes to the previous track, or restarts the current one
when there is none. Prev used to ignore its body, so a body that does
not parse gets the default rather than a 400.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A client that shows what comes before and after the current track has
only next_track to go on, while the daemon already holds the window
Spotify Connect publishes: up to 32 tracks on each side in play order,
with the user's queue and shuffling applied.

GET /player/queue returns {prev_tracks, track, next_tracks}. Each entry
is {uri, uid, provider, track}, where track is the cached metadata or
null, and the current entry carries the playing position. A queue event
without data is raised where the answer can change: when the window is
published or cleared, when its metadata is cached or the product info
arrives, and when a session that listed entries ends. Each site compares
a signature of the answer, position aside, with the last one announced.

The signature reads the window from the metadata cache with get, and
/context/tracks marks it used again after a listing, so that sweeping
other contexts evicts around it. Run's loop moves into serve so that
tests can drive it without a session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@leodurandfr
leodurandfr force-pushed the feat/play-order-window branch from f64f57e to 150b9c0 Compare November 9, 2026 11:52
@leodurandfr

Copy link
Copy Markdown
Contributor Author

Thanks for the review. I rebased on master (9085364) and folded the fixes into the same three commits: bdcc739, 19de8e9 and 150b9c0. The queue event is now emitted where the play order or its metadata changes, and only when the response's signature differs from the last one announced; removing any of those calls fails a test. The only thing I kept as it was is the k.uri != "" filter; the thread on it explains why.

@devgianlu
devgianlu merged commit 775ba58 into devgianlu:master Nov 11, 2026
11 checks passed
@leodurandfr
leodurandfr deleted the feat/play-order-window branch November 11, 2026 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants