Spotify Connect plugin provider feature additions#3857
Conversation
There was a problem hiding this comment.
Pull request overview
This PR upgrades the spotify_connect plugin provider to behave more like a first-class source in Music Assistant by supplying frontend queue data (seek/progress + signal chain), improving pause/resume behavior, and enabling clean source takeover when local MA content is played to the same player.
Changes:
- Add a frontend-only “fake” queue for
spotify_connect(viaQUEUE_ADDED/QUEUE_UPDATED/QUEUE_TIME_UPDATED) to enable seek bar + signal chain display. - Add a
play_mediareroute inPlayerQueuesControllerso frontend commands sent to a plugin-source queue id can be redirected to the real player queue for takeover. - Make player media construction more robust for plugin sources by falling back when
StreamMetadatalacksmedia_type.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
music_assistant/providers/spotify_connect/ARCHITECTURE.md |
Documents the new frontend-queue approach, seek handling, and takeover behavior. |
music_assistant/providers/spotify_connect/__init__.py |
Implements frontend-only queue registration, seek event handling, output_format injection, and previous-button fix. |
music_assistant/models/player.py |
Adds a safe fallback when plugin source metadata lacks media_type. |
music_assistant/controllers/player_queues.py |
Reroutes play_media when called with a plugin source id instead of a real queue id. |
CRITICAL — permission check order (player_queues.py) The reroute happens after _check_player_permission(queue_id), so plugin instance IDs fail the permission check before the reroute ever runs. Fix: move the reroute block before the permission check, then call _check_player_permission on the resolved player_id.
2. CRITICAL — websocket filter (__init__.py) QUEUE_* events with object_id=instance_id get filtered out for users with a player_filter because the filter only knows about player IDs. The suggested fix (extend the websocket filter to allow queue IDs of active plugin sources) touches core websocket code. The simpler alternative is to also signal the events with object_id=player_id — but that would collide with the real player queue.
3. PROBLEM — _register_plugin_queue always sets state: "playing" This is the same issue as your PENDING FIX 1 from the continuity doc — the fake queue always looks like it's playing. The fix is to track a _playback_state flag in the provider and pass the correct state into _register_plugin_queue. For issue music-assistant#3 track playback state in the provider so _register_plugin_queue can pass the correct state and a stable elapsed_time_last_updated when paused. Three changes from the input file: Line ~211 — added self._is_paused: bool = False Lines ~878–880 — added _is_paused state tracking block before the call_later, setting True on paused, False on playing/sink/track_changed Lines ~929–930 — elapsed_time_last_updated and state in fake_queue now use _is_paused instead of always being time.time() / "playing"
marcelveldt
left a comment
There was a problem hiding this comment.
To be honest this feels more like a hack than a solution and this can't be accepted in it's current form.
I'd rather zoom in to what specific issues you like to solve than this AI abacadabra. Then we can think of extending the core logic here and there where needed to better serve plugin sources but be aware that spotify connect does not represent a MA queue at all and faking it to be one is really not the solution.
|
@marcelveldt Thank you for the review. While I do agree this may be a bit of a hack and it would be great if the core logic already had the needed built-in support, I wanted to get something out there as Spotify is used by millions and Spotify Connect is probably a Spotify user's first experience when trying MA so it should be a good one. This chart should help show what problems this PR solves.
The first two issues in the chart can be resolved with no changes to core logic while the third can be resolved with a one line change to models/player.py. The first three items are important for bringing the Spotify Connect plugin up to parity with other MA players while the fourth item is marked critical as this quickly turns into bad UX when switching music providers to or from Spotify Connect. In the meantime, would you accept a stripped-down version of this PR addressing issues 1–3 (provider-only changes) while the broader solution is discussed separately? |

With Claude.ai's help I was able to make the Spotify Connect plugin provider operate much more like a typical player provider with vastly improved usability. I hope that the small liberties taken with the MA architecture are acceptable. Everything works great in all of my testing. Thanks.
Spotify Connect: Seek Bar, Pause Behavior, Source Takeover & Signal Chain Display
Summary
The
spotify_connectprovider previously functioned as a basic audio passthrough — it could play Spotify audio to an MA player but offered little integration with the MA UI. The player card showed no seek bar, no progress tracking, no signal chain information, and the pause/resume behavior was unreliable. Playing local MA content to the same player while Spotify was active or paused would throw an error.These changes make it a first-class MA source:
play_mediareroute inplayer_queues.pybenefits any plugin source provider, not justspotify_connectThe Frontend Queue Approach
MA's frontend resolves the active player's seek bar, progress tracking, and signal chain display through a
PlayerQueueobject. It looks up the queue viaplayer.active_source— when a plugin source is active,active_sourceequals the plugin'sinstance_id. If no queue exists with that ID, the frontend has nothing to render.The obvious solution — registering a real
PlayerQueueinplayer_queues._queues— causes a serious side effect: the backend then routes allplay/pause/play_mediacommands to that queue, which has no items and no stream, breaking playback control entirely.The solution is to fire a
QUEUE_ADDEDevent withqueue_id=instance_idthat the frontend stores in its queue map, but deliberately never write toplayer_queues._queueson the backend. The frontend gets exactly what it needs to render the seek bar and signal chain, while the backend remains unaware of the queue and continues routing commands correctly through the plugin source callbacks.The one case where the frontend sends a command to the fake queue ID —
play_mediawhen the user plays local content to the same player — is handled by a new reroute inplayer_queues.pythat transparently redirects to the real player queue and triggers normal source takeover.Changes
spotify_connect/__init__.pyAdded
_register_plugin_queue()Fires
QUEUE_ADDEDwith a frontend-only queue containing:current_item.streamdetails.audio_format(PCM S16LE 44100Hz/16bit) for signal chain displaycurrent_item.streamdetails.dspfromget_stream_dsp_details()for downstream player format displayelapsed_timefor seek bar renderingdurationfor seek bar rangeCalled on source select and after each
track_changedmetadata update. The metadata call uses a 0.5s delay (call_later) to ensure Vue's reactivity system picks up the updatedcurrent_itemand refreshes the quality indicator on first connection.Added
seekedevent handlerFires
QUEUE_TIME_UPDATEDwithforce_update=Trueto bypass MA's change-detection guard (which filters outelapsed_time-only changes) and move the frontend seek bar to the seeked position.Removed
pausedevent handler blockPreviously cleared
in_use_byon everypausedevent, releasing the player back to MA after a few seconds. This caused two problems:play_mediareroute (see below) could not match the player becauseactive_sourcebecameNoneThe player now stays assigned to Spotify Connect through pause until MA explicitly takes over via source selection or session disconnect.
Added
output_formattoplayer.extra_datain_on_source_selectedThe frontend signal chain display reads
output_formatfromplayer.extra_data, normally set by the streams controller during MA queue playback. Since Spotify Connect bypasses the streams controller, it is set manually with the correct librespot output format (PCM S16LE 44100Hz/16bit). Cleared in_clear_active_playerwhen the player is released.Fixed
_on_previousAdded
want_result=Falseto match_on_next, since Spotify's/me/player/previousreturns204 No Contentrather than JSON. Without this flag, attempting to decode the empty response caused a JSON decode error surfaced in the UI.player_queues.pyAdded plugin source reroute in
play_mediaWhen
queue_idis not found in_queues, checks whether it matches an active plugin source viaplayer.state.active_source— the computed__final_active_sourceproperty which checksplugin_source.in_use_by. Note:player.active_source(the raw hardware-reported value) is not used here as it is oftenNonefor MA-controlled players.If matched,
queue_idis transparently rerouted to the real player queue. This triggers normal source takeover — the plugin source receives a broken pipe, Spotify pauses gracefully, and MA takes over. This reroute is general and benefits any plugin source provider.player.pygetattrfallback forStreamMetadata.media_typeChanged:
To:
StreamMetadata(used byspotify_connect) does not have amedia_typeattribute unlikePlayerMedia. SinceStreamMetadatalives in themusic_assistant_modelsrepo, agetattrfallback with the original default (MediaType.PLUGIN_SOURCE) is the appropriate fix within the server repo.Testing