Skip to content

feat: Opt-in track metadata cache, next_track and GET /context/tracks - #351

Open
palchrb wants to merge 1 commit into
devgianlu:masterfrom
palchrb:claude/upstream-metadata-cache
Open

feat: Opt-in track metadata cache, next_track and GET /context/tracks#351
palchrb wants to merge 1 commit into
devgianlu:masterfrom
palchrb:claude/upstream-metadata-cache

Conversation

@palchrb

@palchrb palchrb commented Aug 3, 2026

Copy link
Copy Markdown

Closes the second half of #346.

The API describes exactly one track — the playing one — so a client that wants to show what's coming has only the public Web API, which rate-limits. Meanwhile the daemon's own session already fetches this metadata for playback and throws it away. This keeps it: a bounded in-memory cache feeds next_track in /status and a GET /context/tracks listing for any playable context (playlist, album, artist, show, Liked Songs — built on InferSpotifyIdTypeFromContextUri from yesterday's commits).

Everything is off by default (metadata.enabled, metadata.context_sweep): disabled, no caches are constructed, every helper is a nil no-op (locked in by test), and the endpoint answers 404. Nothing blocks the control loop — enumeration runs in the background behind a ready flag, sweeps are serialised and paced at one batch of 100 per second.

Note: this and #350 both add fields to the status schema and state to AppPlayer, so they conflict trivially at a couple of insertion points. They're fully independent features — review in either order, and I'll rebase whichever is still open as soon as the other lands.

…ting

The API describes exactly one track: the one playing. NextTracks in the
player state carries bare uris, so a client that wants to render anything
about the upcoming track — its name, its cover — has only the public Web
API, which rate-limits aggressively. Meanwhile the daemon's authenticated
spclient session already fetches this metadata for playback, over
endpoints sized for client traffic, and throws it away.

Keep it instead. An in-memory cache (bounded, ~1000 entries) remembers
the metadata of loaded and prefetched streams, and a batched
extended-metadata request fills it for the tracks around the playback
position. That feeds two things:

- next_track in GET /status: the upcoming track with full metadata, so a
  client can pre-warm its display before the user skips.

- GET /context/tracks?uri=...: any playable context — playlist, album,
  artist — listed in order with metadata, resolved through the same
  context resolver playback uses. With POST /player/play's existing
  skip_to_uri this is enough for a client to render a browsable song
  list and start any entry, at a cost of one load per pick and zero Web
  API calls.

Everything is opt-in and off by default. A headless speaker has no use
for metadata beyond the playing track and must not pay network requests
for it: with metadata.enabled false the caches are never constructed,
every helper treats the nil caches as a no-op (locked in by test), and
the endpoint answers 404. A second flag, metadata.context_sweep,
additionally resolves the whole context when one starts playing — paced
batches of 100, one per second, capped at metadata.max_tracks — so every
track is known before the user skips anywhere.

Nothing here blocks the control loop. The listing endpoint answers from
what is enumerated and cached, reporting ready=false while enumeration
runs in the background; enumeration is single-flighted per context and
its result reused for five minutes, so a polling client does not re-page
the context. Sweeps are serialised — one runs, at most one waits, a
newer job replaces the waiting one — and a waiting job re-checks what is
missing when it starts, so it never re-fetches what the sweep ahead of
it cached.

Tracks and episodes both list: each uri is resolved under its own
metadata kind (TRACK_V4 or EPISODE_V4) in the same batched request, so
shows enumerate to their episodes and a playlist containing episodes
lists completely. A user's Liked Songs collection
(spotify:user:<id>:collection) is accepted as well — its multi-segment
uri just needed to pass validation; the context resolver already
expands it. The listing carries no revision: an edited playlist is
picked up when the five-minute reuse window expires.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FHWG4ossSBydy7jbVFWho
@palchrb
palchrb force-pushed the claude/upstream-metadata-cache branch from 2de359c to 178d7e0 Compare September 1, 2026 10:02
@palchrb

palchrb commented Sep 1, 2026

Copy link
Copy Markdown
Author

Aligning with upstream as of today

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.

1 participant