]
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.
# 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
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
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
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": {
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,
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