Fix: Double grouped players with Universal Groups (#1959)
authorMaxim Raznatovski <nda.mr43@gmail.com>
Sun, 16 Feb 2025 16:59:34 +0000 (17:59 +0100)
committerGitHub <noreply@github.com>
Sun, 16 Feb 2025 16:59:34 +0000 (17:59 +0100)
* fix: don't allow grouping if already grouped

* fix: periodically check if no subgroups are in a universal group

* feat: auto ungroup when adding to a universal group

* fix: restart playback if a player was automatically ungrouped from a invalid subgroup

* refactor: early return

music_assistant/providers/player_group/__init__.py

index 3aa2eeca229317ec604fb4b398705423dde6cb40..c022b7c3a78e0f14f74bc1e4368f881f932fa8bf 100644 (file)
@@ -551,6 +551,7 @@ class PlayerGroupProvider(PlayerProvider):
         """
         if group_player := self.mass.players.get(player_id):
             self._update_attributes(group_player)
+            await self._ungroup_subgroups_if_found(group_player)
 
     async def create_group(
         self, group_type: str, name: str, members: list[str], dynamic: bool = False
@@ -629,6 +630,9 @@ class PlayerGroupProvider(PlayerProvider):
             )
         group_player.group_childs.append(player_id)
 
+        # Ensure that all player are just in this group and not in any other group
+        await self._ungroup_subgroups_if_found(group_player)
+
         # handle resync/resume if group player was already playing
         if group_player.state == PlayerState.PLAYING and group_type == GROUP_TYPE_UNIVERSAL:
             child_player_provider = self.mass.players.get_player_provider(player_id)
@@ -893,6 +897,36 @@ class PlayerGroupProvider(PlayerProvider):
         player.can_group_with = can_group_with
         self.mass.players.update(player.player_id)
 
+    async def _ungroup_subgroups_if_found(self, player: Player) -> None:
+        """Verify that no player is part of a separate group."""
+        group_type = self.mass.config.get_raw_player_config_value(
+            player.player_id, CONF_ENTRY_GROUP_TYPE.key, CONF_ENTRY_GROUP_TYPE.default_value
+        )
+        if group_type != GROUP_TYPE_UNIVERSAL:
+            return
+
+        changed = False
+        # Verify that no player is part of a separate group
+        for child_player_id in player.group_childs:
+            child_player = self.mass.players.get(child_player_id)
+            if PlayerFeature.SET_MEMBERS not in child_player.supported_features:
+                continue
+            if child_player.group_childs:
+                # This is a leader in another group
+                player_provider = self.mass.players.get_player_provider(child_player_id)
+                for sync_child_id in child_player.group_childs:
+                    if sync_child_id == child_player_id:
+                        continue
+                    await player_provider.cmd_ungroup(sync_child_id)
+                    changed = True
+            if child_player.synced_to:
+                # This is a member of another group
+                await self.cmd_ungroup_member(child_player.player_id, child_player.synced_to)
+                changed = True
+        if changed and player.state == PlayerState.PLAYING:
+            # Restart playback to ensure all members play the same content
+            await self.mass.player_queues.resume(player.player_id)
+
     async def _serve_ugp_stream(self, request: web.Request) -> web.Response:
         """Serve the UGP (multi-client) flow stream audio to a player."""
         ugp_player_id = request.path.rsplit(".")[0].rsplit("/")[-1]