expose remote state - #1704
expose remote state#1704ralphptorres wants to merge 35 commits into
Conversation
semantically diff for player changes. also emit cluster and queue updates to their broadcast channels
use server cluster update reasons for cluster changes. also emit cluster topology snapshots with all devices and active device
diff prev and next tracks for queue changes. also emit queue list
There was a problem hiding this comment.
Pull request overview
This PR exposes remote Spotify Connect state updates from Spirc via three broadcast/watch channel pairs (cluster, player, queue) so clients can observe playback/device changes even when this device isn’t the active Connect device (relates to #1448).
Changes:
- Added new public state/event types (
ClusterState,QueueList,*UpdateEvent,*UpdateReason,DeviceInfo) and associated tokio broadcast/watch channels toSpirc. - Implemented emission of player/cluster/queue update events and state snapshots primarily from incoming
ClusterUpdates. - Extended
handle_cluster_updateto maintain and publish cluster/player/queue state, with diff-based semantic “reason” classification.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let info = DeviceInfo { | ||
| device_id: device.device_id.clone(), | ||
| device_alias: device.name.clone(), | ||
| device_type: format!("{:?}", device.device_type), |
There was a problem hiding this comment.
DeviceInfo.device_type is documented as a human-readable value (e.g. "Speaker", "Phone"), but it’s currently built via format!("{:?}", device.device_type). Since this field is a protobuf enum wrapper, the Debug output is typically not user-friendly/stable. Consider using the enum’s string name (e.g. enum_value_or_default().as_str_name()) or mapping to your own display strings to match the docs.
| device_type: format!("{:?}", device.device_type), | |
| device_type: device | |
| .device_type | |
| .enum_value_or_default() | |
| .as_str_name() | |
| .to_string(), |
There was a problem hiding this comment.
didn't implement -- doesn't work with the protobuf version. could add a proper mapping later if needed
account for elapsed time and play/pause state. also rename to SeekChanged
|
Hey, what's the state of this PR @ralphptorres? Ready to merge besides the clippy/fmt fixes? |
|
ready now @photovoltex - fmt/clippy clean, plus fixed a few real bugs found along the way (local playback wasn't reaching the broadcast channels, initial connect never hydrated watch state when another device was already active) and added unit tests plus an example. tested end-to-end against spotifyd :) |
Broadcast and watch channels for the cluster (devices, active device), the player state and the queue, from librespot-org#1704 by Ralph Torres, squashed onto this fork as of its head dff9e8d. Differences from the pull request: the queue channel does not react to the SetQueue player event, which needs librespot-org#1677 and isn't in this fork, so PlayerUpdateReason::QueueChanged is never sent, and the tests import ProvidedTrack themselves. The changelog keeps only this feature's line.
Broadcast and watch channels for the cluster (devices, active device), the player state and the queue, from librespot-org#1704 by Ralph Torres, squashed as of its head dff9e8d. Only the textual merge with this fork's disconnected-playback code differs from the pull request.
librespot-org#1704 reacts to the SetQueue player event from librespot-org#1677, which this fork doesn't have: drop those two match arms, so PlayerUpdateReason::QueueChanged is never sent. The tests import ProvidedTrack themselves, and the changelog keeps only the line for this feature (the others came from upstream's changelog).
Spotifast learned what another device was doing by polling the Web API every 4 seconds, so a pause or stop there took up to that long, plus a request, to show, and a phone playing for an hour cost about 900 requests. librespot already receives every change on the active device from Spotify the moment it happens. The engine now hands those updates to the app. A pause, seek, shuffle or repeat change on the same song shows at once without a request, a new song or device is looked up at once (and again, up to three times, while the Web API still reports the old one), and the Web API is polled every 30 seconds as a fallback. A device switch that went wrong settles on the first update instead of the next poll. Measured with a second librespot device paused from the web player: Spotifast showed the pause 11 ms after Spotify's update reached the engine. In a minute of playback on the other device it made 2 requests instead of 15. Needs the remote state channels from librespot-org/librespot#1704 on top of Spirc::transfer_to (Magniquick/librespot, branch transfer-to-remote-state).
Spotifast learned what another device was doing by polling the Web API every 4 seconds, so a pause or stop there took up to that long, plus a request, to show, and a phone playing for an hour cost about 900 requests. librespot already receives every change on the active device from Spotify the moment it happens. The engine now hands those updates to the app. A pause, seek, shuffle or repeat change on the same song shows at once without a request, a new song or device is looked up at once (and again, up to three times, while the Web API still reports the old one), and the Web API is polled every 30 seconds as a fallback. A device switch that went wrong settles on the first update instead of the next poll. Measured with a second librespot device paused from the web player: Spotifast showed the pause 11 ms after Spotify's update reached the engine. In a minute of playback on the other device it made 2 requests instead of 15. Needs the remote state channels from librespot-org/librespot#1704 with Spirc::transfer_to on top (Magniquick/librespot, branch remote-state-transfer-to).
expose 3 broadcast/watch channel pairs to observe remote playback and other states. pattern-wise, broadcast channels (lightweight) emit only update (or state change) events with semantic reasons. watch channels (stateful) emit complete state snapshots
partially closes #1448
also fixed
ShuffleChanged/RepeatChanged/SetQueueplayer events never carried aplay_request_idso the pre-existing gate inhandle_player_eventsilently dropped them beforeupdate_statewas set. this pr narrows the gate to only the events that actually carry one