From 3f049b396e354529bcc5f9f6ba82f2637a7d73c2 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 27 Jan 2024 16:13:02 +0100 Subject: [PATCH] Add default enqueue options (#1033) --- music_assistant/client/players.py | 2 +- .../server/controllers/player_queues.py | 235 ++++++++++++++++-- 2 files changed, 222 insertions(+), 15 deletions(-) diff --git a/music_assistant/client/players.py b/music_assistant/client/players.py index b8ccda6b..1cf7cb0a 100644 --- a/music_assistant/client/players.py +++ b/music_assistant/client/players.py @@ -264,7 +264,7 @@ class Players: self, queue_id: str, media: MediaItemType | list[MediaItemType] | str | list[str], - option: QueueOption = QueueOption.PLAY, + option: QueueOption | None = None, radio_mode: bool = False, ) -> None: """ diff --git a/music_assistant/server/controllers/player_queues.py b/music_assistant/server/controllers/player_queues.py index b234872d..f29f5c45 100755 --- a/music_assistant/server/controllers/player_queues.py +++ b/music_assistant/server/controllers/player_queues.py @@ -10,7 +10,13 @@ from contextlib import suppress from typing import TYPE_CHECKING, Any from music_assistant.common.helpers.util import get_changed_keys +from music_assistant.common.models.config_entries import ( + ConfigEntry, + ConfigValueOption, + ConfigValueType, +) from music_assistant.common.models.enums import ( + ConfigEntryType, EventType, MediaType, PlayerFeature, @@ -35,10 +41,22 @@ from music_assistant.server.models.core_controller import CoreController if TYPE_CHECKING: from collections.abc import Iterator + from music_assistant.common.models.media_items import Album, Artist, Track from music_assistant.common.models.player import Player LOGGER = logging.getLogger(f"{ROOT_LOGGER_NAME}.players.queue") +CONF_DEFAULT_ENQUEUE_SELECT_ARTIST = "default_enqueue_select_artist" +CONF_DEFAULT_ENQUEUE_SELECT_ALBUM = "default_enqueue_select_album" + +ENQUEUE_SELECT_ARTIST_DEFAULT_VALUE = "all_tracks" +ENQUEUE_SELECT_ALBUM_DEFAULT_VALUE = "all_tracks" + +CONF_DEFAULT_ENQUEUE_OPTION_ARTIST = "default_enqueue_action_artist" +CONF_DEFAULT_ENQUEUE_OPTION_ALBUM = "default_enqueue_action_album" +CONF_DEFAULT_ENQUEUE_OPTION_TRACK = "default_enqueue_action_track" +CONF_DEFAULT_ENQUEUE_OPTION_RADIO = "default_enqueue_action_radio" +CONF_DEFAULT_ENQUEUE_OPTION_PLAYLIST = "default_enqueue_action_playlist" class PlayerQueuesController(CoreController): @@ -66,6 +84,96 @@ class PlayerQueuesController(CoreController): continue await self.stop(queue.queue_id) + async def get_config_entries( + self, + action: str | None = None, # noqa: ARG002 + values: dict[str, ConfigValueType] | None = None, # noqa: ARG002 + ) -> tuple[ConfigEntry, ...]: + """Return all Config Entries for this core module (if any).""" + enqueue_options = tuple(ConfigValueOption(x.name, x.value) for x in QueueOption) + return ( + ConfigEntry( + key=CONF_DEFAULT_ENQUEUE_SELECT_ARTIST, + type=ConfigEntryType.STRING, + default_value=ENQUEUE_SELECT_ARTIST_DEFAULT_VALUE, + label="Items to select when you play a (in-library) artist.", + options=( + ConfigValueOption( + title="Only in-library tracks", + value="library_tracks", + ), + ConfigValueOption( + title="All tracks from all albums in the library", + value="library_album_tracks", + ), + ConfigValueOption( + title="All (top) tracks from (all) streaming provider(s)", + value="all_tracks", + ), + ConfigValueOption( + title="All tracks from all albums from (all) streaming provider(s)", + value="all_album_tracks", + ), + ), + ), + ConfigEntry( + key=CONF_DEFAULT_ENQUEUE_SELECT_ALBUM, + type=ConfigEntryType.STRING, + default_value=ENQUEUE_SELECT_ALBUM_DEFAULT_VALUE, + label="Items to select when you play a (in-library) album.", + options=( + ConfigValueOption( + title="Only in-library tracks", + value="library_tracks", + ), + ConfigValueOption( + title="All tracks for album on (streaming) provider", + value="all_tracks", + ), + ), + ), + ConfigEntry( + key=CONF_DEFAULT_ENQUEUE_OPTION_ARTIST, + type=ConfigEntryType.STRING, + default_value=QueueOption.REPLACE, + label="Default enqueue option for Artist item(s).", + options=enqueue_options, + description="Define the default enqueue action for this mediatype.", + ), + ConfigEntry( + key=CONF_DEFAULT_ENQUEUE_OPTION_ALBUM, + type=ConfigEntryType.STRING, + default_value=QueueOption.REPLACE, + label="Default enqueue option for Album item(s).", + options=enqueue_options, + description="Define the default enqueue action for this mediatype.", + ), + ConfigEntry( + key=CONF_DEFAULT_ENQUEUE_OPTION_TRACK, + type=ConfigEntryType.STRING, + default_value=QueueOption.PLAY, + label="Default enqueue option for Track item(s).", + options=enqueue_options, + description="Define the default enqueue action for this mediatype.", + ), + ConfigEntry( + key=CONF_DEFAULT_ENQUEUE_OPTION_RADIO, + type=ConfigEntryType.STRING, + default_value=QueueOption.REPLACE, + label="Default enqueue option for Track item(s).", + options=enqueue_options, + description="Define the default enqueue action for this mediatype.", + ), + ConfigEntry( + key=CONF_DEFAULT_ENQUEUE_OPTION_PLAYLIST, + type=ConfigEntryType.STRING, + default_value=QueueOption.REPLACE, + label="Default enqueue option for Playlist item(s).", + options=enqueue_options, + description="Define the default enqueue action for this mediatype.", + ), + ) + def __iter__(self) -> Iterator[PlayerQueue]: """Iterate over (available) players.""" return iter(self._queues.values()) @@ -146,7 +254,7 @@ class PlayerQueuesController(CoreController): self, queue_id: str, media: MediaItemType | list[MediaItemType] | str | list[str], - option: QueueOption = QueueOption.PLAY, + option: QueueOption | None = None, radio_mode: bool = False, start_item: str | None = None, ) -> None: @@ -163,9 +271,6 @@ class PlayerQueuesController(CoreController): LOGGER.warning("Ignore queue command: An announcement is in progress") return - if option in QueueOption.REPLACE: - self.clear(queue_id) - # a single item or list of items may be provided if not isinstance(media, list): media = [media] @@ -175,11 +280,8 @@ class PlayerQueuesController(CoreController): queue.current_index = None self._queue_items[queue_id] = [] - # clear radio source items if needed - if option not in (QueueOption.ADD, QueueOption.PLAY, QueueOption.NEXT): - queue.radio_source = [] - tracks: list[MediaItemType] = [] + radio_source: list[MediaItemType] = [] for item in media: # parse provided uri into a MA MediaItem or Basic QueueItem from URL if isinstance(item, str): @@ -193,21 +295,31 @@ class PlayerQueuesController(CoreController): else: media_item = item + # handle default enqueue option if needed + if option is None: + option = QueueOption( + await self.mass.config.get_core_config_value( + self.domain, f"default_enqueue_action_{media_item.media_type.value}" + ) + ) + # collect tracks to play ctrl = self.mass.music.get_controller(media_item.media_type) if radio_mode: - queue.radio_source.append(media_item) + radio_source.append(media_item) elif media_item.media_type == MediaType.PLAYLIST: async for playlist_track in ctrl.tracks(media_item.item_id, media_item.provider): tracks.append(playlist_track) await self.mass.music.mark_item_played( media_item.media_type, media_item.item_id, media_item.provider ) - elif media_item.media_type in ( - MediaType.ARTIST, - MediaType.ALBUM, - ): - tracks += await ctrl.tracks(media_item.item_id, media_item.provider) + elif media_item.media_type == MediaType.ARTIST: + tracks += await self._get_artist_tracks(media_item) + await self.mass.music.mark_item_played( + media_item.media_type, media_item.item_id, media_item.provider + ) + elif media_item.media_type == MediaType.ALBUM: + tracks += await self._get_album_tracks(media_item) await self.mass.music.mark_item_played( media_item.media_type, media_item.item_id, media_item.provider ) @@ -226,6 +338,11 @@ class PlayerQueuesController(CoreController): prev_items.append(track) tracks = next_items + prev_items + # overwrite or append radio source items + if option not in (QueueOption.ADD, QueueOption.PLAY, QueueOption.NEXT): + queue.radio_source = radio_mode + else: + queue.radio_source += radio_mode # Use collected media items to calculate the radio if radio mode is on if radio_mode: tracks = await self._get_radio_tracks(queue_id) @@ -242,6 +359,7 @@ class PlayerQueuesController(CoreController): # handle replace: clear all items and replace with the new items if option == QueueOption.REPLACE: + self.clear(queue_id) self.load( queue_id, queue_items=queue_items, @@ -895,6 +1013,95 @@ class PlayerQueuesController(CoreController): break return tracks + async def _get_artist_tracks(self, artist: Artist) -> list[Track]: + """Return tracks for given artist, based on user preference.""" + artist_items_conf = self.mass.config.get_raw_core_config_value( + self.domain, CONF_DEFAULT_ENQUEUE_SELECT_ARTIST, ENQUEUE_SELECT_ARTIST_DEFAULT_VALUE + ) + if artist_items_conf == "library_tracks": + # make sure we have an in-library artist + artist = await self.mass.music.artists.get( + artist.item_id, artist.provider, lazy=False, details=artist + ) + return await self.mass.music.artists.get_library_artist_tracks(artist.item_id) + if artist_items_conf == "library_album_tracks": + # make sure we have an in-library artist + artist = await self.mass.music.artists.get( + artist.item_id, artist.provider, lazy=False, details=artist + ) + all_items: list[Track] = [] + for library_album in await self.mass.music.artists.get_library_artist_albums( + artist.item_id + ): + for album_track in self.mass.music.albums.tracks( + library_album.item_id, library_album.provider + ): + if album_track not in all_items: + all_items.append(album_track) + random.shuffle(all_items) + return all_items + if artist_items_conf == "all_tracks": + artist = await self.mass.music.artists.get( + artist.item_id, artist.provider, details=artist + ) + all_items: list[Track] = [] + unique_tracks = set() + for provider in artist.provider_mappings: + for artist_track in await self.mass.music.artists.tracks( + provider.item_id, provider.provider_instance + ): + if artist_track in all_items: + continue + unique_key = f"{artist_track.name.lower()}.{artist_track.version.lower()}" + if unique_key in unique_tracks: + continue + all_items.append(artist_track) + unique_tracks.add(unique_key) + random.shuffle(all_items) + return all_items + if artist_items_conf == "all_album_tracks": + artist = await self.mass.music.artists.get( + artist.item_id, artist.provider, details=artist + ) + all_items: list[Track] = [] + unique_tracks = set() + for provider in artist.provider_mappings: + for album in await self.mass.music.artists.albums( + provider.item_id, provider.provider_instance + ): + for album_track in await self.mass.music.albums.tracks( + album.item_id, album.provider + ): + if album_track in all_items: + continue + unique_key = f"{album_track.name.lower()}.{album_track.version.lower()}.{album_track.duration}" # noqa: E501 + if unique_key in unique_tracks: + continue + all_items.append(album_track) + unique_tracks.add(unique_key) + random.shuffle(all_items) + return all_items + return [] + + async def _get_album_tracks(self, album: Album) -> list[Track]: + """Return tracks for given album, based on user preference.""" + album_items_conf = self.mass.config.get_raw_core_config_value( + self.domain, CONF_DEFAULT_ENQUEUE_SELECT_ALBUM, ENQUEUE_SELECT_ALBUM_DEFAULT_VALUE + ) + if album_items_conf == "library_tracks": + # make sure we have an in-library album + album = await self.mass.music.albums.get( + album.item_id, album.provider, lazy=False, details=album + ) + return await self.mass.music.albums.tracks(album.item_id, album.provider) + if album_items_conf == "all_tracks": + for provider in album.provider_mappings: + if album_tracks := await self.mass.music.albums.tracks( + provider.item_id, provider.provider_instance + ): + return album_tracks + return [] + def __get_queue_stream_index(self, queue: PlayerQueue, player: Player) -> tuple[int, int]: """Calculate current queue index and current track elapsed time.""" # player is playing a constant stream so we need to do this the hard way -- 2.34.1