Fix: Handle some small race conditions with sonos players
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 23 Oct 2024 21:51:20 +0000 (23:51 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 23 Oct 2024 21:51:20 +0000 (23:51 +0200)
music_assistant/server/controllers/player_queues.py
music_assistant/server/providers/sonos/player.py
music_assistant/server/providers/sonos/provider.py
music_assistant/server/providers/sonos_s1/player.py

index d838d7214d4fd7f66d4a48c57e79563ec4974cf5..07edd95f71cfa6595956f6771d80340813a39f6f 100644 (file)
@@ -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
index 8dc81aef1f8eed50432a4a39f93852d11345742c..5cc359f0489780407ec0f95f8c5eeca0aad46f52 100644 (file)
@@ -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()
index 14facde255fb6f6a2168394a4acfec5b118af36a..690f4ff1cbc241f6534224bd8ecf81cbc356b2f9 100644 (file)
@@ -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,
index 4f479d0f2348f6aeb982d2725733dac345ee8cc2..600e49709fa626f2054900e8e7aa05fa26ff1ab6 100644 (file)
@@ -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)