Confirm a playback action landed before returning the state - #23
Open
jonico wants to merge 1 commit into
Open
Conversation
control_playback performs its action and then reads the state back exactly once. Spotify's player writes are asynchronous -- /me/player/play and friends answer 204 immediately -- so that single read races the change and frequently reports the state from before the action. Two shapes, both seen repeatedly against the live API: - current_playback() returns None while an idle device wakes, which _playback_state maps to is_playing=False with every field empty. A successful play reads back as "nothing is playing". - current_playback() returns the pre-action state. Enabling shuffle reported shuffle=false; starting a playlist reported the previous track and is_playing=false. A caller cannot distinguish either from a genuine failure, so the honest reading of a successful call is "ignore this return value and poll get_playback_state" -- which defeats the point of returning PlaybackState at all. So confirm the action instead of trusting one read: poll until an action-specific condition holds, up to 5 attempts 200ms apart, and return the last state read if it never does. That keeps the failure mode identical to today's behaviour rather than hanging or raising, and the happy path stays a single request -- an already-correct first read returns immediately. Conditions are chosen to be reliable rather than clever. play only waits for is_playing, because with shuffle on a context need not start on its first track, so the track cannot be predicted. next/previous wait for the track id to differ, which needs the outgoing id, so those two read the state once before acting. seek accepts a window ahead of the target since playback keeps advancing, and rejects a position from before the seek. volume/shuffle/repeat compare against what was requested. A static mock never satisfies a condition, so an autouse fixture zeroes the delay in tests; the attempt count is left alone so the retry behaviour stays under test. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
control_playbackperforms its action and then reads the state back exactly once. Spotify's player writes are asynchronous —PUT /me/player/playand friends answer204immediately — so that single read races the change and frequently returns the state from before the action.Two shapes, both hit repeatedly against the live API:
1. Idle device.
current_playback()returnsNonewhile the device wakes, which_playback_statemaps tois_playing=Falsewith every field empty. A successfulplayreads back as "nothing is playing":2. Pre-action state. Enabling shuffle reports
shuffle: false; starting a playlist reports the previous track withis_playing: false:A caller can't distinguish either from a genuine failure. The honest reading of a successful call becomes "ignore this return value and poll
get_playback_state", which defeats the purpose of returningPlaybackStateat all. It also makes the tool misleading to an LLM caller, which will happily report "playback didn't start" to the user.Fix
Confirm the action rather than trusting one read: poll until an action-specific condition holds, up to 5 attempts 200 ms apart, and return the last state read if it never does.
Conditions are deliberately conservative rather than clever:
playis_playingonly — with shuffle on, a context need not start on its first track, so the track can't be predictedpausenot is_playingnext/previousvolume/shuffle/repeatseekTesting
ruff check,ruff format --check,mypy src/,bandit -r src/all cleanpytest: 178 passed (9 new), suite still runs in 0.16sLive-verified against the real API, reproducing each case that failed before:
Relationship to my other PRs
Independent of #19, #20, #21 and #22 — no shared lines, any merge order works. This one is a bit different in kind: those are withheld-endpoint defects, this is an eventual-consistency race in our own read-back.