From 4041b1cd6e3c09064528cb815c61601f99e14628 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 31 Oct 2023 20:15:38 +0100 Subject: [PATCH] Refactor services --- custom_components/mass/media_player.py | 213 +++++++++++++++----- custom_components/mass/services.py | 190 +++++------------ custom_components/mass/services.yaml | 117 +++++------ custom_components/mass/strings.json | 57 ++++++ custom_components/mass/translations/en.json | 57 ++++++ 5 files changed, 388 insertions(+), 246 deletions(-) diff --git a/custom_components/mass/media_player.py b/custom_components/mass/media_player.py index 30b0872b..9970373d 100644 --- a/custom_components/mass/media_player.py +++ b/custom_components/mass/media_player.py @@ -1,10 +1,12 @@ """MediaPlayer platform for Music Assistant integration.""" from __future__ import annotations -import time from collections.abc import Mapping +from contextlib import suppress from typing import TYPE_CHECKING, Any +import homeassistant.helpers.config_validation as cv +import voluptuous as vol from homeassistant.components import media_source from homeassistant.components.media_player import ( BrowseMedia, @@ -14,27 +16,16 @@ ) from homeassistant.components.media_player.browse_media import async_process_play_media_url from homeassistant.components.media_player.const import ( - SUPPORT_BROWSE_MEDIA, - SUPPORT_CLEAR_PLAYLIST, - SUPPORT_NEXT_TRACK, - SUPPORT_PAUSE, - SUPPORT_PLAY, - SUPPORT_PLAY_MEDIA, - SUPPORT_PREVIOUS_TRACK, - SUPPORT_REPEAT_SET, - SUPPORT_SEEK, - SUPPORT_SHUFFLE_SET, - SUPPORT_STOP, - SUPPORT_TURN_OFF, - SUPPORT_TURN_ON, - SUPPORT_VOLUME_MUTE, - SUPPORT_VOLUME_SET, - SUPPORT_VOLUME_STEP, + ATTR_MEDIA_ANNOUNCE, + ATTR_MEDIA_ENQUEUE, + ATTR_MEDIA_EXTRA, + MediaPlayerEntityFeature, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.entity_platform import AddEntitiesCallback, async_get_current_platform +from music_assistant.common.helpers.datetime import from_utc_timestamp from music_assistant.common.models.enums import ( EventType, MediaType, @@ -42,7 +33,9 @@ QueueOption, RepeatMode, ) +from music_assistant.common.models.errors import MediaNotFoundError from music_assistant.common.models.event import MassEvent +from music_assistant.common.models.media_items import MediaItemType from .const import ( ATTR_ACTIVE_QUEUE, @@ -57,29 +50,30 @@ from .entity import MassBaseEntity from .helpers import get_mass from .media_browser import async_browse_media -from .services import get_item_by_name if TYPE_CHECKING: from music_assistant.client import MusicAssistantClient from music_assistant.common.models.player_queue import PlayerQueue SUPPORTED_FEATURES = ( - SUPPORT_PAUSE - | SUPPORT_VOLUME_SET - | SUPPORT_STOP - | SUPPORT_PREVIOUS_TRACK - | SUPPORT_NEXT_TRACK - | SUPPORT_SHUFFLE_SET - | SUPPORT_REPEAT_SET - | SUPPORT_TURN_ON - | SUPPORT_TURN_OFF - | SUPPORT_PLAY - | SUPPORT_PLAY_MEDIA - | SUPPORT_VOLUME_STEP - | SUPPORT_CLEAR_PLAYLIST - | SUPPORT_BROWSE_MEDIA - | SUPPORT_SEEK - | SUPPORT_VOLUME_MUTE + MediaPlayerEntityFeature.PAUSE + | MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.PREVIOUS_TRACK + | MediaPlayerEntityFeature.NEXT_TRACK + | MediaPlayerEntityFeature.SHUFFLE_SET + | MediaPlayerEntityFeature.REPEAT_SET + | MediaPlayerEntityFeature.TURN_ON + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.PLAY + | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.VOLUME_STEP + | MediaPlayerEntityFeature.CLEAR_PLAYLIST + | MediaPlayerEntityFeature.BROWSE_MEDIA + | MediaPlayerEntityFeature.SEEK + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.MEDIA_ENQUEUE + | MediaPlayerEntityFeature.MEDIA_ANNOUNCE ) STATE_MAPPING = { @@ -97,6 +91,11 @@ MediaPlayerEnqueue.REPLACE: QueueOption.REPLACE, } +SERVICE_PLAY_MEDIA_ADVANCED = "play_media" +ATTR_RADIO_MODE = "radio_mode" +ATTR_MEDIA_ID = "media_id" +ATTR_MEDIA_TYPE = "media_type" + async def async_setup_entry( hass: HomeAssistant, @@ -121,6 +120,20 @@ async def handle_player_added(event: MassEvent) -> None: added_ids.add(player.player_id) async_add_entities([MassPlayer(mass, player.player_id)]) + # add platform service for play_media with advanced options + platform = async_get_current_platform() + platform.async_register_entity_service( + SERVICE_PLAY_MEDIA_ADVANCED, + { + vol.Optional(ATTR_MEDIA_TYPE): vol.Coerce(MediaType), + vol.Required(ATTR_MEDIA_ID): vol.All(cv.ensure_list, [cv.string]), + vol.Exclusive(ATTR_MEDIA_ENQUEUE, "enqueue_announce"): vol.Coerce(QueueOption), + vol.Exclusive(ATTR_MEDIA_ANNOUNCE, "enqueue_announce"): cv.boolean, + vol.Optional(ATTR_RADIO_MODE): vol.Coerce(bool), + }, + "_async_play_media_advanced", + ) + class MassPlayer(MassBaseEntity, MediaPlayerEntity): """Representation of MediaPlayerEntity from Music Assistant Player.""" @@ -200,15 +213,15 @@ async def async_on_update(self) -> None: self._attr_is_volume_muted = player.volume_muted if queue is not None: self._attr_media_position = queue.elapsed_time - self._attr_media_position_updated_at = time.strftime( - "%Y-%m-%dT%H:%M:%S%z", time.gmtime(queue.elapsed_time_last_updated) + self._attr_media_position_updated_at = from_utc_timestamp( + queue.elapsed_time_last_updated ) else: self._attr_media_position = player.elapsed_time - self._attr_media_position_updated_at = time.strftime( - "%Y-%m-%dT%H:%M:%S%z", time.gmtime(player.elapsed_time_last_updated) + self._attr_media_position_updated_at = from_utc_timestamp( + player.elapsed_time_last_updated ) - self._prev_time = self._attr_media_position + self._prev_time = queue.elapsed_time self._update_media_image_url(queue) # update current media item infos media_artist = None @@ -240,13 +253,15 @@ async def async_on_update(self) -> None: self._attr_media_duration = media_duration def _update_media_image_url(self, queue: PlayerQueue) -> None: - """Update image URL forthe active queue item.""" + """Update image URL for the active queue item.""" if queue is None or queue.current_item is None: self._attr_media_image_url = None return if image := queue.current_item.image: self._attr_media_image_remotely_accessible = image.provider == "url" self._attr_media_image_url = self.mass.get_image_url(image) + return + self._attr_media_image_url = None async def async_media_play(self) -> None: """Send play command to device.""" @@ -339,31 +354,67 @@ async def async_clear_playlist(self) -> None: async def async_play_media( self, - media_type: str, # noqa: ARG002 + media_type: str, media_id: str, enqueue: MediaPlayerEnqueue | None = None, - announce: bool | None = None, # noqa: ARG002 + announce: bool | None = None, **kwargs: Any, ) -> None: """Send the play_media command to the media player.""" - # Handle media_source if media_source.is_media_source_id(media_id): + # Handle media_source sourced_media = await media_source.async_resolve_media( self.hass, media_id, self.entity_id ) media_id = sourced_media.url media_id = async_process_play_media_url(self.hass, media_id) - # try to handle playback of item by name - elif "://" not in media_id and (item := await get_item_by_name(self.mass, media_id)): - media_id = item.uri - queue_opt = QUEUE_OPTION_MAP.get(enqueue, QueueOption.PLAY) - radio_mode = kwargs.get("radio_mode") or kwargs.get("extra", {}).get("radio_mode") or False + # forward to our advanced play_media handler + await self._async_play_media_advanced( + media_id=[media_id], + enqueue=enqueue, + announce=announce, + media_type=media_type, + radio_mode=kwargs[ATTR_MEDIA_EXTRA].get(ATTR_RADIO_MODE), + ) + + async def _async_play_media_advanced( + self, + media_id: list[str], + enqueue: MediaPlayerEnqueue | QueueOption | None = QueueOption.PLAY, + announce: bool | None = None, # noqa: ARG002 + radio_mode: bool | None = None, # noqa: ARG002 + media_type: str | None = None, # noqa: ARG002 + ) -> None: + """Send the play_media command to the media player.""" + # pylint: disable=too-many-arguments + media_uris: list[str] = [] + # work out (all) uri(s) to play + for media_id_str in media_id: + # prefer URI format + if "://" in media_id_str: + media_uris.append(media_id_str) + continue + # try content id as library id + if media_type and media_id_str.isnumeric(): + with suppress(MediaNotFoundError): + item = await self.mass.music.get_item(media_type, media_id_str, "library") + media_uris.append(item.uri) + continue + # lookup by name + if item := await self._get_item_by_name(media_id_str, media_type): + media_uris.append(item.uri) + + if not media_uris: + return if queue := self.mass.players.get_player_queue(self.player.active_source): - await self.mass.players.play_media(queue.queue_id, media_id, queue_opt, radio_mode) + queue_id = queue.queue_id else: - await self.mass.players.play_media(self.player_id, media_id, queue_opt, radio_mode) + queue_id = self.player_id + await self.mass.players.play_media( + queue_id, media=media_uris, option=enqueue, radio_mode=radio_mode + ) # announce/alert support # is_tts = "/api/tts_proxy" in media_id @@ -379,3 +430,63 @@ async def async_browse_media( ) -> BrowseMedia: """Implement the websocket media browsing helper.""" return await async_browse_media(self.hass, self.mass, media_content_id, media_content_type) + + async def _get_item_by_name( + self, name: str, media_type: str | None = None + ) -> MediaItemType | None: + """Try to find a media item (such as a playlist) by name.""" + # pylint: disable=too-many-nested-blocks + searchname = name.lower() + library_functions = [ + x + for x in ( + self.mass.music.get_library_playlists, + self.mass.music.get_library_radios, + self.mass.music.get_library_albums, + self.mass.music.get_library_tracks, + self.mass.music.get_library_artists, + ) + if not media_type or media_type.lower() in x.__name__ + ] + if not media_type: + # address (possible) voice command with mediatype in search string + for media_type_str in ("artist", "album", "track", "playlist"): + media_type_subst_str = f"{media_type_str} " + if media_type_subst_str in searchname: + media_type = MediaType(media_type_str) + searchname = searchname.replace(media_type_subst_str, "") + break + + # prefer (exact) lookup in the library by name + for func in library_functions: + result = await func(search=searchname) + for item in result.items: + if searchname == item.name.lower(): + return item + # repeat but account for tracks or albums where an artist name is used + if func in (self.mass.music.get_library_tracks, self.mass.music.get_library_albums): + for splitter in (" - ", " by "): + if splitter in searchname: + artistname, title = searchname.split(splitter, 1) + result = await func(search=title) + for item in result.items: + if item.name.lower() != title: + continue + for artist in item.artists: + if artist.name.lower() == artistname: + return item + # nothing found in the library, fallback to search + result = await self.mass.music.search( + searchname, media_types=[media_type] if media_type else MediaType.ALL + ) + for results in ( + result.tracks, + result.albums, + result.playlists, + result.artists, + result.radio, + ): + for item in results: + # simply return the first item because search is already sorted by best match + return item + return None diff --git a/custom_components/mass/services.py b/custom_components/mass/services.py index b17d9ca4..eb749a64 100644 --- a/custom_components/mass/services.py +++ b/custom_components/mass/services.py @@ -1,157 +1,73 @@ """Custom services for the Music Assistant integration.""" from __future__ import annotations -from typing import TYPE_CHECKING +from typing import Any +import homeassistant.helpers.config_validation as cv import voluptuous as vol -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse, callback from homeassistant.helpers.service import ServiceCall -from music_assistant.common.models.enums import QueueOption, RepeatMode -from music_assistant.common.models.media_items import MediaItemType +from music_assistant.common.models.enums import MediaType from .const import DOMAIN from .helpers import get_mass -if TYPE_CHECKING: - from music_assistant.client import MusicAssistantClient - -CMD_PLAY = "play" -CMD_PAUSE = "pause" -CMD_NEXT = "next" -CMD_PREVIOUS = "previous" -CMD_STOP = "stop" -CMD_CLEAR = "clear" -CMD_PLAY_MEDIA = "play_media" -CMD_SHUFFLE_ON = "shuffle_on" -CMD_SHUFFLE_OFF = "shuffle_off" -CMD_REPEAT = "repeat" -CMD_SNAPSHOT_CREATE = "snapshot_create" -CMD_SNAPSHOT_RESTORE = "snapshot_restore" -CMD_PLAY_ANNOUNCEMENT = "play_announcement" - - -def validate_command_data(data: dict) -> dict: - """Validate command args/mode.""" - cmd = data["command"] - if cmd == CMD_REPEAT and "repeat_mode" not in data: - raise vol.Invalid("Missing repeat_mode") - if cmd == CMD_PLAY_MEDIA and "enqueue_mode" not in data: - raise vol.Invalid("Missing enqueue_mode") - if cmd in (CMD_PLAY_MEDIA, CMD_PLAY_ANNOUNCEMENT) and not data.get("uri"): - raise vol.Invalid("No URI specified") - return data - - -QueueCommandServiceSchema = vol.Schema( - vol.All( - { - "player_id": vol.Union(str, [str]), - "command": str, - "uri": vol.Union(str, [str], None), - "radio_mode": vol.Optional(bool), - "repeat_mode": vol.Optional(vol.Coerce(RepeatMode)), - "enqueue_mode": vol.Optional(vol.Coerce(QueueOption)), - }, - validate_command_data, - ) -) +SERVICE_SEARCH = "search" +ATTR_MEDIA_TYPE = "media_type" +ATTR_QUERY = "query" +ATTR_LIMIT = "limit" @callback -def register_services(hass: HomeAssistant): +def register_services(hass: HomeAssistant) -> None: """Register custom services.""" - async def handle_queue_command(call: ServiceCall) -> None: + async def handle_search(call: ServiceCall) -> ServiceResponse: """Handle queue_command service.""" mass = get_mass(hass) - data = call.data - cmd = data["command"] - if isinstance(data["player_id"], list): - player_ids = data["player_id"] - else: - player_ids = [data["player_id"]] - for player_id in player_ids: - # translate entity_id --> player_id - if entity := hass.states.get(player_id): - player_id = entity.attributes.get("mass_player_id", player_id) # noqa: PLW2901 - player = mass.players.get_player(player_id) - if not player: - raise RuntimeError(f"Invalid player id: {player_id}") - if queue := mass.players.get_player_queue(player.active_source): - queue_id = queue.queue_id - else: - queue_id = player_id - if cmd == CMD_PLAY: - await mass.players.queue_command_play(queue_id) - elif cmd == CMD_PAUSE: - await mass.players.queue_command_pause(queue_id) - elif cmd == CMD_NEXT: - await mass.players.queue_command_next(queue_id) - elif cmd == CMD_PREVIOUS: - await mass.players.queue_command_previous(queue_id) - elif cmd == CMD_STOP: - await mass.players.queue_command_stop(queue_id) - elif cmd == CMD_CLEAR: - await mass.players.queue_command_clear(queue_id) - elif cmd == CMD_PLAY_MEDIA: - media_items = [] - uris = data["uri"] if isinstance(data["uri"], list) else [data["uri"]] - for uri in uris: - # try to handle playback of item by name - if "://" not in uri and (item := await get_item_by_name(mass, uri)): - media_items.append(item) - else: - media_items.append(uri) - await mass.players.play_media( - queue_id, - media_items, - data["enqueue_mode"], - radio_mode=data.get("radio_mode", False), - ) - elif cmd == CMD_SHUFFLE_ON: - await mass.players.queue_command_shuffle(queue_id, True) - elif cmd == CMD_SHUFFLE_OFF: - await mass.players.queue_command_shuffle(queue_id, False) - elif cmd == CMD_REPEAT: - await mass.players.queue_command_repeat(queue_id, data["repeat_mode"]) - # elif cmd == CMD_SNAPSHOT_CREATE: - # await queue.snapshot_create() - # elif cmd == CMD_SNAPSHOT_RESTORE: - # await queue.snapshot_restore() - # elif cmd == CMD_PLAY_ANNOUNCEMENT: - # await queue.play_announcement(data["uri"]) + result = await mass.music.search( + search_query=call.data[ATTR_QUERY], + media_types=call.data.get(ATTR_MEDIA_TYPE, MediaType.ALL), + limit=call.data[ATTR_LIMIT], + ) - hass.services.async_register( - DOMAIN, "queue_command", handle_queue_command, schema=QueueCommandServiceSchema - ) + # return limited result to prevent it being too verbose + def compact_item(item: dict[str, Any]) -> dict[str, Any]: + """Return compacted MediaItem dict.""" + for key in ( + "metadata", + "provider_mappings", + "favorite", + "timestamp_added", + "timestamp_modified", + "mbid", + ): + item.pop(key, None) + for key, value in item.items(): + if isinstance(value, dict): + item[key] = compact_item(value) + elif isinstance(value, list): + for subitem in value: + compact_item(subitem) + # item[key] = [compact_item(x) if isinstance(x, dict) else x for x in value] + return item + dict_result: dict[str, list[dict[str, Any]]] = result.to_dict() + for media_type_key in dict_result: + for item in dict_result[media_type_key]: + compact_item(item) + return dict_result -async def get_item_by_name(mass: MusicAssistantClient, name: str) -> MediaItemType | None: - """Try to find a media item (such as a playlist) by name.""" - # iterate media controllers one by one, - # start with playlists and radio as those are the most common one - for func in ( - mass.music.get_library_playlists, - mass.music.get_library_radios, - mass.music.get_library_albums, - mass.music.get_library_tracks, - mass.music.get_library_artists, - ): - result = await func(search=name) - for item in result.items: - if name.lower() == item.name.lower(): - return item - if " - " in name: - artistname, title = name.lower().split(" - ", 1) - for func in ( - mass.music.get_library_albums, - mass.music.get_library_tracks, - ): - result = await func(search=title) - for item in result.items: - if item.name.lower() != title: - continue - for artist in item.artists: - if artist.name.lower() == artistname: - return item - return None + hass.services.async_register( + DOMAIN, + SERVICE_SEARCH, + handle_search, + schema=vol.Schema( + { + vol.Required(ATTR_QUERY): cv.string, + vol.Optional(ATTR_MEDIA_TYPE): vol.All(cv.ensure_list, [vol.Coerce(MediaType)]), + vol.Optional(ATTR_LIMIT, default=5): vol.Coerce(int), + } + ), + supports_response=SupportsResponse.ONLY, + ) diff --git a/custom_components/mass/services.yaml b/custom_components/mass/services.yaml index 61626ce8..bfc95d76 100644 --- a/custom_components/mass/services.yaml +++ b/custom_components/mass/services.yaml @@ -1,57 +1,34 @@ # Descriptions for Music Assistant custom services -queue_command: - name: Queue command - description: "Send a command directly to a Music Assistant player(queue). Note: Only use this special service for advanced purposes. Usually you can just the builtin HA services." +play_media: + target: + entity: + domain: media_player + integration: mass + supported_features: + - media_player.MediaPlayerEntityFeature.PLAY_MEDIA fields: - - player_id: - name: Player ID - description: Music Assistant Player ID of the player to execute the command. May also be the HA entity ID of a MA player. + media_id: required: true - advanced: false - example: "aa:bb:cc:dd:ee" + example: "spotify://playlist/aabbccddeeff" selector: - text: - - command: - name: Command - description: The command to issue on the player(queue). - required: true - advanced: false - example: "play" + object: + media_type: + required: false + example: "playlist" selector: select: options: - - play - - pause - - next - - previous - - stop - - clear - - play_media - - shuffle_on - - shuffle_off - - repeat - - snapshot_create - - snapshot_restore - - play_announcement - - uri: - name: Media item(s) to play - description: To be used together with play_media command, the uri(s) to play on the queue. May also be the name of an item you want to play. - required: false - advanced: false - example: "spotify://playlist/aabbccddeeff" - selector: - text: - - enqueue_mode: - name: Enqueue mode. - description: "Only to be used with play_media command: Select Enqueue mode." + - artist + - album + - playlist + - track + - radio + enqueue: + filter: + supported_features: + - media_player.MediaPlayerEntityFeature.MEDIA_ENQUEUE required: false - advanced: true - default: "play" selector: select: options: @@ -60,23 +37,47 @@ queue_command: - "next" - "replace_next" - "add" - - repeat_mode: - name: Repeat mode. - description: "Only to be used with repeat command: Select repeat mode." + translation_key: enqueue + announce: + filter: + supported_features: + - media_player.MediaPlayerEntityFeature.MEDIA_ANNOUNCE required: false - advanced: true + example: "true" selector: - select: - options: - - "one" - - "all" - - "off" - + boolean: radio_mode: - name: Enable radio mode. - description: "Only to be used with play_media command: Whether to enable radio mode." required: false advanced: true selector: boolean: + + +search: + fields: + query: + required: true + example: "Queen - Innuendo" + selector: + text: + media_type: + required: false + example: "playlist" + selector: + select: + multiple: true + options: + - artist + - album + - playlist + - track + - radio + limit: + required: false + example: 25 + default: 25 + selector: + number: + min: 1 + max: 100 + step: 1 diff --git a/custom_components/mass/strings.json b/custom_components/mass/strings.json index dd890f84..21d790b0 100644 --- a/custom_components/mass/strings.json +++ b/custom_components/mass/strings.json @@ -57,5 +57,62 @@ "install_addon": "Please wait while the Music Assistant Server add-on installation finishes. This can take several minutes.", "start_addon": "Please wait while the Music Assistant Server add-on starts. This add-on is what powers Music Assistant in Home Assistant. This may take some seconds." } + }, + "services": { + "play_media": { + "name": "Play Media (advanced)", + "description": "Play media on a Music Assistant player with more fine grained control options.", + "fields": { + "media_id": { + "name": "Media ID(s)", + "description": "URI or name of the item you want to play. Specify a list if you want to play/enqueue multiple items." + }, + "media_type": { + "name": "Media type", + "description": "The type of the content to play. Such as artist, album, track or playlist. Will be auto determined if omitted." + }, + "enqueue": { + "name": "Enqueue", + "description": "If the content should be played now or be added to the queue." + }, + "announce": { + "name": "Announce", + "description": "If the media should be played as an announcement." + }, + "radio_mode": { + "name": "Enable Radio Mode", + "description": "Enable radio mode to auto generate a playlist based on the selection." + } + } + }, + "search": { + "name": "Search Music Assistant", + "description": "Perform a global search on the Music Assistant library and all providers.", + "fields": { + "query": { + "name": "Query", + "description": "The search query." + }, + "media_type": { + "name": "Content type(s)", + "description": "The type of the content to search. Such as artist, album, track or playlist. All types if omitted." + }, + "limit": { + "name": "Limit", + "description": "Maximum number of items to return (per media type)." + } + } + } + }, + "selector": { + "enqueue": { + "options": { + "play": "Play", + "next": "Play next", + "add": "Add to queue", + "replace": "Play now and clear queue", + "replace_next": "Play next and clear queue" + } + } } } diff --git a/custom_components/mass/translations/en.json b/custom_components/mass/translations/en.json index dd890f84..97a1cdae 100644 --- a/custom_components/mass/translations/en.json +++ b/custom_components/mass/translations/en.json @@ -57,5 +57,62 @@ "install_addon": "Please wait while the Music Assistant Server add-on installation finishes. This can take several minutes.", "start_addon": "Please wait while the Music Assistant Server add-on starts. This add-on is what powers Music Assistant in Home Assistant. This may take some seconds." } + }, + "services": { + "play_media": { + "name": "Play Media (advanced)", + "description": "Play media on a Music Assistant player with more fine grained control options.", + "fields": { + "media_id": { + "name": "Media ID(s)", + "description": "URI or name of the item you want to play. Specify a list if you want to play/enqueue multiple items." + }, + "media_type": { + "name": "Media type", + "description": "The type of the content to play. Such as artist, album, track or playlist. Will be auto determined if omitted." + }, + "enqueue": { + "name": "Enqueue", + "description": "If the content should be played now or be added to the queue." + }, + "announce": { + "name": "Announce", + "description": "If the media should be played as an announcement." + }, + "radio_mode": { + "name": "Enable Radio Mode", + "description": "Enable radio mode to auto generate a playlist based on the selection." + } + } + }, + "search": { + "name": "Search Music Assistant", + "description": "Perform a global search on the Music Assistant library and all providers.", + "fields": { + "query": { + "name": "Query", + "description": "The search query." + }, + "media_type": { + "name": "Media type(s)", + "description": "The type of the content to search. Such as artist, album, track or playlist. All types if omitted." + }, + "limit": { + "name": "Limit", + "description": "Maximum number of items to return (per media type)." + } + } + } + }, + "selector": { + "enqueue": { + "options": { + "play": "Play", + "next": "Play next", + "add": "Add to queue", + "replace": "Play now and clear queue", + "replace_next": "Play next and clear queue" + } + } } }