Various fixes and enhancements for (TTS) Announcemens (#1728)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sat, 19 Oct 2024 22:35:08 +0000 (00:35 +0200)
committerGitHub <noreply@github.com>
Sat, 19 Oct 2024 22:35:08 +0000 (00:35 +0200)
14 files changed:
music_assistant/server/controllers/player_queues.py
music_assistant/server/controllers/players.py
music_assistant/server/providers/_template_player_provider/__init__.py
music_assistant/server/providers/airplay/__init__.py
music_assistant/server/providers/bluesound/__init__.py
music_assistant/server/providers/chromecast/__init__.py
music_assistant/server/providers/dlna/__init__.py
music_assistant/server/providers/fully_kiosk/__init__.py
music_assistant/server/providers/hass_players/__init__.py
music_assistant/server/providers/player_group/__init__.py
music_assistant/server/providers/slimproto/__init__.py
music_assistant/server/providers/snapcast/__init__.py
music_assistant/server/providers/sonos/__init__.py
music_assistant/server/providers/sonos_s1/__init__.py

index 58362cd1979ba5315dae07210538906936e6f6f7..9a39d27615a5e61fc4ee66f4e2691a7412f6f75a 100644 (file)
@@ -258,15 +258,9 @@ class PlayerQueuesController(CoreController):
     @api_command("player_queues/shuffle")
     def set_shuffle(self, queue_id: str, shuffle_enabled: bool) -> None:
         """Configure shuffle setting on the the queue."""
-        # always fetch the underlying player so we can raise early if its not available
-        queue_player = self.mass.players.get(queue_id, True)
-        if queue_player.announcement_in_progress:
-            self.logger.warning("Ignore queue command: An announcement is in progress")
-            return
         queue = self._queues[queue_id]
         if queue.shuffle_enabled == shuffle_enabled:
             return  # no change
-
         queue.shuffle_enabled = shuffle_enabled
         queue_items = self._queue_items[queue_id]
         cur_index = queue.index_in_buffer or queue.current_index
@@ -276,7 +270,6 @@ class PlayerQueuesController(CoreController):
         else:
             next_items = []
             next_index = 0
-
         if not shuffle_enabled:
             # shuffle disabled, try to restore original sort order of the remaining items
             next_items.sort(key=lambda x: x.sort_index, reverse=False)
@@ -298,11 +291,6 @@ class PlayerQueuesController(CoreController):
     @api_command("player_queues/repeat")
     def set_repeat(self, queue_id: str, repeat_mode: RepeatMode) -> None:
         """Configure repeat setting on the the queue."""
-        # always fetch the underlying player so we can raise early if its not available
-        queue_player = self.mass.players.get(queue_id, True)
-        if queue_player.announcement_in_progress:
-            self.logger.warning("Ignore queue command: An announcement is in progress")
-            return
         queue = self._queues[queue_id]
         if queue.repeat_mode == repeat_mode:
             return  # no change
@@ -559,11 +547,6 @@ class PlayerQueuesController(CoreController):
         - pos_shift: move item x positions up if negative value
         - pos_shift:  move item to top of queue as next item if 0.
         """
-        # always fetch the underlying player so we can raise early if its not available
-        queue_player = self.mass.players.get(queue_id, True)
-        if queue_player.announcement_in_progress:
-            self.logger.warning("Ignore queue command: An announcement is in progress")
-            return
         queue = self._queues[queue_id]
         item_index = self.index_by_id(queue_id, queue_item_id)
         if item_index <= queue.index_in_buffer:
@@ -588,11 +571,6 @@ class PlayerQueuesController(CoreController):
     @api_command("player_queues/delete_item")
     def delete_item(self, queue_id: str, item_id_or_index: int | str) -> None:
         """Delete item (by id or index) from the queue."""
-        # always fetch the underlying player so we can raise early if its not available
-        queue_player = self.mass.players.get(queue_id, True)
-        if queue_player.announcement_in_progress:
-            self.logger.warning("Ignore queue command: An announcement is in progress")
-            return
         if isinstance(item_id_or_index, str):
             item_index = self.index_by_id(queue_id, item_id_or_index)
         else:
@@ -610,11 +588,6 @@ class PlayerQueuesController(CoreController):
     @api_command("player_queues/clear")
     def clear(self, queue_id: str) -> None:
         """Clear all items in the queue."""
-        # always fetch the underlying player so we can raise early if its not available
-        queue_player = self.mass.players.get(queue_id, True)
-        if queue_player.announcement_in_progress:
-            self.logger.warning("Ignore queue command: An announcement is in progress")
-            return
         queue = self._queues[queue_id]
         queue.radio_source = []
         queue.stream_finished = None
@@ -634,10 +607,6 @@ class PlayerQueuesController(CoreController):
 
         - queue_id: queue_id of the playerqueue to handle the command.
         """
-        queue_player: Player = self.mass.players.get(queue_id, True)
-        if queue_player.announcement_in_progress:
-            self.logger.warning("Ignore queue command: An announcement is in progress")
-            return
         if (queue := self.get(queue_id)) and queue.active:
             queue.resume_pos = queue.corrected_elapsed_time
             queue.stream_finished = None
@@ -654,9 +623,6 @@ class PlayerQueuesController(CoreController):
         - queue_id: queue_id of the playerqueue to handle the command.
         """
         queue_player: Player = self.mass.players.get(queue_id, True)
-        if queue_player.announcement_in_progress:
-            self.logger.warning("Ignore queue command: An announcement is in progress")
-            return
         if (
             (queue := self._queues.get(queue_id))
             and queue.active
@@ -697,10 +663,6 @@ class PlayerQueuesController(CoreController):
 
         - queue_id: queue_id of the queue to handle the command.
         """
-        queue_player: Player = self.mass.players.get(queue_id, True)
-        if queue_player.announcement_in_progress:
-            self.logger.warning("Ignore queue command: An announcement is in progress")
-            return
         if (queue := self.get(queue_id)) is None or not queue.active:
             # TODO: forward to underlying player if not active
             return
@@ -722,10 +684,6 @@ class PlayerQueuesController(CoreController):
 
         - queue_id: queue_id of the queue to handle the command.
         """
-        queue_player: Player = self.mass.players.get(queue_id, True)
-        if queue_player.announcement_in_progress:
-            self.logger.warning("Ignore queue command: An announcement is in progress")
-            return
         if (queue := self.get(queue_id)) is None or not queue.active:
             # TODO: forward to underlying player if not active
             return
@@ -741,10 +699,6 @@ class PlayerQueuesController(CoreController):
         - queue_id: queue_id of the queue to handle the command.
         - seconds: number of seconds to skip in track. Use negative value to skip back.
         """
-        queue_player: Player = self.mass.players.get(queue_id, True)
-        if queue_player.announcement_in_progress:
-            self.logger.warning("Ignore queue command: An announcement is in progress")
-            return
         if (queue := self.get(queue_id)) is None or not queue.active:
             # TODO: forward to underlying player if not active
             return
@@ -757,12 +711,9 @@ class PlayerQueuesController(CoreController):
         - queue_id: queue_id of the queue to handle the command.
         - position: position in seconds to seek to in the current playing item.
         """
-        queue_player: Player = self.mass.players.get(queue_id, True)
-        if queue_player.announcement_in_progress:
-            self.logger.warning("Ignore queue command: An announcement is in progress")
-            return
         if not (queue := self.get(queue_id)):
             return
+        queue_player: Player = self.mass.players.get(queue_id, True)
         if not queue.current_item:
             raise InvalidCommand(f"Queue {queue_player.display_name} has no item(s) loaded.")
         if (
@@ -1409,15 +1360,12 @@ class PlayerQueuesController(CoreController):
 
     async def _enqueue_next(self, queue: PlayerQueue, current_index: int | str) -> None:
         """Enqueue the next item in the queue."""
-        if (player := self.mass.players.get(queue.queue_id)) and player.announcement_in_progress:
-            self.logger.warning("Ignore queue command: An announcement is in progress")
-            return
         if isinstance(current_index, str):
             current_index = self.index_by_id(queue.queue_id, current_index)
         with suppress(QueueEmpty):
             next_item = await self.preload_next_item(queue.queue_id, current_index)
             await self.mass.players.enqueue_next_media(
-                player_id=player.player_id,
+                player_id=queue.queue_id,
                 media=self.player_media_from_queue_item(next_item, queue.flow_mode),
             )
 
index e41d2d2d30f48d758370965301e94b757cadd75b..ecac01266d85815a7528aec107d53cd93de4ba89 100644 (file)
@@ -182,9 +182,9 @@ class PlayerController(CoreController):
             await self.mass.player_queues.stop(active_queue.queue_id)
             return
         # send to player provider
-        async with self._player_throttlers[player_id]:
-            if player_provider := self.get_player_provider(player_id):
-                await player_provider.cmd_stop(player_id)
+        async with self._player_throttlers[player.player_id]:
+            if player_provider := self.get_player_provider(player.player_id):
+                await player_provider.cmd_stop(player.player_id)
 
     @api_command("players/cmd/play")
     @handle_player_command
@@ -200,9 +200,9 @@ class PlayerController(CoreController):
             await self.mass.player_queues.play(active_queue.queue_id)
             return
         # send to player provider
-        player_provider = self.get_player_provider(player_id)
-        async with self._player_throttlers[player_id]:
-            await player_provider.cmd_play(player_id)
+        player_provider = self.get_player_provider(player.player_id)
+        async with self._player_throttlers[player.player_id]:
+            await player_provider.cmd_play(player.player_id)
 
     @api_command("players/cmd/pause")
     @handle_player_command
@@ -220,10 +220,10 @@ class PlayerController(CoreController):
             self.logger.info(
                 "Player %s does not support pause, using STOP instead", player.display_name
             )
-            await self.cmd_stop(player_id)
+            await self.cmd_stop(player.player_id)
             return
-        player_provider = self.get_player_provider(player_id)
-        await player_provider.cmd_pause(player_id)
+        player_provider = self.get_player_provider(player.player_id)
+        await player_provider.cmd_pause(player.player_id)
 
         async def _watch_pause(_player_id: str) -> None:
             player = self.get(_player_id, True)
@@ -255,9 +255,9 @@ class PlayerController(CoreController):
         """
         player = self._get_player_with_redirect(player_id)
         if player.state == PlayerState.PLAYING:
-            await self.cmd_pause(player_id)
+            await self.cmd_pause(player.player_id)
         else:
-            await self.cmd_play(player_id)
+            await self.cmd_play(player.player_id)
 
     @api_command("players/cmd/seek")
     async def cmd_seek(self, player_id: str, position: int) -> None:
@@ -275,8 +275,8 @@ class PlayerController(CoreController):
         if PlayerFeature.SEEK not in player.supported_features:
             msg = f"Player {player.display_name} does not support seeking"
             raise UnsupportedFeaturedException(msg)
-        player_prov = self.mass.players.get_player_provider(player_id)
-        await player_prov.cmd_seek(player_id, position)
+        player_prov = self.mass.players.get_player_provider(player.player_id)
+        await player_prov.cmd_seek(player.player_id, position)
 
     @api_command("players/cmd/next")
     async def cmd_next_track(self, player_id: str) -> None:
@@ -290,8 +290,8 @@ class PlayerController(CoreController):
         if PlayerFeature.NEXT_PREVIOUS not in player.supported_features:
             msg = f"Player {player.display_name} does not support skipping to the next track."
             raise UnsupportedFeaturedException(msg)
-        player_prov = self.mass.players.get_player_provider(player_id)
-        await player_prov.cmd_next(player_id)
+        player_prov = self.mass.players.get_player_provider(player.player_id)
+        await player_prov.cmd_next(player.player_id)
 
     @api_command("players/cmd/previous")
     async def cmd_previous_track(self, player_id: str) -> None:
@@ -305,8 +305,8 @@ class PlayerController(CoreController):
         if PlayerFeature.NEXT_PREVIOUS not in player.supported_features:
             msg = f"Player {player.display_name} does not support skipping to the previous track."
             raise UnsupportedFeaturedException(msg)
-        player_prov = self.mass.players.get_player_provider(player_id)
-        await player_prov.cmd_previous(player_id)
+        player_prov = self.mass.players.get_player_provider(player.player_id)
+        await player_prov.cmd_previous(player.player_id)
 
     @api_command("players/cmd/power")
     @handle_player_command
@@ -322,13 +322,14 @@ class PlayerController(CoreController):
             return  # nothing to do
 
         # unsync player at power off
+        player_was_synced = player.synced_to is not None
         if not powered and (player.synced_to):
             await self.cmd_unsync(player_id)
 
         # always stop player at power off
         if (
             not powered
-            and not player.synced_to
+            and not player_was_synced
             and player.state in (PlayerState.PLAYING, PlayerState.PAUSED)
         ):
             await self.cmd_stop(player_id)
@@ -348,6 +349,7 @@ class PlayerController(CoreController):
         else:
             # allow the stop command to process and prevent race conditions
             await asyncio.sleep(0.2)
+            await self.mass.cache.set(player_id, powered, base_key="player_power")
 
         # always optimistically set the power state to update the UI
         # as fast as possible and prevent race conditions
@@ -473,24 +475,28 @@ class PlayerController(CoreController):
     ) -> None:
         """Handle playback of an announcement (url) on given player."""
         player = self.get(player_id, True)
-        while player.announcement_in_progress:
-            await asyncio.sleep(0.5)
         if not url.startswith("http"):
             raise PlayerCommandFailed("Only URLs are supported for announcements")
+        if player.announcement_in_progress:
+            raise PlayerCommandFailed(
+                f"An announcement is already in progress to player {player.display_name}"
+            )
         try:
             # mark announcement_in_progress on player
             player.announcement_in_progress = True
-            # determine if the player(group) has native announcements support
+            # determine if the player has native announcements support
             native_announce_support = PlayerFeature.PLAY_ANNOUNCEMENT in player.supported_features
-            if not native_announce_support and player.synced_to:
-                # redirect to sync master if player is group child
-                self.logger.warning(
-                    "Detected announcement request to a player that is currently synced, "
-                    "this will be redirected to the entire syncgroup."
+            # determine pre-announce from (group)player config
+            if use_pre_announce is None and "tts" in url:
+                use_pre_announce = await self.mass.config.get_player_config_value(
+                    player_id,
+                    CONF_TTS_PRE_ANNOUNCE,
                 )
-                await self.play_announcement(player.synced_to, url, use_pre_announce, volume_level)
-                return
             if not native_announce_support and player.active_group:
+                for group_member in self.iter_group_members(player, True, True):
+                    if PlayerFeature.PLAY_ANNOUNCEMENT in group_member.supported_features:
+                        native_announce_support = True
+                        break
                 # redirect to group player if playergroup is active
                 self.logger.warning(
                     "Detected announcement request to a player which has a group active, "
@@ -500,33 +506,30 @@ class PlayerController(CoreController):
                     player.active_group, url, use_pre_announce, volume_level
                 )
                 return
-            if player.type == PlayerType.GROUP and not player.powered:
-                # announcement request sent to inactive group, check if any child's are playing
-                if len(list(self.iter_group_members(player, True, True))) > 0:
-                    # just for the sake of simplicity we handle this request per-player
-                    # so we can restore the individual players again.
-                    self.logger.warning(
-                        "Detected announcement request to an inactive playergroup, "
-                        "while one or more individual players are playing. "
-                        "This announcement will be redirected to the individual players."
-                    )
-                    async with TaskManager(self.mass) as tg:
-                        for group_member in player.group_childs:
-                            tg.create_task(
-                                self.play_announcement(
-                                    group_member,
-                                    url=url,
-                                    use_pre_announce=use_pre_announce,
-                                    volume_level=volume_level,
-                                )
-                            )
-                    return
-            # determine pre-announce from (group)player config
-            if use_pre_announce is None and "tts" in url:
-                use_pre_announce = await self.mass.config.get_player_config_value(
-                    player_id,
-                    CONF_TTS_PRE_ANNOUNCE,
+
+            # if player type is group with all members supporting announcements
+            # or if the groupplayer is not powered, we forward the request to each individual player
+            if player.type == PlayerType.GROUP and (
+                all(
+                    x
+                    for x in self.iter_group_members(player)
+                    if PlayerFeature.PLAY_ANNOUNCEMENT in x.supported_features
                 )
+                or not player.powered
+            ):
+                # forward the request to each individual player
+                async with TaskManager(self.mass) as tg:
+                    for group_member in player.group_childs:
+                        tg.create_task(
+                            self.play_announcement(
+                                group_member,
+                                url=url,
+                                use_pre_announce=use_pre_announce,
+                                volume_level=volume_level,
+                            )
+                        )
+                return
+
             self.logger.info(
                 "Playback announcement to player %s (with pre-announce: %s): %s",
                 player.display_name,
@@ -561,10 +564,10 @@ class PlayerController(CoreController):
         player = self._get_player_with_redirect(player_id)
         # power on the player if needed
         if not player.powered:
-            await self.cmd_power(player_id, True)
-        player_prov = self.mass.players.get_player_provider(player_id)
+            await self.cmd_power(player.player_id, True)
+        player_prov = self.mass.players.get_player_provider(player.player_id)
         await player_prov.play_media(
-            player_id=player_id,
+            player_id=player.player_id,
             media=media,
         )
 
@@ -717,7 +720,7 @@ class PlayerController(CoreController):
         self._players[player.player_id] = player
         self.update(player.player_id)
 
-    def register(self, player: Player) -> None:
+    async def register(self, player: Player) -> None:
         """Register a new player on the controller."""
         if self.mass.closing:
             return
@@ -752,6 +755,12 @@ class PlayerController(CoreController):
         if not player.enabled:
             return
 
+        # restore powered state from cache
+        if player.state == PlayerState.PLAYING:
+            player.powered = True
+        elif (cache := await self.mass.cache.get(player_id, base_key="player_power")) is not None:
+            player.powered = cache
+
         self.logger.info(
             "Player registered: %s/%s",
             player_id,
@@ -761,7 +770,7 @@ class PlayerController(CoreController):
         # always call update to fix special attributes like display name, group volume etc.
         self.update(player.player_id)
 
-    def register_or_update(self, player: Player) -> None:
+    async def register_or_update(self, player: Player) -> None:
         """Register a new player on the controller or update existing one."""
         if self.mass.closing:
             return
@@ -771,7 +780,7 @@ class PlayerController(CoreController):
             self.update(player.player_id)
             return
 
-        self.register(player)
+        await self.register(player)
 
     def remove(self, player_id: str, cleanup_config: bool = True) -> None:
         """Remove a player from the player manager."""
@@ -794,11 +803,19 @@ class PlayerController(CoreController):
         if player_id not in self._players:
             return
         player = self._players[player_id]
+        prev_state = self._prev_states.get(player_id, {})
         player.active_source = self._get_active_source(player)
         player.volume_level = player.volume_level or 0  # guard for None volume
         # correct group_members if needed
         if player.group_childs == {player.player_id}:
             player.group_childs = set()
+        # Auto correct player state if player is synced (or group child)
+        # This is because some players/providers do not accurately update this info
+        # for the sync child's.
+        if player.synced_to and (sync_leader := self.get(player.synced_to)):
+            player.state = sync_leader.state
+            player.elapsed_time = sync_leader.elapsed_time
+            player.elapsed_time_last_updated = sync_leader.elapsed_time_last_updated
         # calculate group volume
         player.group_volume = self._get_group_volume_level(player)
         if player.type == PlayerType.GROUP:
@@ -820,8 +837,11 @@ class PlayerController(CoreController):
             else CONF_ENTRY_PLAYER_ICON.default_value,
         )
 
+        # correct available state if needed
+        if not player.enabled:
+            player.available = False
+
         # basic throttle: do not send state changed events if player did not actually change
-        prev_state = self._prev_states.get(player_id, {})
         new_state = self._players[player_id].to_dict()
         changed_values = get_changed_values(
             prev_state,
@@ -839,10 +859,6 @@ class PlayerController(CoreController):
             # ignore updates for disabled players
             return
 
-        # correct available state if needed
-        if not player.enabled:
-            player.available = False
-
         # always signal update to the playerqueue
         self.mass.player_queues.on_player_update(player, changed_values)
 
@@ -851,13 +867,12 @@ class PlayerController(CoreController):
 
         self.mass.signal_event(EventType.PLAYER_UPDATED, object_id=player_id, data=player)
 
-        if skip_forward:
+        if skip_forward and not force_update:
             return
 
         # update/signal group player(s) child's when group updates
-        if player.type == PlayerType.GROUP:
-            for child_player in self.iter_group_members(player, exclude_self=True):
-                self.update(child_player.player_id, skip_forward=True)
+        for child_player in self.iter_group_members(player, exclude_self=True):
+            self.update(child_player.player_id, skip_forward=True)
         # update/signal group player(s) when child updates
         for group_player in self._get_player_groups(player, powered_only=False):
             if player_prov := self.mass.get_provider(group_player.provider):
@@ -927,18 +942,6 @@ class PlayerController(CoreController):
                 player.name,
             )
             return active_group
-        if (
-            player.active_source
-            and player.active_source != player.player_id
-            and (active_source := self.get(player.active_source))
-        ):
-            self.logger.info(
-                "Player %s has a different source active (%s), "
-                "redirected the command to the source player.",
-                player.name,
-                active_source.display_name,
-            )
-            return active_source
         return player
 
     def _get_player_groups(
@@ -1098,11 +1101,19 @@ class PlayerController(CoreController):
         """
         prev_power = player.powered
         prev_state = player.state
+        prev_synced_to = player.synced_to
         queue = self.mass.player_queues.get_active_queue(player.player_id)
         prev_queue_active = queue.active
         prev_item_id = player.current_item_id
+        # unsync player if its currently synced
+        if prev_synced_to:
+            self.logger.debug(
+                "Announcement to player %s - unsyncing player...",
+                player.display_name,
+            )
+            await self.cmd_unsync(player.player_id)
         # stop player if its currently playing
-        if prev_state in (PlayerState.PLAYING, PlayerState.PAUSED):
+        elif prev_state in (PlayerState.PLAYING, PlayerState.PAUSED):
             self.logger.debug(
                 "Announcement to player %s - stop existing content (%s)...",
                 player.display_name,
@@ -1171,6 +1182,8 @@ class PlayerController(CoreController):
         if not prev_power:
             await self.cmd_power(player.player_id, False)
             return
+        elif prev_synced_to:
+            await self.cmd_sync(player.player_id, prev_synced_to)
         elif prev_queue_active and prev_state == PlayerState.PLAYING:
             await self.mass.player_queues.resume(queue.queue_id, True)
         elif prev_state == PlayerState.PLAYING:
index ae811a2aa4acc57762fdb460e79e5c3558b4bcc4..cf2b897c02b9c5c5a60edf82cc1193019965f8e9 100644 (file)
@@ -218,7 +218,7 @@ class MyDemoPlayerprovider(PlayerProvider):
             ),
         )
         # register the player with the player manager
-        self.mass.players.register(mass_player)
+        await self.mass.players.register(mass_player)
 
         # once the player is registered, you can either instruct the player manager to
         # poll the player for state changes or you can implement your own logic to
index d2fbd05b1ff4889a13c0f915fbaf6e8af09250da..2ca3c8cce7ce53c1fcf5983bc8ff06a90ef98c76 100644 (file)
@@ -900,7 +900,7 @@ class AirplayProvider(PlayerProvider):
             ),
             volume_level=volume,
         )
-        self.mass.players.register_or_update(mass_player)
+        await self.mass.players.register_or_update(mass_player)
 
     async def _handle_dacp_request(  # noqa: PLR0915
         self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
index 828faa4f66b89fd9a3e692ce13fbf21551cbd554..3a7b39010110f3a18fba0a11a1d03053234237c9 100644 (file)
@@ -302,7 +302,7 @@ class BluesoundPlayerProvider(PlayerProvider):
             needs_poll=True,
             poll_interval=30,
         )
-        self.mass.players.register(mass_player)
+        await self.mass.players.register(mass_player)
 
         # TODO sync
         await bluos_player.update_attributes()
index f7fa1da02ce44a80817c03c45f7ef249dd9a7c53..032669ab24e2600621126b6f987db2eeb6c88593 100644 (file)
@@ -402,8 +402,8 @@ class ChromecastProvider(PlayerProvider):
                 castplayer.mz_controller = mz_controller
 
             castplayer.cc.start()
-            self.mass.loop.call_soon_threadsafe(
-                self.mass.players.register_or_update, castplayer.player
+            asyncio.run_coroutine_threadsafe(
+                self.mass.players.register_or_update(castplayer.player), loop=self.mass.loop
             )
 
     def _on_chromecast_removed(self, uuid, service, cast_info) -> None:
index 8a03e35a043d311b530bdd5f04705a01cf8a27eb..234f064b556c2cdc4afcd91568e14784c3209719 100644 (file)
@@ -532,7 +532,7 @@ class DLNAPlayerProvider(PlayerProvider):
 
             self._set_player_features(dlna_player)
             dlna_player.update_attributes()
-            self.mass.players.register_or_update(dlna_player.player)
+            await self.mass.players.register_or_update(dlna_player.player)
 
     async def _device_connect(self, dlna_player: DLNAPlayer) -> None:
         """Connect DLNA/DMR Device."""
index 8f8ce734952ecc434e2f68f0c9fe986000b71c66..1977ccd2d050ecdbe1ee25ad7d4722daf68a31ea 100644 (file)
@@ -138,7 +138,7 @@ class FullyKioskProvider(PlayerProvider):
                 needs_poll=True,
                 poll_interval=10,
             )
-        self.mass.players.register_or_update(player)
+        await self.mass.players.register_or_update(player)
         self._handle_player_update()
 
     def _handle_player_update(self) -> None:
index b9075512b1fd8f2962caa9130657ed9eeb6fab5b..f70d7f0bb4e42d87ab1c48007849d6aef0f11659 100644 (file)
@@ -403,7 +403,7 @@ class HomeAssistantPlayers(PlayerProvider):
             state=StateMap.get(state["state"], PlayerState.IDLE),
         )
         self._update_player_attributes(player, state["attributes"])
-        self.mass.players.register_or_update(player)
+        await self.mass.players.register_or_update(player)
 
     def _on_entity_state_update(self, event: EntityStateEvent) -> None:
         """Handle Entity State event."""
index 8360af365c75ba5279426bf93f46fa16c3878e9a..2332e2bfc3fb77c2c5fbb3760ca9bc6bc68209d0 100644 (file)
@@ -395,6 +395,11 @@ class PlayerGroupProvider(PlayerProvider):
         # optimistically set the group state
         group_player.powered = powered
         self.mass.players.update(group_player.player_id)
+        if not powered:
+            # reset the group members when powered off
+            group_player.group_childs = self.mass.config.get_raw_player_config_value(
+                player_id, CONF_GROUP_MEMBERS
+            )
 
     async def cmd_volume_set(self, player_id: str, volume_level: int) -> None:
         """Send VOLUME_SET command to given player."""
@@ -530,7 +535,7 @@ class PlayerGroupProvider(PlayerProvider):
             enabled=True,
             values={CONF_GROUP_MEMBERS: members, CONF_GROUP_TYPE: group_type},
         )
-        return self._register_group_player(
+        return await self._register_group_player(
             group_player_id=new_group_id, group_type=group_type, name=name, members=members
         )
 
@@ -620,14 +625,14 @@ class PlayerGroupProvider(PlayerProvider):
             members = player_config.get_value(CONF_GROUP_MEMBERS)
             group_type = player_config.get_value(CONF_GROUP_TYPE)
             with suppress(PlayerUnavailableError):
-                self._register_group_player(
+                await self._register_group_player(
                     player_config.player_id,
                     group_type,
                     player_config.name or player_config.default_name,
                     members,
                 )
 
-    def _register_group_player(
+    async def _register_group_player(
         self, group_player_id: str, group_type: str, name: str, members: Iterable[str]
     ) -> Player:
         """Register a syncgroup player."""
@@ -679,7 +684,7 @@ class PlayerGroupProvider(PlayerProvider):
             active_source=group_player_id,
         )
 
-        self.mass.players.register_or_update(player)
+        await self.mass.players.register_or_update(player)
         self._update_attributes(player)
         return player
 
@@ -688,20 +693,21 @@ class PlayerGroupProvider(PlayerProvider):
         if group_player.synced_to:
             # should not happen but just in case...
             return self.mass.players.get(group_player.synced_to)
+        if len(group_player.group_childs) == 1:
+            # Return the (first/only) player
+            # this is to handle the edge case where players are not
+            # yet synced or there simply is just one player
+            for child_player in self.mass.players.iter_group_members(
+                group_player, only_powered=False, only_playing=False, active_only=False
+            ):
+                if not child_player.synced_to:
+                    return child_player
         # Return the (first/only) player that has group childs
         for child_player in self.mass.players.iter_group_members(
             group_player, only_powered=False, only_playing=False, active_only=False
         ):
             if child_player.group_childs:
                 return child_player
-        # Return the (first/only) player
-        # this is to handle the edge case where players are not
-        # yet synced or there simply is just one player
-        for child_player in self.mass.players.iter_group_members(
-            group_player, only_powered=False, only_playing=False, active_only=False
-        ):
-            if not child_player.synced_to:
-                return child_player
         return None
 
     def _select_sync_leader(self, group_player: Player) -> Player | None:
index a0f1bc92d401c1b741037566b2eadd760792f004..51b1da42c47b42d05e0af7595126ee6640cc47cd 100644 (file)
@@ -648,7 +648,7 @@ class SlimprotoProvider(PlayerProvider):
                     PlayerFeature.VOLUME_MUTE,
                 ),
             )
-            self.mass.players.register_or_update(player)
+            await self.mass.players.register_or_update(player)
 
         # update player state on player events
         player.available = True
index 80af82be179067cacf309f930d578c03411e244f..c2916893c6124f4a1d4dd0eb78323529278d2781 100644 (file)
@@ -383,12 +383,16 @@ class SnapCastProvider(PlayerProvider):
                 group_childs=set(),
                 synced_to=self._synced_to(player_id),
             )
-        self.mass.players.register_or_update(player)
+        asyncio.run_coroutine_threadsafe(
+            self.mass.players.register_or_update(player), loop=self.mass.loop
+        )
 
     def _handle_player_update(self, snap_client: Snapclient) -> None:
         """Process Snapcast update to Player controller."""
         player_id = self._get_ma_id(snap_client.identifier)
         player = self.mass.players.get(player_id)
+        if not player:
+            return
         player.name = snap_client.friendly_name
         player.volume_level = snap_client.volume
         player.volume_muted = snap_client.muted
@@ -430,7 +434,7 @@ class SnapCastProvider(PlayerProvider):
                 stream_task.cancel()
         player.state = PlayerState.IDLE
         self._set_childs_state(player_id)
-        self.mass.players.register_or_update(player)
+        self.mass.players.update(player_id)
         # assign default/empty stream to the player
         await self._get_snapgroup(player_id).set_stream("default")
 
index 8893b88b07eec95a3581722b4ed158342fdd8db4..f1b62be140bc8fdb7773fab540591f93fcd6ecff 100644 (file)
@@ -181,7 +181,7 @@ class SonosPlayer:
             supported_features=tuple(supported_features),
         )
         self.update_attributes()
-        self.mass.players.register_or_update(mass_player)
+        await self.mass.players.register_or_update(mass_player)
 
         # register callback for state changed
         self.client.subscribe(
@@ -649,7 +649,7 @@ class SonosPlayerProvider(PlayerProvider):
                 self.mass.call_later(5, self.cmd_sync_many(player_id, group_childs))
             return
 
-        if media.queue_id.startswith("ugp_"):
+        if media.queue_id and media.queue_id.startswith("ugp_"):
             # Special UGP stream - handle with play URL
             await sonos_player.client.player.group.play_stream_url(media.uri, None)
             return
index 3b63b062e67a19646905bd484456f3b033192bd7..d6e9cbf8ca16363f019f34c09e8ce4bd0e30ae0f 100644 (file)
@@ -37,12 +37,7 @@ from music_assistant.common.models.enums import (
 )
 from music_assistant.common.models.errors import PlayerCommandFailed, PlayerUnavailableError
 from music_assistant.common.models.player import DeviceInfo, Player, PlayerMedia
-from music_assistant.constants import (
-    CONF_CROSSFADE,
-    CONF_ENFORCE_MP3,
-    CONF_FLOW_MODE,
-    VERBOSE_LOG_LEVEL,
-)
+from music_assistant.constants import CONF_CROSSFADE, CONF_ENFORCE_MP3, VERBOSE_LOG_LEVEL
 from music_assistant.server.helpers.didl_lite import create_didl_metadata
 from music_assistant.server.models.player_provider import PlayerProvider
 
@@ -447,19 +442,8 @@ class SonosPlayerProvider(PlayerProvider):
                 *mass_player.supported_features,
                 PlayerFeature.VOLUME_SET,
             )
-
-        # bugfix: correct flow-mode setting as sonos doesn't support it
-        # but we did accidentally expose the setting for a couple of releases
-        # remove this after MA release 2.5+
-        self.mass.loop.call_soon_threadsafe(
-            self.mass.config.set_raw_player_config_value,
-            player_id,
-            CONF_FLOW_MODE,
-            False,
-        )
-
-        self.mass.loop.call_soon_threadsafe(
-            self.mass.players.register_or_update, sonos_player.mass_player
+        asyncio.run_coroutine_threadsafe(
+            self.mass.players.register_or_update(sonos_player.mass_player), loop=self.mass.loop
         )