Skip to content

Commit cf8407c

Browse files
authored
Fix Sonos abrupt track switches when reordering an active queue (#4237)
1 parent 0987dc3 commit cf8407c

3 files changed

Lines changed: 36 additions & 9 deletions

File tree

music_assistant/controllers/player_queues.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,13 +1521,22 @@ async def _enqueue_with_option(
15211521
return
15221522
# handle add: add/append item(s) to the remaining queue items
15231523
if option == QueueOption.ADD:
1524+
# When shuffling, mix the new items into the not-yet-played tail. While playing,
1525+
# keep the item right after the buffered one in place: it has already been enqueued
1526+
# to the player (and prepared for crossfade), so reshuffling it would swap the
1527+
# upcoming track underneath the player and cause an abrupt, non-crossfaded switch.
1528+
if not queue.shuffle_enabled:
1529+
add_at_index = len(self._queue_items[queue_id]) + 1
1530+
elif queue.state in (PlaybackState.PLAYING, PlaybackState.PAUSED):
1531+
add_at_index = insert_at_index + 1
1532+
else:
1533+
add_at_index = insert_at_index
15241534
await self.load(
15251535
queue_id=queue_id,
15261536
queue_items=queue_items,
1527-
insert_at_index=insert_at_index
1528-
if queue.shuffle_enabled
1529-
else len(self._queue_items[queue_id]) + 1,
1530-
shuffle=queue.shuffle_enabled,
1537+
insert_at_index=add_at_index,
1538+
# radio tracks are already ordered in a pattern we want to keep
1539+
shuffle=queue.shuffle_enabled and not radio_mode,
15311540
)
15321541
# handle edgecase, queue is empty and items are only added (not played)
15331542
# mark first item as new index

music_assistant/providers/sonos/player.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class SonosQueue:
7171
"""Simple representation of a Sonos (cloud) Queue."""
7272

7373
items: list[PlayerMedia] = field(default_factory=list)
74-
last_updated: float = time.time()
74+
last_updated: float = field(default_factory=time.time)
7575
includes_beginning: bool = False
7676
includes_end: bool = False
7777

@@ -789,6 +789,7 @@ async def _set_sonos_queue_from_mass_queue(self, queue_id: str) -> None:
789789
self.sonos_queue.items.clear()
790790
self.sonos_queue.includes_beginning = False
791791
self.sonos_queue.includes_end = False
792+
self._bump_queue_version()
792793
return
793794
current_index = queue.current_index or 0
794795
current_index = (
@@ -832,6 +833,9 @@ async def _set_sonos_queue_from_mass_queue(self, queue_id: str) -> None:
832833
self.sonos_queue.items = items
833834
self.sonos_queue.includes_beginning = offset == 0
834835
self.sonos_queue.includes_end = includes_end
836+
# bump only after the new window is assigned (no await in between) so Sonos never sees a
837+
# new version while itemWindow still serves the previous items
838+
self._bump_queue_version()
835839
self.logger.log(
836840
VERBOSE_LOG_LEVEL,
837841
"Set Sonos queue items from MA queue %s on player %s: %s",
@@ -866,3 +870,13 @@ def _extract_mac_from_player_id(self) -> str | None:
866870

867871
# Format as XX:XX:XX:XX:XX:XX
868872
return ":".join(mac_hex[i : i + 2].upper() for i in range(0, 12, 2))
873+
874+
def _bump_queue_version(self) -> None:
875+
"""
876+
Advance the Sonos cloud-queue version to the current time.
877+
878+
The version is exposed as the queueVersion in the cloud-queue endpoints; advancing it on
879+
every window rebuild is what makes Sonos refetch the window instead of replaying a stale
880+
cached one.
881+
"""
882+
self.sonos_queue.last_updated = time.time()

music_assistant/providers/sonos/provider.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,6 @@ async def _handle_sonos_queue_itemwindow(
175175
https://docs.sonos.com/reference/itemwindow
176176
"""
177177
context_version = request.query.get("contextVersion", "1")
178-
queue_version = request.query.get("queueVersion", str(int(player.sonos_queue.last_updated)))
179178
# because Sonos does not show our queue in the app anyways,
180179
# we just return the previous, current and next item in the queue.
181180
# the beginning/end flags must be honest though: signalling end-of-queue
@@ -187,7 +186,10 @@ async def _handle_sonos_queue_itemwindow(
187186
"includesBeginningOfQueue": player.sonos_queue.includes_beginning,
188187
"includesEndOfQueue": player.sonos_queue.includes_end,
189188
"contextVersion": context_version,
190-
"queueVersion": queue_version,
189+
# report the version of the items we actually serve (the current window) instead of
190+
# echoing the player's requested version, otherwise a refreshed window keeps a stale
191+
# version label and Sonos never realises it changed.
192+
"queueVersion": str(player.sonos_queue.last_updated),
191193
"items": [self._parse_sonos_queue_item(x) for x in items],
192194
}
193195
return web.json_response(result)
@@ -201,9 +203,11 @@ async def _handle_sonos_queue_version(
201203
https://docs.sonos.com/reference/version
202204
"""
203205
context_version = request.query.get("contextVersion") or "1"
206+
# keep sub-second resolution: the window can be rebuilt several times within the same
207+
# second and Sonos treats an unchanged queueVersion as "nothing changed" (stale window).
204208
result = {
205209
"contextVersion": context_version,
206-
"queueVersion": str(int(player.sonos_queue.last_updated)),
210+
"queueVersion": str(player.sonos_queue.last_updated),
207211
}
208212
return web.json_response(result)
209213

@@ -217,7 +221,7 @@ async def _handle_sonos_queue_context(
217221
"""
218222
result = {
219223
"contextVersion": "1",
220-
"queueVersion": str(int(player.sonos_queue.last_updated)),
224+
"queueVersion": str(player.sonos_queue.last_updated),
221225
"container": {
222226
"type": "trackList",
223227
"name": "Music Assistant",

0 commit comments

Comments
 (0)