From 903c936ae62932591ec50ebf28687602a099ca4d Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 29 Jan 2024 00:20:25 +0100 Subject: [PATCH] Some small bugfixes and optimizations (#1039) * Fix radio mode error in queue controller * fix clear queue * get rid of some blocking IO in sonos * fix syncgroup power state * filter self from sync with list in sonos * change poll logic a bit --- .../server/controllers/player_queues.py | 8 ++-- music_assistant/server/controllers/players.py | 14 +++---- .../server/providers/sonos/__init__.py | 20 ++++----- .../server/providers/sonos/player.py | 41 +++++++++++++++---- 4 files changed, 52 insertions(+), 31 deletions(-) diff --git a/music_assistant/server/controllers/player_queues.py b/music_assistant/server/controllers/player_queues.py index f29f5c453..163df9e00 100755 --- a/music_assistant/server/controllers/player_queues.py +++ b/music_assistant/server/controllers/player_queues.py @@ -302,6 +302,9 @@ async def play_media( self.domain, f"default_enqueue_action_{media_item.media_type.value}" ) ) + # clear queue if needed + if option == QueueOption.REPLACE: + self.clear(queue_id) # collect tracks to play ctrl = self.mass.music.get_controller(media_item.media_type) @@ -340,9 +343,9 @@ async def play_media( # overwrite or append radio source items if option not in (QueueOption.ADD, QueueOption.PLAY, QueueOption.NEXT): - queue.radio_source = radio_mode + queue.radio_source = radio_source else: - queue.radio_source += radio_mode + queue.radio_source += radio_source # Use collected media items to calculate the radio if radio mode is on if radio_mode: tracks = await self._get_radio_tracks(queue_id) @@ -359,7 +362,6 @@ async def play_media( # handle replace: clear all items and replace with the new items if option == QueueOption.REPLACE: - self.clear(queue_id) self.load( queue_id, queue_items=queue_items, diff --git a/music_assistant/server/controllers/players.py b/music_assistant/server/controllers/players.py index 012e20cc3..1f36c346b 100755 --- a/music_assistant/server/controllers/players.py +++ b/music_assistant/server/controllers/players.py @@ -253,7 +253,7 @@ def update( ) # handle syncgroup - get attributes from first player that has this group as source if player.player_id.startswith(SYNCGROUP_PREFIX): - if sync_leader := self.get_sync_leader(player): + if player.powered and (sync_leader := self.get_sync_leader(player)): player.state = sync_leader.state player.current_item_id = sync_leader.current_item_id player.elapsed_time = sync_leader.elapsed_time @@ -835,14 +835,10 @@ async def _poll_players(self) -> None: # - every 30 seconds if the player is powered # - every 10 seconds if the player is playing if ( - (player.available or count == 360) - and ( - (player.powered and count % 30 == 0) - or (player_playing and count % 10 == 0) - or count == 360 - ) - and (player_prov := self.get_player_provider(player_id)) - ): + (player.powered and count % 30 == 0) + or (player_playing and count % 10 == 0) + or count == 360 + ) and (player_prov := self.get_player_provider(player_id)): try: await player_prov.poll_player(player_id) except PlayerUnavailableError: diff --git a/music_assistant/server/providers/sonos/__init__.py b/music_assistant/server/providers/sonos/__init__.py index 814f1f9c5..420b0159d 100644 --- a/music_assistant/server/providers/sonos/__init__.py +++ b/music_assistant/server/providers/sonos/__init__.py @@ -177,8 +177,6 @@ async def get_player_config_entries( ) -> tuple[ConfigEntry, ...]: """Return Config Entries for the given player.""" base_entries = await super().get_player_config_entries(player_id) - if not (sonos_player := self.sonosplayers.get(player_id)): - return base_entries return base_entries + ( CONF_ENTRY_CROSSFADE, ConfigEntry( @@ -188,7 +186,6 @@ async def get_player_config_entries( default_value=0, range=(-10, 10), description="Set the Bass level for the Sonos player", - value=sonos_player.soco.bass, advanced=True, ), ConfigEntry( @@ -198,7 +195,6 @@ async def get_player_config_entries( default_value=0, range=(-10, 10), description="Set the Treble level for the Sonos player", - value=sonos_player.soco.treble, advanced=True, ), ConfigEntry( @@ -207,7 +203,6 @@ async def get_player_config_entries( label="Loudness compensation", default_value=True, description="Enable loudness compensation on the Sonos player", - value=sonos_player.soco.loudness, advanced=True, ), ) @@ -256,7 +251,7 @@ async def cmd_stop(self, player_id: str) -> None: player_id, ) return - await asyncio.to_thread(sonos_player.soco.stop) + await self.mass.create_task(sonos_player.soco.stop) async def cmd_play(self, player_id: str) -> None: """Send PLAY command to given player.""" @@ -267,7 +262,7 @@ async def cmd_play(self, player_id: str) -> None: player_id, ) return - await asyncio.to_thread(sonos_player.soco.play) + await self.mass.create_task(sonos_player.soco.play) async def cmd_pause(self, player_id: str) -> None: """Send PAUSE command to given player.""" @@ -282,7 +277,7 @@ async def cmd_pause(self, player_id: str) -> None: # pause not possible await self.cmd_stop(player_id) return - await asyncio.to_thread(sonos_player.soco.pause) + await self.mass.create_task(sonos_player.soco.pause) async def cmd_volume_set(self, player_id: str, volume_level: int) -> None: """Send VOLUME_SET command to given player.""" @@ -291,7 +286,7 @@ def set_volume_level(player_id: str, volume_level: int) -> None: sonos_player = self.sonosplayers[player_id] sonos_player.soco.volume = volume_level - await asyncio.to_thread(set_volume_level, player_id, volume_level) + await self.mass.create_task(set_volume_level, player_id, volume_level) async def cmd_volume_mute(self, player_id: str, muted: bool) -> None: """Send VOLUME MUTE command to given player.""" @@ -300,7 +295,7 @@ def set_volume_mute(player_id: str, muted: bool) -> None: sonos_player = self.sonosplayers[player_id] sonos_player.soco.mute = muted - await asyncio.to_thread(set_volume_mute, player_id, muted) + await self.mass.create_task(set_volume_mute, player_id, muted) async def cmd_sync(self, player_id: str, target_player: str) -> None: """Handle SYNC command for given player. @@ -356,7 +351,7 @@ async def play_media( "accept play_media command, it is synced to another player." ) metadata = create_didl_metadata(self.mass, url, queue_item) - self.mass.create_task(sonos_player.soco.play_uri, url, meta=metadata) + await self.mass.create_task(sonos_player.soco.play_uri, url, meta=metadata) async def play_stream(self, player_id: str, stream_job: MultiClientStreamJob) -> None: """Handle PLAY STREAM on given player. @@ -403,11 +398,12 @@ async def enqueue_next_queue_item(self, player_id: str, queue_item: QueueItem): ) # set crossfade according to player setting crossfade = await self.mass.config.get_player_config_value(player_id, CONF_CROSSFADE) - if sonos_player.soco.cross_fade != crossfade: + if sonos_player.crossfade != crossfade: def set_crossfade(): with suppress(Exception): sonos_player.soco.cross_fade = crossfade + sonos_player.crossfade = crossfade await asyncio.to_thread(set_crossfade) diff --git a/music_assistant/server/providers/sonos/player.py b/music_assistant/server/providers/sonos/player.py index dba6733c1..1637d1c9c 100644 --- a/music_assistant/server/providers/sonos/player.py +++ b/music_assistant/server/providers/sonos/player.py @@ -125,6 +125,7 @@ def __init__( self.mass_player: Player = mass_player self.available: bool = True # cached attributes + self.crossfade: bool = False self.play_mode: str | None = None self.playback_status: str | None = None self.channel: str | None = None @@ -173,9 +174,27 @@ def missing_subscriptions(self) -> set[str]: def setup(self) -> None: """Run initial setup of the speaker (NOT async friendly).""" - # update volume + self.crossfade = self.soco.cross_fade self.mass_player.volume_level = self.soco.volume self.mass_player.volume_muted = self.soco.mute + self.mass.loop.call_soon_threadsafe( + self.mass.config.set_raw_player_config_value, + self.player_id, + "sonos_loudness", + self.soco.loudness, + ) + self.mass.loop.call_soon_threadsafe( + self.mass.config.set_raw_player_config_value, + self.player_id, + "sonos_bass", + self.soco.bass, + ) + self.mass.loop.call_soon_threadsafe( + self.mass.config.set_raw_player_config_value, + self.player_id, + "sonos_treble", + self.soco.treble, + ) self.update_groups() if not self.sync_coordinator: self.poll_media() @@ -415,7 +434,7 @@ def _handle_avtransport_event(self, event: SonosEvent) -> None: return if crossfade := event.variables.get("current_crossfade_mode"): - self.logger.debug("crossfade changed to %s", crossfade) + self.crossfade = bool(int(crossfade)) # Missing transport_state indicates a transient error if (new_status := event.variables.get("transport_state")) is None: @@ -476,8 +495,11 @@ def _handle_rendering_control_event(self, event: SonosEvent) -> None: if loudness := variables.get("loudness"): # TODO: handle this is a better way - self.mass.config.set_raw_player_config_value( - self.player_id, "sonos_loudness", loudness["Master"] == "1" + self.mass.loop.call_soon_threadsafe( + self.mass.config.set_raw_player_config_value, + self.player_id, + "sonos_loudness", + loudness["Master"] == "1", ) for int_var in ( @@ -486,8 +508,11 @@ def _handle_rendering_control_event(self, event: SonosEvent) -> None: ): if int_var in variables: # TODO: handle this is a better way - self.mass.config.set_raw_player_config_value( - self.player_id, f"sonos_{int_var}", variables[int_var] + self.mass.loop.call_soon_threadsafe( + self.mass.config.set_raw_player_config_value, + self.player_id, + f"sonos_{int_var}", + variables[int_var], ) self.update_player() @@ -676,7 +701,9 @@ def _update_attributes(self): # zone topology (syncing/grouping) details self.mass_player.can_sync_with = tuple( - x.player_id for x in self.sonos_prov.sonosplayers.values() if x.sync_coordinator is None + x.player_id + for x in self.sonos_prov.sonosplayers.values() + if x.sync_coordinator is None and x.player_id != self.player_id ) if self.sync_coordinator: # player is syned to another player