Several fixes for YTM and Radio (#717)
authorMarvin Schenkel <marvinschenkel@gmail.com>
Thu, 15 Jun 2023 16:41:03 +0000 (18:41 +0200)
committerGitHub <noreply@github.com>
Thu, 15 Jun 2023 16:41:03 +0000 (18:41 +0200)
music_assistant/server/controllers/media/playlists.py
music_assistant/server/controllers/player_queues.py
music_assistant/server/providers/ytmusic/__init__.py

index ae213accfacafa4500b7e9598ffc8d2cd2977fed..165dee6b9fe510c6724e9981c5182d23bd999a4a 100644 (file)
@@ -319,7 +319,7 @@ class PlaylistController(MediaControllerBase[Playlist]):
         ]
         limit = min(limit, len(playlist_tracks))
         # use set to prevent duplicates
-        final_items = set()
+        final_items = []
         # to account for playlists with mixed content we grab suggestions from a few
         # random playlist tracks to prevent getting too many tracks of one of the
         # source playlist's genres.
@@ -327,17 +327,18 @@ class PlaylistController(MediaControllerBase[Playlist]):
             # grab 5 random tracks from the playlist
             base_tracks = random.sample(playlist_tracks, 5)
             # add the source/base playlist tracks to the final list...
-            final_items.update(base_tracks)
+            final_items.extend(base_tracks)
             # get 5 suggestions for one of the base tracks
             base_track = next(x for x in base_tracks if x.available)
             similar_tracks = await provider.get_similar_tracks(
                 prov_track_id=base_track.item_id, limit=5
             )
-            final_items.update(x for x in similar_tracks if x.available)
-
+            final_items.extend(x for x in similar_tracks if x.available)
+        # Remove duplicate tracks
+        radio_items = {track.sort_name: track for track in final_items}.values()
         # NOTE: In theory we can return a few more items than limit here
         # Shuffle the final items list
-        return random.sample(list(final_items), len(final_items))
+        return random.sample(radio_items, len(radio_items))
 
     async def _get_dynamic_tracks(
         self, media_item: Playlist, limit: int = 25  # noqa: ARG002
index 5af41f29f2e8b4d404f9f7723feff1dd9e16e6d0..c624391c97513b4f10064a6c29484d3920cc00d7 100755 (executable)
@@ -22,6 +22,7 @@ from music_assistant.common.models.player_queue import PlayerQueue
 from music_assistant.common.models.queue_item import QueueItem
 from music_assistant.constants import CONF_FLOW_MODE, FALLBACK_DURATION, ROOT_LOGGER_NAME
 from music_assistant.server.helpers.api import api_command
+from music_assistant.server.helpers.audio import get_stream_details
 
 if TYPE_CHECKING:
     from collections.abc import Iterator
@@ -631,10 +632,25 @@ class PlayerQueuesController:
         queue = self.get_active_queue(queue_or_player_id)
         cur_index = self.index_by_id(queue.queue_id, current_item_id)
         cur_item = self.get_item(queue.queue_id, cur_index)
-        next_index = self.get_next_index(queue.queue_id, cur_index)
-        next_item = self.get_item(queue.queue_id, next_index)
-        if not cur_item or not next_item:
-            raise QueueEmpty("No more tracks left in the queue.")
+        idx = 0
+        while True:
+            next_index = self.get_next_index(queue.queue_id, cur_index + idx)
+            next_item = self.get_item(queue.queue_id, next_index)
+            if not cur_item or not next_item:
+                raise QueueEmpty("No more tracks left in the queue.")
+            try:
+                # Check if the QueueItem is playable. For example, YT Music returns Radio Items
+                # that are not playable which will stop playback.
+                next_item.streamdetails = await get_stream_details(
+                    mass=self.mass, queue_item=next_item
+                )
+                # Lazy load the full MediaItem for the QueueItem, making sure to get the
+                # maximum quality of thumbs
+                next_item.media_item = await self.mass.music.get_item_by_uri(next_item.uri)
+                break
+            except MediaNotFoundError:
+                # No stream details found, skip this QueueItem
+                idx += 1
         queue.index_in_buffer = next_index
         # work out crossfade
         crossfade = queue.crossfade_enabled
index d8012355810e42bc14b0df916e2345abf3f1bbb9..161ac7a8c051c0e752f3747d6c4b0bce7ce606ff 100644 (file)
@@ -475,7 +475,7 @@ class YoutubeMusicProvider(MusicProvider):
             return tracks
         return []
 
-    async def get_stream_details(self, item_id: str, retry=True) -> StreamDetails:
+    async def get_stream_details(self, item_id: str, retry=0) -> StreamDetails:
         """Return the content details for the given track when it will be streamed."""
         data = {
             "playbackContext": {
@@ -487,14 +487,18 @@ class YoutubeMusicProvider(MusicProvider):
         stream_format = await self._parse_stream_format(track_obj)
         url = await self._parse_stream_url(stream_format=stream_format, item_id=item_id)
         if not await self._is_valid_deciphered_url(url=url):
-            if not retry:
+            if retry > 4:
+                self.logger.warn(
+                    f"Could not resolve a valid URL for item '{item_id}'. "
+                    "Are you playing music on another device using the same account?"
+                )
                 raise UnplayableMediaError(f"Could not resolve a valid URL for item '{item_id}'.")
             self.logger.debug(
                 "Invalid playback URL encountered. Retrying with new signature timestamp."
             )
-            self._signature_timestamp = await self._get_signature_timestamp()
             self._cipher = None
-            return await self.get_stream_details(item_id=item_id, retry=False)
+            self._signature_timestamp = await self._get_signature_timestamp()
+            return await self.get_stream_details(item_id=item_id, retry=retry + 1)
         stream_details = StreamDetails(
             provider=self.instance_id,
             item_id=item_id,
@@ -512,11 +516,6 @@ class YoutubeMusicProvider(MusicProvider):
             stream_details.channels = int(stream_format.get("audioChannels"))
         if stream_format.get("audioSampleRate") and stream_format.get("audioSampleRate").isdigit():
             stream_details.sample_rate = int(stream_format.get("audioSampleRate"))
-        if not stream_details:
-            self.logger.debug(
-                f"Returning NULL stream details for stream_format {stream_format}, "
-                "track_obj {track_obj}. "
-            )
         return stream_details
 
     async def _post_data(self, endpoint: str, data: dict[str, str], **kwargs):  # noqa: ARG002