From 1020b2aeae82d87b10788a1f5c72d18be236608b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 1 Nov 2023 16:39:43 +0100 Subject: [PATCH] Some improvements to the HA services (again) (#1596) --- custom_components/mass/media_player.py | 91 ++++++++++++--------- custom_components/mass/services.py | 19 ++++- custom_components/mass/services.yaml | 27 +++++- custom_components/mass/strings.json | 26 ++++-- custom_components/mass/translations/en.json | 24 +++++- 5 files changed, 133 insertions(+), 54 deletions(-) diff --git a/custom_components/mass/media_player.py b/custom_components/mass/media_player.py index 1c18982a..ec43a786 100644 --- a/custom_components/mass/media_player.py +++ b/custom_components/mass/media_player.py @@ -95,6 +95,8 @@ ATTR_RADIO_MODE = "radio_mode" ATTR_MEDIA_ID = "media_id" ATTR_MEDIA_TYPE = "media_type" +ATTR_ARTIST = "artist" +ATTR_ALBUM = "album" async def async_setup_entry( @@ -125,10 +127,12 @@ async def handle_player_added(event: MassEvent) -> None: 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.Optional(ATTR_MEDIA_TYPE): vol.Coerce(MediaType), vol.Exclusive(ATTR_MEDIA_ENQUEUE, "enqueue_announce"): vol.Coerce(QueueOption), vol.Exclusive(ATTR_MEDIA_ANNOUNCE, "enqueue_announce"): cv.boolean, + vol.Optional(ATTR_ARTIST): cv.string, + vol.Optional(ATTR_ALBUM): cv.string, vol.Optional(ATTR_RADIO_MODE): vol.Coerce(bool), }, "_async_play_media_advanced", @@ -382,10 +386,12 @@ async def async_play_media( async def _async_play_media_advanced( self, media_id: list[str], + artist: str | None = None, + album: str | None = None, 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 + radio_mode: bool | None = None, + media_type: str | None = None, ) -> None: """Send the play_media command to the media player.""" # pylint: disable=too-many-arguments @@ -403,29 +409,30 @@ async def _async_play_media_advanced( media_uris.append(item.uri) continue # lookup by name - if item := await self._get_item_by_name(media_id_str, media_type): + if item := await self._get_item_by_name(media_id_str, artist, album, media_type): media_uris.append(item.uri) if not media_uris: - return + raise MediaNotFoundError(f"Could not resolve {media_id} to playable media item") + # determine active queue to send the play request to if queue := self.mass.players.get_player_queue(self.player.active_source): queue_id = queue.queue_id else: queue_id = self.player_id + + # announce/alert support (WIP) + if announce and radio_mode: + radio_mode = None + if announce is None and "/api/tts_proxy" in media_id: + announce = True + if announce: + raise NotImplementedError("Music Assistant does not yet support announcements") + 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 - # if announce or is_tts: - # self.hass.create_task( - # self.player.active_queue.play_announcement(media_id, is_tts) - # ) - # else: - # await self.player.active_queue.play_media(media_id, queue_opt) - async def async_browse_media( self, media_content_type=None, media_content_id=None ) -> BrowseMedia: @@ -433,7 +440,11 @@ async def async_browse_media( 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 + self, + name: str, + artist: str | None = None, + album: str | None = None, + 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 @@ -443,42 +454,44 @@ async def _get_item_by_name( 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_albums, 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: + # handle optional artist filter + if ( + artist + and (artists := getattr(item, "artists", None)) + and not any(x for x in artists if x.name.lower() == artist.lower()) + ): + continue + # handle optional album filter + if ( + album + and (item_album := getattr(item, "album", None)) + and item_album.name.lower() != album.lower() + ): + continue 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 + # nothing found in the library, fallback to global search + search_name = name + if album and artist: + search_name = f"{artist} - {album} - {name}" + elif album: + search_name = f"{album} - {name}" + elif artist: + search_name = f"{artist} - {name}" result = await self.mass.music.search( - searchname, media_types=[media_type] if media_type else MediaType.ALL + search_query=search_name, + media_types=[media_type] if media_type else MediaType.ALL, + limit=5, ) for results in ( result.tracks, diff --git a/custom_components/mass/services.py b/custom_components/mass/services.py index eb749a64..4e09413a 100644 --- a/custom_components/mass/services.py +++ b/custom_components/mass/services.py @@ -14,7 +14,9 @@ SERVICE_SEARCH = "search" ATTR_MEDIA_TYPE = "media_type" -ATTR_QUERY = "query" +ATTR_SEARCH_NAME = "name" +ATTR_SEARCH_ARTIST = "artist" +ATTR_SEARCH_ALBUM = "album" ATTR_LIMIT = "limit" @@ -25,8 +27,17 @@ def register_services(hass: HomeAssistant) -> None: async def handle_search(call: ServiceCall) -> ServiceResponse: """Handle queue_command service.""" mass = get_mass(hass) + search_name = call.data[ATTR_SEARCH_NAME] + search_artist = call.data.get(ATTR_SEARCH_ARTIST) + search_album = call.data.get(ATTR_SEARCH_ALBUM) + if search_album and search_artist: + search_name = f"{search_artist} - {search_album} - {search_name}" + elif search_album: + search_name = f"{search_album} - {search_name}" + elif search_artist: + search_name = f"{search_artist} - {search_name}" result = await mass.music.search( - search_query=call.data[ATTR_QUERY], + search_query=search_name, media_types=call.data.get(ATTR_MEDIA_TYPE, MediaType.ALL), limit=call.data[ATTR_LIMIT], ) @@ -64,8 +75,10 @@ def compact_item(item: dict[str, Any]) -> dict[str, Any]: handle_search, schema=vol.Schema( { - vol.Required(ATTR_QUERY): cv.string, + vol.Required(ATTR_SEARCH_NAME): cv.string, vol.Optional(ATTR_MEDIA_TYPE): vol.All(cv.ensure_list, [vol.Coerce(MediaType)]), + vol.Optional(ATTR_SEARCH_ARTIST): cv.string, + vol.Optional(ATTR_SEARCH_ALBUM): cv.string, vol.Optional(ATTR_LIMIT, default=5): vol.Coerce(int), } ), diff --git a/custom_components/mass/services.yaml b/custom_components/mass/services.yaml index bfc95d76..f9748779 100644 --- a/custom_components/mass/services.yaml +++ b/custom_components/mass/services.yaml @@ -46,6 +46,16 @@ play_media: example: "true" selector: boolean: + artist: + required: false + example: "Queen" + selector: + text: + album: + required: false + example: "News of the world" + selector: + text: radio_mode: required: false advanced: true @@ -55,9 +65,9 @@ play_media: search: fields: - query: + name: required: true - example: "Queen - Innuendo" + example: "We Are The Champions" selector: text: media_type: @@ -72,10 +82,21 @@ search: - playlist - track - radio + artist: + required: false + example: "Queen" + selector: + text: + album: + required: false + example: "News of the world" + selector: + text: limit: required: false + advanced: true example: 25 - default: 25 + default: 5 selector: number: min: 1 diff --git a/custom_components/mass/strings.json b/custom_components/mass/strings.json index ff5a82b8..9b9542ea 100644 --- a/custom_components/mass/strings.json +++ b/custom_components/mass/strings.json @@ -79,6 +79,14 @@ "name": "Announce", "description": "If the media should be played as an announcement." }, + "artist": { + "name": "Artist name", + "description": "When specifying a track or album by name in the Media ID field, you can optionally restrict results by this artist name." + }, + "album": { + "name": "Album name", + "description": "When specifying a track by name in the Media ID field, you can optionally restrict results by this album name." + }, "radio_mode": { "name": "Enable Radio Mode", "description": "Enable radio mode to auto generate a playlist based on the selection." @@ -89,13 +97,21 @@ "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." + "name": { + "name": "Search name", + "description": "The name/title to search for." }, "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." + "name": "Media type(s)", + "description": "The type of the content to search. Such as artist, album, track, radio or playlist. All types if omitted." + }, + "artist": { + "name": "Artist name", + "description": "When specifying a track or album name in the name field, you can optionally restrict results by this artist name." + }, + "album": { + "name": "Album name", + "description": "When specifying a track name in the name field, you can optionally restrict results by this album name." }, "limit": { "name": "Limit", diff --git a/custom_components/mass/translations/en.json b/custom_components/mass/translations/en.json index e3cc4325..7927278a 100644 --- a/custom_components/mass/translations/en.json +++ b/custom_components/mass/translations/en.json @@ -79,6 +79,14 @@ "name": "Announce", "description": "If the media should be played as an announcement." }, + "artist": { + "name": "Artist name", + "description": "When specifying a track or album by name in the Media ID field, you can optionally restrict results by this artist name." + }, + "album": { + "name": "Album name", + "description": "When specifying a track by name in the Media ID field, you can optionally restrict results by this album name." + }, "radio_mode": { "name": "Enable Radio Mode", "description": "Enable radio mode to auto generate a playlist based on the selection." @@ -89,13 +97,21 @@ "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." + "name": { + "name": "Search name", + "description": "The name/title to search for." }, "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." + "description": "The type of the content to search. Such as artist, album, track, radio or playlist. All types if omitted." + }, + "artist": { + "name": "Artist name", + "description": "When specifying a track or album name in the name field, you can optionally restrict results by this artist name." + }, + "album": { + "name": "Album name", + "description": "When specifying a track name in the name field, you can optionally restrict results by this album name." }, "limit": { "name": "Limit",