From d150b8750ffa3c8c63c4396abc21eae1840fdf92 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 23 Oct 2024 23:51:20 +0200 Subject: [PATCH] Fix: Handle some small race conditions with sonos players --- .../server/controllers/player_queues.py | 19 +++++++++++-------- .../server/providers/sonos/player.py | 18 ++++++++++++++---- .../server/providers/sonos/provider.py | 6 ++++-- .../server/providers/sonos_s1/player.py | 2 +- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/music_assistant/server/controllers/player_queues.py b/music_assistant/server/controllers/player_queues.py index d838d721..07edd95f 100644 --- a/music_assistant/server/controllers/player_queues.py +++ b/music_assistant/server/controllers/player_queues.py @@ -1468,15 +1468,18 @@ class PlayerQueuesController(CoreController): ) return queue_tracks - def _check_clear_queue(self, queue: PlayerQueue) -> None: + async def _check_clear_queue(self, queue: PlayerQueue) -> None: """Check if the queue should be cleared after the current item.""" - if queue.state != PlayerState.IDLE: - return - if queue.next_item is not None: - return - if queue.current_index >= len(self._queue_items[queue.queue_id]) - 1: - self.logger.info("End of queue reached, clearing items") - self.clear(queue.queue_id) + for _ in range(5): + await asyncio.sleep(1) + if queue.state != PlayerState.IDLE: + return + if queue.next_item is not None: + return + if not (queue.current_index >= len(self._queue_items[queue.queue_id]) - 1): + return + self.logger.info("End of queue reached, clearing items") + self.clear(queue.queue_id) def _get_flow_queue_stream_index( self, queue: PlayerQueue, player: Player diff --git a/music_assistant/server/providers/sonos/player.py b/music_assistant/server/providers/sonos/player.py index 8dc81aef..5cc359f0 100644 --- a/music_assistant/server/providers/sonos/player.py +++ b/music_assistant/server/providers/sonos/player.py @@ -15,6 +15,7 @@ from collections.abc import Callable from typing import TYPE_CHECKING import shortuuid +from aiohttp.client_exceptions import ClientConnectorError from aiosonos.api.models import ContainerType, MusicService, SonosCapability from aiosonos.api.models import PlayBackState as SonosPlayBackState from aiosonos.client import SonosLocalApiClient @@ -97,7 +98,7 @@ class SonosPlayer: async def setup(self) -> None: """Handle setup of the player.""" # connect the player first so we can fail early - await self._connect() + await self._connect(False) # collect supported features supported_features = set(PLAYER_FEATURES_BASE) @@ -169,7 +170,7 @@ class SonosPlayer: """Reconnect the player.""" # use a task_id to prevent multiple reconnects task_id = f"sonos_reconnect_{self.player_id}" - self.mass.call_later(delay, self._connect, task_id=task_id) + self.mass.call_later(delay, self._connect, delay, task_id=task_id) async def cmd_stop(self) -> None: """Send STOP command to given player.""" @@ -370,12 +371,21 @@ class SonosPlayer: self.mass_player.current_media = current_media - async def _connect(self) -> None: + async def _connect(self, retry_on_fail: int = 0) -> None: """Connect to the Sonos player.""" if self._listen_task and not self._listen_task.done(): self.logger.debug("Already connected to Sonos player: %s", self.player_id) return - await self.client.connect() + try: + await self.client.connect() + except (ConnectionFailed, ClientConnectorError) as err: + self.logger.warning("Failed to connect to Sonos player: %s", err) + self.mass_player.available = False + self.mass.players.update(self.player_id) + if not retry_on_fail: + raise + self.reconnect(min(retry_on_fail + 30), 3600) + return self.connected = True self.logger.debug("Connected to player API") init_ready = asyncio.Event() diff --git a/music_assistant/server/providers/sonos/provider.py b/music_assistant/server/providers/sonos/provider.py index 14facde2..690f4ff1 100644 --- a/music_assistant/server/providers/sonos/provider.py +++ b/music_assistant/server/providers/sonos/provider.py @@ -109,8 +109,10 @@ class SonosPlayerProvider(PlayerProvider): sonos_player.reconnect() self.mass.players.update(player_id) return - # handle new player - await self._setup_player(player_id, name, info) + # handle new player setup in a delayed task because mdns announcements + # can arrive in (duplicated) bursts + task_id = f"setup_sonos_{player_id}" + self.mass.call_later(5, self._setup_player(player_id, name, info), task_id=task_id) async def get_player_config_entries( self, diff --git a/music_assistant/server/providers/sonos_s1/player.py b/music_assistant/server/providers/sonos_s1/player.py index 4f479d0f..600e4970 100644 --- a/music_assistant/server/providers/sonos_s1/player.py +++ b/music_assistant/server/providers/sonos_s1/player.py @@ -560,7 +560,7 @@ class SonosPlayer: self.sync_coordinator = None self.group_members = group_members self.group_members_ids = group_members_ids - self.mass.players.update(self.player_id) + self.mass.loop.call_soon_threadsafe(self.mass.players.update, self.player_id) for joined_uid in group[1:]: joined_speaker: SonosPlayer = self.sonos_prov.sonosplayers.get(joined_uid) -- 2.34.1