Auto ungroup when trying to form syncgroup with already synced player
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 22 Feb 2026 14:17:53 +0000 (15:17 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 22 Feb 2026 14:17:53 +0000 (15:17 +0100)
music_assistant/controllers/players/controller.py

index 476e16b8cf1bfd25cf43a41cbeb1d9217fef3506..2056915d464038292a660f631ff9e19dadd6e34a 100644 (file)
@@ -1026,12 +1026,9 @@ class PlayerController(ProtocolLinkingMixin, CoreController):
             msg = f"Player {parent_player.name} does not support group commands"
             raise UnsupportedFeaturedException(msg)
 
-        # guard edge case: player already synced to another player
-        if parent_player.state.synced_to:
-            raise PlayerCommandFailed(
-                f"Player {parent_player.name} is already synced to another player on its own, "
-                "you need to ungroup it first before you can join other players to it.",
-            )
+        # handle edge case: player already synced to another player
+        # automatically ungroup it first and wait for state to propagate
+        await self._auto_ungroup_if_synced(parent_player, "setting members")
         # handle dissolve sync group if the target player is currently
         # a sync leader and is being removed from itself
         should_stop = False
@@ -1076,6 +1073,11 @@ class PlayerController(ProtocolLinkingMixin, CoreController):
             ):
                 continue  # already synced to this target
 
+            # handle edge case: child player is synced to a different player
+            # automatically ungroup it first and wait for state to propagate
+            if child_player.state.synced_to and child_player.state.synced_to != target_player:
+                await self._auto_ungroup_if_synced(child_player, f"joining {parent_player.name}")
+
             # power on the player if needed
             if (
                 not child_player.state.powered
@@ -2390,6 +2392,24 @@ class PlayerController(ProtocolLinkingMixin, CoreController):
         task_id = "update_all_players_on_registration"
         self.mass.call_later(delay, _update_all_players, task_id=task_id)
 
+    async def _auto_ungroup_if_synced(self, player: Player, log_context: str) -> None:
+        """
+        Automatically ungroup a player if it's synced to another player.
+
+        :param player: The player to check and potentially ungroup.
+        :param log_context: Additional context for the log message (e.g., target player name).
+        """
+        if not player.state.synced_to:
+            return
+        self.logger.info(
+            "Player %s is already synced to %s, ungrouping it first before %s",
+            player.name,
+            player.state.synced_to,
+            log_context,
+        )
+        await self.cmd_set_members(player.state.synced_to, player_ids_to_remove=[player.player_id])
+        await asyncio.sleep(2)
+
     async def _handle_set_members_with_protocols(
         self,
         parent_player: Player,