Plex Connect: Ungroup player before starting playback (#2877)
authorAnatosun <33899455+anatosun@users.noreply.github.com>
Mon, 5 Jan 2026 12:02:43 +0000 (13:02 +0100)
committerGitHub <noreply@github.com>
Mon, 5 Jan 2026 12:02:43 +0000 (13:02 +0100)
* Plex Connect: Ungroup player before starting playback

When a user selects a specific player for playback in Plex, automatically
remove it from any sync groups or permanent groups. This improves UX by
ensuring playback only happens on the selected player, not all grouped players.

- Added _ungroup_player_if_needed() helper method
- Calls set_members directly to bypass static member restrictions
- Applied to handle_play_media (new playback) and handle_play (resume)
- Works with temporary syncs, permanent groups, and dynamic groups

* Plex Connect: Add error handling and feature checks to player ungrouping

* Plex Connect: implemented corrections

music_assistant/providers/plex_connect/player_remote.py

index 9d8269f4c3ffe94c35ff957c02e58216cccdd131..a6e2d5095db213b98d2b73861ef5d3a724cf29bb 100644 (file)
@@ -13,7 +13,13 @@ from typing import TYPE_CHECKING, Any
 from urllib.parse import urlparse
 
 from aiohttp import ClientTimeout, web
-from music_assistant_models.enums import EventType, PlayerType, QueueOption, RepeatMode
+from music_assistant_models.enums import (
+    EventType,
+    PlayerFeature,
+    PlayerType,
+    QueueOption,
+    RepeatMode,
+)
 from plexapi.playqueue import PlayQueue
 
 from .gdm import PlexGDMAdvertiser
@@ -343,6 +349,32 @@ class PlexRemoteControlServer:
             },
         )
 
+    async def _ungroup_player_if_needed(self, player_id: str) -> None:
+        """Ungroup player before playback if it's part of a group/sync."""
+        player = self.provider.mass.players.get(player_id)
+        if not player or player.type == PlayerType.GROUP:
+            return
+
+        if not (player.synced_to or player.group_members or player.active_group):
+            return
+
+        LOGGER.debug("Ungrouping player %s before starting playback from Plex", player.display_name)
+        # Use set_members directly on the group to bypass static member check
+        if (
+            player.active_group
+            and (group := self.provider.mass.players.get(player.active_group))
+            and group.supports_feature(PlayerFeature.SET_MEMBERS)
+        ):
+            await group.set_members(player_ids_to_remove=[player_id])
+        elif (
+            player.synced_to
+            and (sync_leader := self.provider.mass.players.get(player.synced_to))
+            and sync_leader.supports_feature(PlayerFeature.SET_MEMBERS)
+        ):
+            await sync_leader.set_members(player_ids_to_remove=[player_id])
+        elif player.group_members and player.supports_feature(PlayerFeature.SET_MEMBERS):
+            await player.set_members(player_ids_to_remove=player.group_members)
+
     async def handle_play_media(self, request: web.Request) -> web.Response:
         """
         Handle playMedia command from Plex controller.
@@ -377,6 +409,10 @@ class PlexRemoteControlServer:
             if not player_id:
                 return web.Response(status=500, text="No player assigned to this server")
 
+            # Ungroup player if it's part of a group/sync
+            # User selected this specific player, so remove from any groups
+            await self._ungroup_player_if_needed(player_id)
+
             if container_key and "/playQueues/" in container_key:
                 # Extract play queue ID from container key
                 queue_id_match = re.search(r"/playQueues/(\d+)", container_key)
@@ -964,6 +1000,8 @@ class PlexRemoteControlServer:
         self._updating_from_plex = True
         try:
             if self._ma_player_id:
+                # Ungroup player before resuming playback
+                await self._ungroup_player_if_needed(self._ma_player_id)
                 await self.provider.mass.players.cmd_play(self._ma_player_id)
             await self._broadcast_timeline()
             return web.Response(status=200)