From 21eeeae6ec8a1e44d97b0d598a31f15562548fa0 Mon Sep 17 00:00:00 2001 From: Marvin Schenkel Date: Thu, 15 Jun 2023 18:41:03 +0200 Subject: [PATCH] Several fixes for YTM and Radio (#717) --- .../server/controllers/media/playlists.py | 11 +++++---- .../server/controllers/player_queues.py | 24 +++++++++++++++---- .../server/providers/ytmusic/__init__.py | 17 +++++++------ 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/music_assistant/server/controllers/media/playlists.py b/music_assistant/server/controllers/media/playlists.py index ae213acc..165dee6b 100644 --- a/music_assistant/server/controllers/media/playlists.py +++ b/music_assistant/server/controllers/media/playlists.py @@ -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 diff --git a/music_assistant/server/controllers/player_queues.py b/music_assistant/server/controllers/player_queues.py index 5af41f29..c624391c 100755 --- a/music_assistant/server/controllers/player_queues.py +++ b/music_assistant/server/controllers/player_queues.py @@ -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 diff --git a/music_assistant/server/providers/ytmusic/__init__.py b/music_assistant/server/providers/ytmusic/__init__.py index d8012355..161ac7a8 100644 --- a/music_assistant/server/providers/ytmusic/__init__.py +++ b/music_assistant/server/providers/ytmusic/__init__.py @@ -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 -- 2.34.1