From ae5b468e37ceefd7a7cfc7dbdba30dc6f8a43924 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 19 Oct 2024 19:23:26 +0200 Subject: [PATCH 1/2] Code tweaks --- music_assistant/server/controllers/players.py | 60 +++++++++++-------- .../server/providers/player_group/__init__.py | 17 +++--- 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/music_assistant/server/controllers/players.py b/music_assistant/server/controllers/players.py index e41d2d2d3..18df5ac3c 100644 --- a/music_assistant/server/controllers/players.py +++ b/music_assistant/server/controllers/players.py @@ -182,9 +182,9 @@ async def cmd_stop(self, player_id: str) -> None: 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 @@ async def cmd_play(self, player_id: str) -> None: 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 @@ async def cmd_pause(self, player_id: str) -> None: 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 @@ async def cmd_play_pause(self, player_id: str) -> None: """ 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 @@ async def cmd_seek(self, player_id: str, position: int) -> None: 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 @@ async def cmd_next_track(self, player_id: str) -> None: 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 @@ async def cmd_previous_track(self, player_id: str) -> None: 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 @@ -473,8 +473,6 @@ async def play_announcement( ) -> 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") try: @@ -561,10 +559,10 @@ async def play_media(self, player_id: str, media: PlayerMedia) -> None: 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, ) @@ -799,6 +797,17 @@ def update( # 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 + player.powered = sync_leader.powered or player.powered + # correct power state if needed + if player.state == PlayerState.PLAYING and not player.powered: + player.powered = True # calculate group volume player.group_volume = self._get_group_volume_level(player) if player.type == PlayerType.GROUP: @@ -851,13 +860,12 @@ def update( 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): diff --git a/music_assistant/server/providers/player_group/__init__.py b/music_assistant/server/providers/player_group/__init__.py index 8360af365..bb894362a 100644 --- a/music_assistant/server/providers/player_group/__init__.py +++ b/music_assistant/server/providers/player_group/__init__.py @@ -688,20 +688,21 @@ def _get_sync_leader(self, group_player: Player) -> Player | None: 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: From 86f44c470aaa854afc4c6ce1a4eff06c0e465e61 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sun, 20 Oct 2024 00:31:27 +0200 Subject: [PATCH 2/2] Tweak handling of announcements (especially to playergroups) --- .../server/controllers/player_queues.py | 56 +------- music_assistant/server/controllers/players.py | 125 +++++++++--------- .../_template_player_provider/__init__.py | 2 +- .../server/providers/airplay/__init__.py | 2 +- .../server/providers/bluesound/__init__.py | 2 +- .../server/providers/chromecast/__init__.py | 4 +- .../server/providers/dlna/__init__.py | 2 +- .../server/providers/fully_kiosk/__init__.py | 2 +- .../server/providers/hass_players/__init__.py | 2 +- .../server/providers/player_group/__init__.py | 13 +- .../server/providers/slimproto/__init__.py | 2 +- .../server/providers/snapcast/__init__.py | 8 +- .../server/providers/sonos/__init__.py | 4 +- .../server/providers/sonos_s1/__init__.py | 22 +-- 14 files changed, 96 insertions(+), 150 deletions(-) diff --git a/music_assistant/server/controllers/player_queues.py b/music_assistant/server/controllers/player_queues.py index 58362cd19..9a39d2761 100644 --- a/music_assistant/server/controllers/player_queues.py +++ b/music_assistant/server/controllers/player_queues.py @@ -258,15 +258,9 @@ def get_active_queue(self, player_id: str) -> PlayerQueue: @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 @@ def set_shuffle(self, queue_id: str, shuffle_enabled: bool) -> None: 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 @@ def set_dont_stop_the_music(self, queue_id: str, dont_stop_the_music_enabled: bo @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 @@ def move_item(self, queue_id: str, queue_item_id: str, pos_shift: int = 1) -> No - 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 @@ def move_item(self, queue_id: str, queue_item_id: str, pos_shift: int = 1) -> No @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 @@ def delete_item(self, queue_id: str, item_id_or_index: int | str) -> None: @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 @@ async def stop(self, queue_id: str) -> None: - 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 @@ async def play(self, queue_id: str) -> None: - 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 @@ async def next(self, queue_id: str) -> None: - 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 @@ async def previous(self, queue_id: str) -> None: - 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 @@ async def skip(self, queue_id: str, seconds: int = 10) -> None: - 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 @@ async def seek(self, queue_id: str, position: int = 10) -> None: - 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 @@ async def _fill_radio_tracks(self, queue_id: str) -> None: 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), ) diff --git a/music_assistant/server/controllers/players.py b/music_assistant/server/controllers/players.py index 18df5ac3c..ecac01266 100644 --- a/music_assistant/server/controllers/players.py +++ b/music_assistant/server/controllers/players.py @@ -322,13 +322,14 @@ async def cmd_power(self, player_id: str, powered: bool, skip_update: bool = Fal 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 @@ async def cmd_power(self, player_id: str, powered: bool, skip_update: bool = Fal 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 @@ -475,20 +477,26 @@ async def play_announcement( player = self.get(player_id, True) 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, " @@ -498,33 +506,30 @@ async def play_announcement( 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, @@ -715,7 +720,7 @@ def set(self, player: Player) -> None: 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 @@ -750,6 +755,12 @@ def register(self, player: Player) -> None: 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, @@ -759,7 +770,7 @@ def register(self, player: Player) -> None: # 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 @@ -769,7 +780,7 @@ def register_or_update(self, player: Player) -> None: 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.""" @@ -792,6 +803,7 @@ def update( 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 @@ -804,10 +816,6 @@ def update( player.state = sync_leader.state player.elapsed_time = sync_leader.elapsed_time player.elapsed_time_last_updated = sync_leader.elapsed_time_last_updated - player.powered = sync_leader.powered or player.powered - # correct power state if needed - if player.state == PlayerState.PLAYING and not player.powered: - player.powered = True # calculate group volume player.group_volume = self._get_group_volume_level(player) if player.type == PlayerType.GROUP: @@ -829,8 +837,11 @@ def update( 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, @@ -848,10 +859,6 @@ def update( # 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) @@ -935,18 +942,6 @@ def _get_player_with_redirect(self, player_id: str) -> Player: 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( @@ -1106,11 +1101,19 @@ async def _play_announcement( """ 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, @@ -1179,6 +1182,8 @@ async def _play_announcement( 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: diff --git a/music_assistant/server/providers/_template_player_provider/__init__.py b/music_assistant/server/providers/_template_player_provider/__init__.py index ae811a2aa..cf2b897c0 100644 --- a/music_assistant/server/providers/_template_player_provider/__init__.py +++ b/music_assistant/server/providers/_template_player_provider/__init__.py @@ -218,7 +218,7 @@ async def on_mdns_service_state_change( ), ) # 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 diff --git a/music_assistant/server/providers/airplay/__init__.py b/music_assistant/server/providers/airplay/__init__.py index d2fbd05b1..2ca3c8cce 100644 --- a/music_assistant/server/providers/airplay/__init__.py +++ b/music_assistant/server/providers/airplay/__init__.py @@ -900,7 +900,7 @@ async def _setup_player( ), 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 diff --git a/music_assistant/server/providers/bluesound/__init__.py b/music_assistant/server/providers/bluesound/__init__.py index 828faa4f6..3a7b39010 100644 --- a/music_assistant/server/providers/bluesound/__init__.py +++ b/music_assistant/server/providers/bluesound/__init__.py @@ -302,7 +302,7 @@ async def on_mdns_service_state_change( 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() diff --git a/music_assistant/server/providers/chromecast/__init__.py b/music_assistant/server/providers/chromecast/__init__.py index f7fa1da02..032669ab2 100644 --- a/music_assistant/server/providers/chromecast/__init__.py +++ b/music_assistant/server/providers/chromecast/__init__.py @@ -402,8 +402,8 @@ def _on_chromecast_discovered(self, uuid, _) -> None: 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: diff --git a/music_assistant/server/providers/dlna/__init__.py b/music_assistant/server/providers/dlna/__init__.py index 8a03e35a0..234f064b5 100644 --- a/music_assistant/server/providers/dlna/__init__.py +++ b/music_assistant/server/providers/dlna/__init__.py @@ -532,7 +532,7 @@ async def _device_discovered(self, udn: str, description_url: str) -> None: 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.""" diff --git a/music_assistant/server/providers/fully_kiosk/__init__.py b/music_assistant/server/providers/fully_kiosk/__init__.py index 8f8ce7349..1977ccd2d 100644 --- a/music_assistant/server/providers/fully_kiosk/__init__.py +++ b/music_assistant/server/providers/fully_kiosk/__init__.py @@ -138,7 +138,7 @@ async def loaded_in_mass(self) -> None: 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: diff --git a/music_assistant/server/providers/hass_players/__init__.py b/music_assistant/server/providers/hass_players/__init__.py index b9075512b..f70d7f0bb 100644 --- a/music_assistant/server/providers/hass_players/__init__.py +++ b/music_assistant/server/providers/hass_players/__init__.py @@ -403,7 +403,7 @@ async def _setup_player( 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.""" diff --git a/music_assistant/server/providers/player_group/__init__.py b/music_assistant/server/providers/player_group/__init__.py index bb894362a..2332e2bfc 100644 --- a/music_assistant/server/providers/player_group/__init__.py +++ b/music_assistant/server/providers/player_group/__init__.py @@ -395,6 +395,11 @@ async def cmd_power(self, player_id: str, powered: bool) -> None: # 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 @@ async def create_group(self, group_type: str, name: str, members: list[str]) -> 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 @@ async def _register_all_players(self) -> None: 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 @@ def _register_group_player( 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 diff --git a/music_assistant/server/providers/slimproto/__init__.py b/music_assistant/server/providers/slimproto/__init__.py index a0f1bc92d..51b1da42c 100644 --- a/music_assistant/server/providers/slimproto/__init__.py +++ b/music_assistant/server/providers/slimproto/__init__.py @@ -648,7 +648,7 @@ async def _handle_player_update(self, slimplayer: SlimClient) -> None: 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 diff --git a/music_assistant/server/providers/snapcast/__init__.py b/music_assistant/server/providers/snapcast/__init__.py index 80af82be1..c2916893c 100644 --- a/music_assistant/server/providers/snapcast/__init__.py +++ b/music_assistant/server/providers/snapcast/__init__.py @@ -383,12 +383,16 @@ def _handle_player_init(self, snap_client: Snapclient) -> None: 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 @@ async def cmd_stop(self, player_id: str) -> None: 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") diff --git a/music_assistant/server/providers/sonos/__init__.py b/music_assistant/server/providers/sonos/__init__.py index 8893b88b0..f1b62be14 100644 --- a/music_assistant/server/providers/sonos/__init__.py +++ b/music_assistant/server/providers/sonos/__init__.py @@ -181,7 +181,7 @@ async def setup(self) -> None: 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 @@ async def play_media( 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 diff --git a/music_assistant/server/providers/sonos_s1/__init__.py b/music_assistant/server/providers/sonos_s1/__init__.py index 3b63b062e..d6e9cbf8c 100644 --- a/music_assistant/server/providers/sonos_s1/__init__.py +++ b/music_assistant/server/providers/sonos_s1/__init__.py @@ -37,12 +37,7 @@ ) 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 @@ def _add_player(self, soco: SoCo) -> None: *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 )