Don't stop the music improvements (#2597)
authorMarvin Schenkel <marvinschenkel@gmail.com>
Wed, 5 Nov 2025 15:23:33 +0000 (16:23 +0100)
committerGitHub <noreply@github.com>
Wed, 5 Nov 2025 15:23:33 +0000 (16:23 +0100)
music_assistant/controllers/player_queues.py

index 46924f70779355f59fd4b961a7847b2ad7600fca..a92484f382d90c4e4e64794dc8dd27e5c6a864f3 100644 (file)
@@ -1674,6 +1674,7 @@ class PlayerQueuesController(CoreController):
     ) -> list[Track]:
         """Call the registered music providers for dynamic tracks."""
         queue = self._queues[queue_id]
+        queue_track_items = [q.media_item for q in self._queue_items[queue_id] if q.media_item]
         if not queue.radio_source:
             # this may happen during race conditions as this method is called delayed
             return None
@@ -1684,27 +1685,37 @@ class PlayerQueuesController(CoreController):
         )
         available_base_tracks: list[Track] = []
         base_track_sample_size = 5
-        # Grab all the available base tracks based on the selected source items.
-        # shuffle the source items, just in case
-        for radio_item in random.sample(queue.radio_source, len(queue.radio_source)):
-            ctrl = self.mass.music.get_controller(radio_item.media_type)
-            try:
-                available_base_tracks += [
-                    track
-                    for track in await ctrl.radio_mode_base_tracks(
-                        radio_item.item_id, radio_item.provider
+        # Some providers have very deterministic similar track algorithms when providing
+        # a single track item. When we have a radio mode based on 1 track and we have to
+        # refill the queue (ie not initial radio mode), we use the play history as base tracks
+        if (
+            len(queue.radio_source) == 1
+            and queue.radio_source[0].media_type == MediaType.TRACK
+            and not is_initial_radio_mode
+        ):
+            available_base_tracks = queue_track_items
+        else:
+            # Grab all the available base tracks based on the selected source items.
+            # shuffle the source items, just in case
+            for radio_item in random.sample(queue.radio_source, len(queue.radio_source)):
+                ctrl = self.mass.music.get_controller(radio_item.media_type)
+                try:
+                    available_base_tracks += [
+                        track
+                        for track in await ctrl.radio_mode_base_tracks(
+                            radio_item.item_id, radio_item.provider
+                        )
+                        # Avoid duplicate base tracks
+                        if track not in available_base_tracks
+                    ]
+                except UnsupportedFeaturedException as err:
+                    self.logger.debug(
+                        "Skip loading radio items for %s: %s ",
+                        radio_item.uri,
+                        str(err),
                     )
-                    # Avoid duplicate base tracks
-                    if track not in available_base_tracks
-                ]
-            except UnsupportedFeaturedException as err:
-                self.logger.debug(
-                    "Skip loading radio items for %s: %s ",
-                    radio_item.uri,
-                    str(err),
-                )
-        if not available_base_tracks:
-            raise UnsupportedFeaturedException("Radio mode not available for source items")
+            if not available_base_tracks:
+                raise UnsupportedFeaturedException("Radio mode not available for source items")
 
         # Sample tracks from the base tracks, which will be used to calculate the dynamic ones
         base_tracks = random.sample(
@@ -1718,17 +1729,27 @@ class PlayerQueuesController(CoreController):
             if dynamic_tracks:
                 break
             for base_track in base_tracks:
-                [
-                    dynamic_tracks.add(track)
-                    for track in await self.mass.music.tracks.similar_tracks(
+                try:
+                    _similar_tracks = await self.mass.music.tracks.similar_tracks(
                         base_track.item_id,
                         base_track.provider,
                         allow_lookup=allow_lookup,
                     )
-                    if track not in base_tracks
-                    # Ignore tracks that are too long for radio mode, e.g. mixes
-                    and track.duration <= RADIO_TRACK_MAX_DURATION_SECS
-                ]
+                except MediaNotFoundError:
+                    # Some providers don't have similar tracks for all items. For example,
+                    # Tidal can sometimes return a 404 when the 'similar_tracks' endpoint is called.
+                    # in that case, just skip the track.
+                    self.logger.debug("No similar tracks not found for track %s", base_track.name)
+                    continue
+                for track in _similar_tracks:
+                    if (
+                        track not in base_tracks
+                        # Exclude tracks we have already played / queued
+                        and track not in queue_track_items
+                        # Ignore tracks that are too long for radio mode, e.g. mixes
+                        and track.duration <= RADIO_TRACK_MAX_DURATION_SECS
+                    ):
+                        dynamic_tracks.add(track)
                 if len(dynamic_tracks) >= 50:
                     break
         queue_tracks: list[Track] = []