Skip to content

Spotify Connect plugin provider feature additions#3857

Draft
iVolt1 wants to merge 20 commits into
music-assistant:devfrom
iVolt1:Spotify_Connect_PR
Draft

Spotify Connect plugin provider feature additions#3857
iVolt1 wants to merge 20 commits into
music-assistant:devfrom
iVolt1:Spotify_Connect_PR

Conversation

@iVolt1
Copy link
Copy Markdown
Contributor

@iVolt1 iVolt1 commented May 8, 2026

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_connect provider 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:

  • Seek bar — the progress bar moves correctly during playback and jumps to the seeked position when the user seeks from the Spotify app
  • Pause behavior — the player stays assigned to Spotify Connect through pause, so resume works from the MA UI without having to go back to the Spotify app
  • Source takeover — playing local MA content to the same player now correctly pauses Spotify and hands off the player cleanly
  • Signal chain display — the info button now shows the full signal chain including input format (44.1kHz/16bit from librespot) and downstream player output formats
  • Previous button fix — the previous track button no longer throws a JSON decode error
  • General MA improvement — the play_media reroute in player_queues.py benefits any plugin source provider, not just spotify_connect

The Frontend Queue Approach

MA's frontend resolves the active player's seek bar, progress tracking, and signal chain display through a PlayerQueue object. It looks up the queue via player.active_source — when a plugin source is active, active_source equals the plugin's instance_id. If no queue exists with that ID, the frontend has nothing to render.

The obvious solution — registering a real PlayerQueue in player_queues._queues — causes a serious side effect: the backend then routes all play/pause/play_media commands to that queue, which has no items and no stream, breaking playback control entirely.

The solution is to fire a QUEUE_ADDED event with queue_id=instance_id that the frontend stores in its queue map, but deliberately never write to player_queues._queues on 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_media when the user plays local content to the same player — is handled by a new reroute in player_queues.py that transparently redirects to the real player queue and triggers normal source takeover.


Changes

spotify_connect/__init__.py

Added _register_plugin_queue()

Fires QUEUE_ADDED with a frontend-only queue containing:

  • current_item.streamdetails.audio_format (PCM S16LE 44100Hz/16bit) for signal chain display
  • current_item.streamdetails.dsp from get_stream_dsp_details() for downstream player format display
  • elapsed_time for seek bar rendering
  • duration for seek bar range

Called on source select and after each track_changed metadata update. The metadata call uses a 0.5s delay (call_later) to ensure Vue's reactivity system picks up the updated current_item and refreshes the quality indicator on first connection.

Added seeked event handler

Fires QUEUE_TIME_UPDATED with force_update=True to bypass MA's change-detection guard (which filters out elapsed_time-only changes) and move the frontend seek bar to the seeked position.

Removed paused event handler block

Previously cleared in_use_by on every paused event, releasing the player back to MA after a few seconds. This caused two problems:

  1. MA would take over the player UI during a temporary pause, preventing resume from the MA UI
  2. The play_media reroute (see below) could not match the player because active_source became None

The player now stays assigned to Spotify Connect through pause until MA explicitly takes over via source selection or session disconnect.

Added output_format to player.extra_data in _on_source_selected

The frontend signal chain display reads output_format from player.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_player when the player is released.

Fixed _on_previous

Added want_result=False to match _on_next, since Spotify's /me/player/previous returns 204 No Content rather than JSON. Without this flag, attempting to decode the empty response caused a JSON decode error surfaced in the UI.


player_queues.py

Added plugin source reroute in play_media

When queue_id is not found in _queues, checks whether it matches an active plugin source via player.state.active_source — the computed __final_active_source property which checks plugin_source.in_use_by. Note: player.active_source (the raw hardware-reported value) is not used here as it is often None for MA-controlled players.

If matched, queue_id is 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.py

getattr fallback for StreamMetadata.media_type

Changed:

media_type=source.metadata.media_type,

To:

media_type=getattr(source.metadata, 'media_type', MediaType.PLUGIN_SOURCE),  # fallback for providers using StreamMetadata which lacks media_type

StreamMetadata (used by spotify_connect) does not have a media_type attribute unlike PlayerMedia. Since StreamMetadata lives in the music_assistant_models repo, a getattr fallback with the original default (MediaType.PLUGIN_SOURCE) is the appropriate fix within the server repo.


Testing

  • Seek bar tracks position during playback and moves correctly on seek from Spotify app
  • Pause from MA UI freezes bar at correct position; resume works from MA UI
  • Play Now on local content while Spotify is playing correctly takes over the player
  • Play Now on local content while Spotify is paused correctly takes over the player
  • Next and previous buttons work without errors
  • Signal chain info button shows 44.1kHz/16bit input and correct downstream player output formats
  • Quality indicator (LQ) appears immediately on first track connection, not only on track change

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (via QUEUE_ADDED/QUEUE_UPDATED/QUEUE_TIME_UPDATED) to enable seek bar + signal chain display.
  • Add a play_media reroute in PlayerQueuesController so 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 StreamMetadata lacks media_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.

Comment thread music_assistant/controllers/player_queues.py Outdated
Comment thread music_assistant/providers/spotify_connect/__init__.py
Comment thread music_assistant/providers/spotify_connect/__init__.py
iVolt1 added 4 commits May 12, 2026 11:07
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"
Copy link
Copy Markdown
Member

@marcelveldt marcelveldt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@MarvinSchenkel MarvinSchenkel marked this pull request as draft May 13, 2026 09:54
@iVolt1
Copy link
Copy Markdown
Contributor Author

iVolt1 commented May 13, 2026

@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.

image

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants