Small simplification for GroupPlayer
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 21 Feb 2026 22:59:42 +0000 (23:59 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 21 Feb 2026 22:59:42 +0000 (23:59 +0100)
music_assistant/models/player.py
music_assistant/providers/sync_group/player.py
music_assistant/providers/universal_group/player.py

index 0ef7d258f86d1e8c4cde95e42cfca377a612deac..9575dd2be9deadb7636680c00a33eef502350351 100644 (file)
@@ -1988,26 +1988,3 @@ __all__ = [
     "PlayerSource",
     "PlayerState",
 ]
-
-
-class GroupPlayer(Player):
-    """Helper class for a (generic) group player."""
-
-    _attr_type: PlayerType = PlayerType.GROUP
-
-    @cached_property
-    def synced_to(self) -> str | None:
-        """Return the id of the player this player is synced to (sync leader)."""
-        # default implementation: groups can't be synced
-        return None
-
-    async def volume_set(self, volume_level: int) -> None:
-        """
-        Handle VOLUME_SET command on the player.
-
-        :param volume_level: volume level (0..100) to set on the player.
-        """
-        # Default implementation:
-        # This will set the (relative) volume level on all child players.
-        # free to override if you want to handle this differently.
-        await self.mass.players.set_group_volume(self, volume_level)
index 097045b932afa099e177f2e53abaffa407bad5d0..9a2be8d1d9181237f970a959d203e9d0d15f128e 100644 (file)
@@ -15,7 +15,7 @@ from music_assistant.constants import (
     CONF_DYNAMIC_GROUP_MEMBERS,
     CONF_GROUP_MEMBERS,
 )
-from music_assistant.models.player import DeviceInfo, GroupPlayer, Player, PlayerMedia
+from music_assistant.models.player import DeviceInfo, Player, PlayerMedia
 
 from .constants import CONF_ENTRY_SGP_NOTE, EXTRA_FEATURES_FROM_MEMBERS
 
@@ -23,7 +23,7 @@ if TYPE_CHECKING:
     from .provider import SyncGroupProvider
 
 
-class SyncGroupPlayer(GroupPlayer):
+class SyncGroupPlayer(Player):
     """Sync Group Player implementation."""
 
     _attr_type: PlayerType = PlayerType.GROUP
@@ -49,6 +49,12 @@ class SyncGroupPlayer(GroupPlayer):
         """Return if the player is a dynamic group player."""
         return bool(self.config.get_value(CONF_DYNAMIC_GROUP_MEMBERS, False))
 
+    @cached_property
+    def synced_to(self) -> str | None:
+        """Return the id of the player this player is synced to (sync leader)."""
+        # groups can't be synced
+        return None
+
     async def on_config_updated(self) -> None:
         """Handle logic when the player is loaded or updated."""
         # Config is only available after the player was registered
@@ -128,6 +134,7 @@ class SyncGroupPlayer(GroupPlayer):
             if (
                 PlayerFeature.SET_MEMBERS in player.state.supported_features
                 and player.state.can_group_with
+                and not player.state.active_group
             ):
                 temp_can_group_with.add(player.player_id)
         return temp_can_group_with
@@ -219,14 +226,21 @@ class SyncGroupPlayer(GroupPlayer):
         prev_leader = self.sync_leader
         was_playing = self.playback_state == PlaybackState.PLAYING
         needs_restart = False
-        if was_playing and prev_leader and prev_leader.player_id in (player_ids_to_remove or []):
+        if prev_leader and prev_leader.player_id in (player_ids_to_remove or []):
             # We're removing the current sync leader while the group is active
             # We need to select a new leader before we can handle the member changes
-            await self.mass.players._handle_cmd_stop(prev_leader.player_id)
-            await asyncio.sleep(1)
+            self.logger.debug(
+                "Removing current sync leader %s from group %s while it is active, "
+                "selecting a new leader and dissolving the current syncgroup",
+                prev_leader.display_name,
+                self.display_name,
+            )
+            if was_playing:
+                await self.mass.players._handle_cmd_stop(prev_leader.player_id)
+                await asyncio.sleep(1)
             await self._dissolve_syncgroup()
             await asyncio.sleep(2)
-            needs_restart = True
+            needs_restart = was_playing
 
         cur_leader = self._select_sync_leader(new_members=player_ids_to_add)
         # handle additions
index ba04fe62bec2cb588dc7325d1605aeccfd323e91..88b5bafa79cadba0b39a7790eaf4539ef1086f17 100644 (file)
@@ -30,7 +30,7 @@ from music_assistant.constants import (
 )
 from music_assistant.helpers.audio import get_player_filter_params
 from music_assistant.helpers.util import TaskManager
-from music_assistant.models.player import DeviceInfo, GroupPlayer, PlayerMedia
+from music_assistant.models.player import DeviceInfo, Player, PlayerMedia
 from music_assistant.providers.universal_group.constants import UGP_FORMAT
 
 from .constants import CONF_ENTRY_SAMPLE_RATES_UGP, CONFIG_ENTRY_UGP_NOTE
@@ -47,15 +47,17 @@ BASE_FEATURES = {
 }
 
 
-class UniversalGroupPlayer(GroupPlayer):
+class UniversalGroupPlayer(Player):
     """Universal Group Player implementation."""
 
+    _attr_type: PlayerType = PlayerType.GROUP
+
     def __init__(
         self,
         provider: UniversalGroupProvider,
         player_id: str,
     ) -> None:
-        """Initialize GroupPlayer instance."""
+        """Initialize UniversalGroupPlayer instance."""
         super().__init__(provider, player_id)
         self.stream: UGPStream | None = None
         self._attr_name = self.config.name or f"Universal Group {player_id}"
@@ -88,6 +90,12 @@ class UniversalGroupPlayer(GroupPlayer):
         """Return if the player requires flow mode."""
         return True
 
+    @cached_property
+    def synced_to(self) -> str | None:
+        """Return the id of the player this player is synced to (sync leader)."""
+        # groups can't be synced
+        return None
+
     @property
     def can_group_with(self) -> set[str]:
         """Return the id's of players this player can group with."""