Fix repeat when flow mode enabled (#1215)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 9 Apr 2024 09:05:43 +0000 (11:05 +0200)
committerGitHub <noreply@github.com>
Tue, 9 Apr 2024 09:05:43 +0000 (11:05 +0200)
music_assistant/common/models/player_queue.py
music_assistant/server/controllers/player_queues.py
music_assistant/server/controllers/streams.py

index de1a193023bf51b4abd90cad3db00a22ba5a7457..28ceb298b06d96a4626ffba8c593559e08ef2b3e 100644 (file)
@@ -39,6 +39,7 @@ class PlayerQueue(DataClassDictMixin):
     flow_mode: bool = False
     # flow_mode_start_index: index of the first item of the flow stream
     flow_mode_start_index: int = 0
+    stream_finished: bool = False
 
     @property
     def corrected_elapsed_time(self) -> float:
index fc607e7a3b0ed51575e2872af248121f8ae2ea88..b662c5600a76579fee7d8234e0a8ed762d885399 100644 (file)
@@ -512,6 +512,8 @@ class PlayerQueuesController(CoreController):
         if (player := self.mass.players.get(queue_id)) and player.announcement_in_progress:
             self.logger.warning("Ignore queue command: An announcement is in progress")
             return
+        if queue := self.get(queue_id):
+            queue.stream_finished = False
         # simply forward the command to underlying player
         await self.mass.players.cmd_stop(queue_id)
 
@@ -569,10 +571,8 @@ class PlayerQueuesController(CoreController):
             self.logger.warning("Ignore queue command: An announcement is in progress")
             return
         current_index = self._queues[queue_id].current_index
-        next_index = self._get_next_index(queue_id, current_index, True)
-        if next_index is None:
-            return
-        await self.play_index(queue_id, next_index)
+        if (next_index := self._get_next_index(queue_id, current_index, True)) is not None:
+            await self.play_index(queue_id, next_index)
 
     @api_command("players/queue/previous")
     async def previous(self, queue_id: str) -> None:
@@ -675,9 +675,11 @@ class PlayerQueuesController(CoreController):
         queue.current_index = index
         queue.index_in_buffer = index
         queue.flow_mode_start_index = index
-        queue.flow_mode = self.mass.config.get_raw_player_config_value(
+        player_needs_flow_mode = self.mass.config.get_raw_player_config_value(
             queue_id, CONF_FLOW_MODE, False
         )
+        next_index = self._get_next_index(queue_id, index, allow_repeat=False)
+        queue.flow_mode = player_needs_flow_mode and next_index is not None
         # get streamdetails - do this here to catch unavailable items early
         queue_item.streamdetails = await get_stream_details(
             self.mass, queue_item, seek_position=seek_position, fade_in=fade_in
@@ -792,7 +794,7 @@ class PlayerQueuesController(CoreController):
         if len(changed_keys) == 0:
             return
         # handle enqueuing of next item to play
-        if not queue.flow_mode:
+        if not queue.flow_mode or queue.stream_finished:
             self._check_enqueue_next(player, queue, prev_state, new_state)
         # do not send full updates if only time was updated
         if changed_keys == {"elapsed_time"}:
@@ -835,7 +837,10 @@ class PlayerQueuesController(CoreController):
         self._queue_items.pop(player_id, None)
 
     async def preload_next_item(
-        self, queue_id: str, current_item_id_or_index: str | int | None = None
+        self,
+        queue_id: str,
+        current_item_id_or_index: str | int | None = None,
+        allow_repeat: bool = True,
     ) -> QueueItem:
         """Call when a player wants to (pre)load the next item into the buffer.
 
@@ -853,10 +858,9 @@ class PlayerQueuesController(CoreController):
             cur_index = current_item_id_or_index
         idx = 0
         while True:
-            next_index = self._get_next_index(queue_id, cur_index + idx)
+            next_index = self._get_next_index(queue_id, cur_index + idx, allow_repeat=allow_repeat)
             if next_index is None:
-                msg = "No more tracks left in the queue."
-                raise QueueEmpty(msg)
+                raise QueueEmpty("No more tracks left in the queue.")
             next_item = self.get_item(queue_id, next_index)
             try:
                 # Check if the QueueItem is playable. For example, YT Music returns Radio Items
@@ -873,8 +877,7 @@ class PlayerQueuesController(CoreController):
                 next_item = None
                 idx += 1
         if next_item is None:
-            msg = "No more (playable) tracks left in the queue."
-            raise QueueEmpty(msg)
+            raise QueueEmpty("No more (playable) tracks left in the queue.")
         return next_item
 
     # Main queue manipulation methods
@@ -983,7 +986,7 @@ class PlayerQueuesController(CoreController):
         return media
 
     def _get_next_index(
-        self, queue_id: str, cur_index: int | None, is_skip: bool = False
+        self, queue_id: str, cur_index: int | None, is_skip: bool = False, allow_repeat: bool = True
     ) -> int | None:
         """
         Return the next index for the queue, accounting for repeat settings.
@@ -997,11 +1000,14 @@ class PlayerQueuesController(CoreController):
             return None
         # handle repeat single track
         if queue.repeat_mode == RepeatMode.ONE and not is_skip:
-            return cur_index
+            return cur_index if allow_repeat else None
         # handle cur_index is last index of the queue
         if cur_index >= (len(queue_items) - 1):
-            # if repeat all is enabled, we simply start again from the beginning
-            return 0 if queue.repeat_mode == RepeatMode.ALL else None
+            if allow_repeat and queue.repeat_mode == RepeatMode.ALL:
+                # if repeat all is enabled, we simply start again from the beginning
+                return 0
+            return None
+        # all other: just the next index
         return cur_index + 1
 
     def _get_next_item(self, queue_id: str, cur_index: int | None = None) -> QueueItem | None:
@@ -1048,14 +1054,14 @@ class PlayerQueuesController(CoreController):
             duration = current_item.duration
         seconds_remaining = int(duration - player.corrected_elapsed_time)
 
-        async def _enqueue_next(index: int, supports_enqueue: bool = False) -> None:
+        async def _enqueue_next(current_index: int, supports_enqueue: bool = False) -> None:
             if (
                 player := self.mass.players.get(queue.queue_id)
             ) and player.announcement_in_progress:
                 self.logger.warning("Ignore queue command: An announcement is in progress")
                 return
             with suppress(QueueEmpty):
-                next_item = await self.preload_next_item(queue.queue_id, index)
+                next_item = await self.preload_next_item(queue.queue_id, current_index)
                 if supports_enqueue:
                     await self.mass.players.enqueue_next_media(
                         player_id=player.player_id,
@@ -1064,8 +1070,18 @@ class PlayerQueuesController(CoreController):
                     return
                 await self.play_index(queue.queue_id, next_item.queue_item_id)
 
+        # handle queue fully played - clear it completely once the player stopped
+        if (
+            queue.stream_finished
+            and queue.state == PlayerState.IDLE
+            and self._get_next_index(queue.queue_id, queue.current_index) is None
+        ):
+            self.logger.debug("End of queue reached for %s", queue.display_name)
+            self.clear(queue.queue_id)
+            return
+
+        # handle native enqueue next support of player
         if PlayerFeature.ENQUEUE_NEXT in player.supported_features:
-            # player supports enqueue next feature.
             # we enqueue the next track after a new track
             # has started playing and (repeat) before the current track ends
             new_track_started = new_state.get("state") == PlayerState.PLAYING and prev_state.get(
@@ -1081,13 +1097,10 @@ class PlayerQueuesController(CoreController):
 
         # player does not support enqueue next feature.
         # we wait for the player to stop after it reaches the end of the track
-        prev_seconds_remaining = prev_state.get("seconds_remaining", seconds_remaining)
-        if prev_seconds_remaining <= 6 and queue.state == PlayerState.IDLE:
+        if queue.stream_finished and queue.state == PlayerState.IDLE:
             self.mass.create_task(_enqueue_next(queue.current_index, False))
             return
 
-        new_state["seconds_remaining"] = seconds_remaining
-
     async def _get_radio_tracks(self, queue_id: str) -> list[MediaItemType]:
         """Call the registered music providers for dynamic tracks."""
         queue = self._queues[queue_id]
index 85eddb9eb8d2c67a90d7ef3012d8c229e95f810b..1a1a3a18e31fed3578dd270a24d596a437134fbb 100644 (file)
@@ -349,6 +349,7 @@ class StreamsController(CoreController):
 
         # all checks passed, start streaming!
         self.logger.debug("Start serving Queue flow audio stream for %s", queue.display_name)
+        queue.stream_finished = False
 
         # collect player specific ffmpeg args to re-encode the source PCM stream
         pcm_format = AudioFormat(
@@ -397,6 +398,7 @@ class StreamsController(CoreController):
             length_b = chr(int(length / 16)).encode()
             await resp.write(length_b + metadata)
 
+        queue.stream_finished = True
         return resp
 
     async def serve_command_request(self, request: web.Request) -> web.Response:
@@ -493,6 +495,7 @@ class StreamsController(CoreController):
         queue_track = None
         last_fadeout_part = b""
         queue.flow_mode = True
+        queue.stream_finished = False
         use_crossfade = self.mass.config.get_raw_player_config_value(
             queue.queue_id, CONF_CROSSFADE, False
         )
@@ -512,7 +515,9 @@ class StreamsController(CoreController):
                 queue_track = start_queue_item
             else:
                 try:
-                    queue_track = await self.mass.player_queues.preload_next_item(queue.queue_id)
+                    queue_track = await self.mass.player_queues.preload_next_item(
+                        queue.queue_id, allow_repeat=False
+                    )
                 except QueueEmpty:
                     break
 
@@ -638,6 +643,7 @@ class StreamsController(CoreController):
             queue_track.streamdetails.duration += last_part_seconds
             del last_fadeout_part
         total_bytes_sent += bytes_written
+        queue.stream_finished = True
         self.logger.info("Finished Queue Flow stream for Queue %s", queue.display_name)
 
     async def get_announcement_stream(
@@ -697,7 +703,6 @@ class StreamsController(CoreController):
             # always require a small amount of buffer to prevent livestreams stuttering
             else pcm_sample_size * 2
         )
-
         # collect all arguments for ffmpeg
         filter_params = []
         if streamdetails.target_loudness is not None: