Fix Sonos enqueue issues
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 4 Nov 2025 00:12:57 +0000 (01:12 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 4 Nov 2025 00:12:57 +0000 (01:12 +0100)
music_assistant/providers/sonos/player.py

index 39e9f39a99ab7053d689b01e60e2674997ca0af8..3bdbccae1537e417391d0f998cd9d150c8f62608 100644 (file)
@@ -11,7 +11,6 @@ from __future__ import annotations
 
 import asyncio
 import time
-from collections import deque
 from dataclasses import dataclass, field
 from typing import TYPE_CHECKING
 
@@ -75,47 +74,9 @@ SUPPORTED_FEATURES = {
 class SonosQueue:
     """Simple representation of a Sonos (cloud) Queue."""
 
-    _items: deque[PlayerMedia] = field(default_factory=lambda: deque(maxlen=5))
+    items: list[PlayerMedia] = field(default_factory=list)
     last_updated: float = time.time()
 
-    @property
-    def items(self) -> list[PlayerMedia]:
-        """Return the current sonos queue items."""
-        return list(self._items)
-
-    def set_items(self, new_items: list[PlayerMedia]) -> None:
-        """Set the sonos queue items."""
-        self._items = deque(new_items, maxlen=5)
-        self.last_updated = time.time()
-
-    def enqueue_next(self, current_item_id: str | None, next_item: PlayerMedia) -> None:
-        """Enqueue the next item in the sonos queue."""
-        current_index = next(
-            (i for i, item in enumerate(self._items) if item.queue_item_id == current_item_id),
-            None,
-        )
-        if current_index is None:
-            self._items.append(next_item)
-        else:
-            prev_items = self.items[: current_index + 1]
-            # because the next item could potentially have been overwritten,
-            # we rebuild the deque here
-            self._items = deque([*prev_items, next_item], maxlen=5)
-        self.last_updated = time.time()
-
-    def get_queue_from_item(self, item_id: str) -> list[PlayerMedia]:
-        """Return the sonos queue starting from the given item id."""
-        current_index = next(
-            (i for i, item in enumerate(self._items) if item.queue_item_id == item_id), None
-        )
-        if current_index is None:
-            raise IndexError("Current item id not found in sonos queue.")
-        return self.items[current_index:]
-
-    def is_item_in_queue(self, item_id: str) -> bool:
-        """Check if the given item id is in the sonos queue."""
-        return any(item.queue_item_id == item_id for item in self._items)
-
 
 class SonosPlayer(Player):
     """Holds the details of the (discovered) Sonosplayer."""
@@ -408,8 +369,6 @@ class SonosPlayer(Player):
 
         :param media: Details of the item that needs to be played on the player.
         """
-        self.sonos_queue.set_items([media])
-
         if self.client.player.is_passive:
             # this should be already handled by the player manager, but just in case...
             msg = (
@@ -424,6 +383,8 @@ class SonosPlayer(Player):
             self.logger.debug("Redirecting PLAY_MEDIA command to linked airplay player.")
             await self._play_media_airplay(airplay_player, media)
             return
+        if media.source_id:
+            await self._set_sonos_queue_from_mass_queue(media.source_id)
 
         if (media.source_id and media.queue_item_id) or media.media_type == MediaType.PLUGIN_SOURCE:
             # Regular Queue item playback
@@ -489,14 +450,14 @@ class SonosPlayer(Player):
 
          :param media: Details of the item that needs to be enqueued on the player.
         """
-        current_item_id = self.current_media.queue_item_id if self.current_media else None
-        self.sonos_queue.enqueue_next(current_item_id, media)
-        self.logger.debug(f"Enqueued next media item: {media.title}")
-        self.logger.debug(
-            "Current Sonos queue items: %s", [x.title for x in self.sonos_queue.items[-2:]]
-        )
+        if media.source_id:
+            await self._set_sonos_queue_from_mass_queue(media.source_id)
         if session_id := self.client.player.group.active_session_id:
             await self.client.api.playback_session.refresh_cloud_queue(session_id)
+            # repeat the command after a while because sonos seems to miss it sometimes ?!
+            self.mass.call_later(
+                30, self.client.api.playback_session.refresh_cloud_queue(session_id)
+            )
 
     async def set_members(
         self,
@@ -922,3 +883,25 @@ class SonosPlayer(Player):
                 raise PlayerCommandFailed(
                     f"Failed to send command to Sonos player: {resp.status} {resp.reason}"
                 )
+
+    async def _set_sonos_queue_from_mass_queue(self, queue_id: str) -> None:
+        """Set the SonosQueue items from the given MA PlayerQueue."""
+        items = []
+        queue = self.mass.player_queues.get(queue_id)
+        if not queue:
+            self.sonos_queue.items.clear()
+            return
+        current_index = queue.current_index or 0
+        offset = max(0, current_index - 4)
+        queue_items = self.mass.player_queues.items(queue_id=queue_id, offset=offset, limit=10)
+        for item in queue_items:
+            if not item.available:
+                continue
+            media = await self.mass.player_queues.player_media_from_queue_item(item, False)
+            items.append(media)
+        self.sonos_queue.items = items
+        self.logger.debug(
+            "Set Sonos queue items from MA queue %s: %s",
+            queue_id,
+            [x.title for x in self.sonos_queue.items],
+        )