Skip to content

fix: start an external-volume session from the real volume - #425

Merged
devgianlu merged 2 commits into
masterfrom
feat/external-volume-across-sessions
Oct 16, 2026
Merged

devgianlu merged 2 commits into
masterfrom
feat/external-volume-across-sessions

Conversation

@devgianlu

@devgianlu devgianlu commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Cause

With external_volume, a new session's device state started at MaxStateVolume, and the initial-volume branch in handleDealerMessage is skipped for external volume. So the session's first PutState (NEW_DEVICE) told Spotify the device was at 100%, until the controlling application pushed its level through POST /player/volume.

That window opens on every zeroconf takeover and every session restored after a logout. The restored session is idle, so it can stay at 100% for a long time: the Web API's device list showed it at 100 while the real level was 37.

Spotify clients step their volume buttons from the level the device advertised. Reproduced with an Android phone (Spotify 9.1.92): after a takeover, a volume key pressed before the application's push landed sent a SetVolumeCommand for 96% (down) or 100% (up), and the speaker jumped there from 37%.

POST /player/volume couldn't close the window ahead of time either: without a session it answered ErrNoSession.

Change

  • The app holds a volume across sessions, under stateMu. Every change sets it, from the API or a Spotify client, and LastVolume is still saved as before.
  • POST /player/volume is accepted while there is no session, in both no-session paths: the zeroconf branch, and answerWithoutSession during login. It only updates that volume.
  • GET /player/volume without a session answers with that volume, the one the next session starts from, instead of 204.
  • Under external_volume, a new session starts at that volume, so its first PutState carries the real level. The order is: the last volume set while this process runs, else the saved one unless ignore_last_volume is set, else initial_volume (capped at volume_steps).
  • Without external_volume nothing changes: the session still sets its own level once the dealer connects.
  • The API's volume conversion moves to one helper, used with and without a session. A negative absolute volume now clamps to 0, where it used to wrap to full volume.

Checked

  • Unit tests:
    • TestSessionVolume: the precedence above.
    • TestVolumeFromApi: absolute, relative and clamping.
    • TestAnswerWithoutSessionTakesVolume: with no session, POST /player/volume is kept and saved, and GET /player/volume reports it.
    • TestInitStateVolume: the first state carries the held level under external volume, and 100% otherwise.
  • gofmt, the daemon build and go test -tags "test_unit test_integration" ./... pass.
  • With an Android phone (Spotify 9.1.92) against this branch, and a controlling application that pushes its level shortly after active. Volumes read from the decoded PutStates and SetVolumeCommands:
    • Takeover, then volume-down before the push: NEW_DEVICE at 37%, and the phone sent 33%. Before this change it sent 96%.
    • Transfer away: the restored idle session advertised the last level (33%), not 100%.
    • No session, POST /player/volume 25, then takeover: the session started at 25%.
    • Volume keys with the app in the foreground, in the background, after background to foreground, and after a cold start: each a correct step from the real level.

🤖 Generated with Claude Code

devgianlu and others added 2 commits October 16, 2026 10:22
Under external_volume a new session advertised 100% (MaxStateVolume) in its
first state, and kept advertising it until the controlling application pushed
its level. Every zeroconf takeover and every session restored after a logout
opened that window; an idle restored session stayed at 100% until playback
started. A phone that steps its volume buttons from what the device advertised
sent 96% or 100% from there, and the speaker jumped to it.

The app now holds the volume across sessions: every change, from the API or a
Spotify client, sets it, and POST /player/volume is accepted while there is no
session, including during login. Under external_volume a new session starts at
that volume: the last one set in this process, else the saved one unless
ignore_last_volume is set, else initial_volume.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Now that the app holds a volume across sessions and POST /player/volume is
accepted without one, GET answers with the volume the next session starts
from instead of 204. The conversion to the API's steps is shared by every
path that reports or reads a volume in steps.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@devgianlu
devgianlu merged commit 90a4fda into master Oct 16, 2026
11 checks passed
@devgianlu
devgianlu deleted the feat/external-volume-across-sessions branch October 16, 2026 08:50
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