Fix snapcast grouping after the player model refactor (#2372)
authorMaxim Raznatovski <nda.mr43@gmail.com>
Fri, 5 Sep 2025 10:22:27 +0000 (12:22 +0200)
committerGitHub <noreply@github.com>
Fri, 5 Sep 2025 10:22:27 +0000 (12:22 +0200)
music_assistant/providers/snapcast/player.py
music_assistant/providers/snapcast/provider.py

index b4d8175474eae3794027082ba26d192213ef9224..06b6f38bd14c6ed417aedf591b9e4e05439862bc 100644 (file)
@@ -119,6 +119,7 @@ class SnapCastPlayer(Player):
                 self._stream_task.cancel()
                 with suppress(asyncio.CancelledError):
                     await self._stream_task
+            self._stream_task = None
 
     async def volume_mute(self, muted: bool) -> None:
         """Send MUTE command to given player."""
@@ -137,39 +138,25 @@ class SnapCastPlayer(Player):
         assert group is not None  # for type checking
         # handle client additions
         for player_id in player_ids_to_add or []:
-            if player_id not in group.clients:
-                snapcast_id = self.provider._get_snapclient_id(player_id)
+            snapcast_id = self.provider._get_snapclient_id(player_id)
+            if snapcast_id not in group.clients:
                 await group.add_client(snapcast_id)
-                self._attr_group_members.append(player_id)
+                if player_id not in self._attr_group_members:
+                    self._attr_group_members.append(player_id)
         # handle client removals
         for player_id in player_ids_to_remove or []:
-            if player_id in group.clients:
-                snapcast_id = self.provider._get_snapclient_id(player_id)
+            snapcast_id = self.provider._get_snapclient_id(player_id)
+            if snapcast_id in group.clients:
                 await group.remove_client(snapcast_id)
-                self._attr_group_members.remove(player_id)
+                if player_id in self._attr_group_members:
+                    self._attr_group_members.remove(player_id)
+                # Set default stream and stop ungrouped players
+                removed_snapclient = self.provider._snapserver.client(snapcast_id)
+                await removed_snapclient.group.set_stream("default")
+                if removed_player := self.mass.players.get(player_id):
+                    await removed_player.stop()
         self.update_state()
 
-    async def ungroup(self) -> None:
-        """Ungroup."""
-        if self.synced_to is None:
-            for mass_child_id in list(self.group_members):
-                if mass_child_id != self.player_id:
-                    if child_player := self.mass.players.get(mass_child_id):
-                        await child_player.ungroup()
-            return
-        mass_sync_master_player = self.mass.players.get(self.synced_to)
-        assert mass_sync_master_player is not None  # for type checking
-        mass_sync_master_player._attr_group_members.remove(self.player_id)
-        group = self._get_snapgroup()
-        assert group is not None  # for type checking
-        await group.remove_client(self.snap_client_id)
-        # assign default/empty stream to the player
-        await group.set_stream("default")
-        await self.stop()
-        # make sure that the player manager gets an update
-        self.update_state()
-        mass_sync_master_player.update_state()
-
     async def play_media(self, media: PlayerMedia) -> None:
         """Handle PLAY MEDIA on given player."""
         # ruff: noqa: PLR0915
@@ -183,6 +170,7 @@ class SnapCastPlayer(Player):
                 self._stream_task.cancel()
                 with suppress(asyncio.CancelledError):
                     await self._stream_task
+            self._stream_task = None
 
         # get stream or create new one
         stream_name = self._get_stream_name(SnapCastStreamType.MUSIC)
@@ -391,7 +379,7 @@ class SnapCastPlayer(Player):
         2. Easily identify which stream belongs to which player, for instance to be able to
            delete a music stream even when it is not active due to an announcement.
         """
-        safe_name = create_safe_string(self.display_name, replace_space=True)
+        safe_name = create_safe_string(self.player_id, replace_space=True)
         stream_name = f"{MASS_STREAM_PREFIX}{safe_name}"
         if stream_type == SnapCastStreamType.ANNOUNCEMENT:
             stream_name += MASS_ANNOUNCEMENT_POSTFIX
index 05e08b53723036f2f91d605fed5936f051b7dd8f..714bdeae2c28c2429a0d691c2c88ed308eca2cfd 100644 (file)
@@ -82,7 +82,6 @@ class SnapCastProvider(PlayerProvider):
                 str(self.config.get_value(CONF_SERVER_CONTROL_PORT))
             )
         self._snapcast_stream_idle_threshold = self.config.get_value(CONF_STREAM_IDLE_THRESHOLD)
-        self._stream_tasks = {}
         self._ids_map = bidict({})
 
         if self._use_builtin_server:
@@ -277,7 +276,7 @@ class SnapCastProvider(PlayerProvider):
         for snap_client in self._snapserver.clients:
             if ma_player := self.mass.players.get(self._get_ma_id(snap_client.identifier)):
                 assert isinstance(ma_player, SnapCastPlayer)  # for type checking
-                snap_client.set_callback(ma_player._handle_player_update)
+                ma_player._handle_player_update(snap_client)
 
     def _handle_disconnect(self, exc: Exception) -> None:
         """Handle disconnect callback from snapserver."""