diff --git a/custom_components/mass_queue/actions.py b/custom_components/mass_queue/actions.py index 9fc3f23..6840521 100644 --- a/custom_components/mass_queue/actions.py +++ b/custom_components/mass_queue/actions.py @@ -17,11 +17,13 @@ from music_assistant_models.errors import ( InvalidCommand, MediaNotFoundError, + ProviderUnavailableError, ) from .const import ( ATTR_COMMAND, ATTR_DATA, + ATTR_DURATION, ATTR_FAVORITE, ATTR_LIMIT, ATTR_LIMIT_AFTER, @@ -62,12 +64,12 @@ MOVE_QUEUE_ITEM_NEXT_SERVICE_SCHEMA, MOVE_QUEUE_ITEM_UP_SERVICE_SCHEMA, PLAY_QUEUE_ITEM_SERVICE_SCHEMA, - PLAYLIST_ITEM_SCHEMA, QUEUE_ITEM_SCHEMA, QUEUE_ITEMS_SERVICE_SCHEMA, REMOVE_QUEUE_ITEM_SERVICE_SCHEMA, SEND_COMMAND_SERVICE_SCHEMA, SET_GROUP_VOLUME_SERVICE_SCHEMA, + TRACK_ITEM_SCHEMA, ) from .utils import ( find_image, @@ -196,7 +198,6 @@ async def get_active_queue(self, entity_id: str): async def _format_queue_item(self, queue_item: dict) -> dict: """Format list of queue items for response.""" - LOGGER.debug(f"Got queue item with keys {queue_item.keys()}") media = queue_item["media_item"] queue_item_id = queue_item["queue_item_id"] @@ -224,7 +225,6 @@ async def _format_queue_item(self, queue_item: dict) -> dict: ) if local_image_encoded: response[ATTR_LOCAL_IMAGE_ENCODED] = local_image_encoded - LOGGER.debug(f"Sending back response with keys {response.keys()}") return response async def send_command(self, call: ServiceCall) -> ServiceResponse: @@ -346,22 +346,72 @@ async def unfavorite_item(self, call: ServiceCall) -> ServiceResponse: library_item_id=item_id, ) - async def get_playlist_items(self, playlist_uri: str): + async def get_artist_details(self, artist_uri): + """Retrieves the details for an artist.""" + provider, item_id = parse_uri(artist_uri) + LOGGER.debug(f"Getting artist details for provider {provider}") + return await self._client.music.get_artist(item_id, provider) + + async def get_album_details(self, album_uri): + """Retrieves the details for an album.""" + provider, item_id = parse_uri(album_uri) + LOGGER.debug(f"Getting album details for provider {provider}") + return await self._client.music.get_album(item_id, provider) + + async def get_playlist_details(self, playlist_uri): + """Retrieves the details for a playlist.""" + provider, item_id = parse_uri(playlist_uri) + LOGGER.debug(f"Getting album details for provider {provider}") + return await self._client.music.get_playlist(item_id, provider) + + async def get_artist_tracks(self, artist_uri: str, page: int | None = None): + """Retrieves a limited number of tracks from an artist.""" + details = await self.get_artist_details(artist_uri) + mappings = list(details.provider_mappings) + if not len(mappings) > 0: + msg = f"URI {artist_uri} returned no results!" + raise ProviderUnavailableError(msg) + mapping = mappings[0] + item_id = mapping.item_id + provider = mapping.provider_domain + resp = ( + await self._client.music.get_artist_tracks(item_id, provider) + if not page + else await self._client.music.get_artist_tracks(item_id, provider, page) + ) + return [self.format_track_item(item.to_dict()) for item in resp] + + async def get_album_tracks(self, album_uri: str, page: int | None = None): + """Retrieves all tracks from an album.""" + details = await self.get_album_details(album_uri) + mappings = list(details.provider_mappings) + if not len(mappings) > 0: + msg = f"URI {album_uri} returned no results!" + raise ProviderUnavailableError(msg) + mapping = mappings[0] + item_id = mapping.item_id + provider = mapping.provider_domain + resp = ( + await self._client.music.get_album_tracks(item_id, provider) + if not page + else await self._client.music.get_album_tracks(item_id, provider, page) + ) + return [self.format_track_item(item.to_dict()) for item in resp] + + async def get_playlist_tracks(self, playlist_uri: str, page: int | None = None): """Retrieves all playlist items.""" provider, item_id = parse_uri(playlist_uri) LOGGER.debug( f"Getting playlist items for provider {provider}, item_id {item_id}", ) - resp = await self._client.music.get_playlist_tracks(item_id, provider) - LOGGER.debug(f"Got response with {len(resp) if resp else 0} items") - result = [self.format_playlist_item(item.to_dict()) for item in resp] - msg = f"Got response {result[0]}" - if len(msg) > 200: - msg = f"{msg[180]}..." + "}" - LOGGER.debug(msg) - return result - - def format_playlist_item(self, playlist_item: dict) -> dict: + resp = ( + await self._client.music.get_playlist_tracks(item_id, provider) + if not page + else await self._client.music.get_playlist_tracks(item_id, provider, page) + ) + return [self.format_track_item(item.to_dict()) for item in resp] + + def format_track_item(self, playlist_item: dict) -> TRACK_ITEM_SCHEMA: """Processes the individual items in a playlist.""" media_title = playlist_item.get("name") or "N/A" media_album = playlist_item.get("album") or "N/A" @@ -370,16 +420,18 @@ def format_playlist_item(self, playlist_item: dict) -> dict: media_image = find_image(playlist_item) or "" local_image_encoded = playlist_item.get(ATTR_LOCAL_IMAGE_ENCODED) favorite = playlist_item["favorite"] + duration = playlist_item["duration"] or 0 artists = playlist_item["artists"] artist_names = [artist["name"] for artist in artists] media_artist = ", ".join(artist_names) - response: ServiceResponse = PLAYLIST_ITEM_SCHEMA( + response: ServiceResponse = TRACK_ITEM_SCHEMA( { ATTR_MEDIA_TITLE: media_title, ATTR_MEDIA_ALBUM_NAME: media_album_name, ATTR_MEDIA_ARTIST: media_artist, ATTR_MEDIA_CONTENT_ID: media_content_id, + ATTR_DURATION: duration, ATTR_MEDIA_IMAGE: media_image, ATTR_FAVORITE: favorite, }, diff --git a/custom_components/mass_queue/const.py b/custom_components/mass_queue/const.py index bb33dbf..1d0ae11 100644 --- a/custom_components/mass_queue/const.py +++ b/custom_components/mass_queue/const.py @@ -11,6 +11,11 @@ DEFAULT_NAME = "Music Assistant Queue Items" SERVICE_CLEAR_QUEUE_FROM_HERE = "clear_queue_from_here" SERVICE_GET_GROUP_VOLUME = "get_group_volume" +SERVICE_GET_ALBUM = "get_album" +SERVICE_GET_ALBUM_TRACKS = "get_album_tracks" +SERVICE_GET_ARTIST = "get_artist" +SERVICE_GET_ARTIST_TRACKS = "get_artist_tracks" +SERVICE_GET_PLAYLIST = "get_playlist" SERVICE_GET_PLAYLIST_TRACKS = "get_playlist_tracks" SERVICE_GET_QUEUE_ITEMS = "get_queue_items" SERVICE_GET_RECOMMENDATIONS = "get_recommendations" @@ -26,6 +31,7 @@ ATTR_COMMAND = "command" ATTR_CONFIG_ENTRY_ID = "config_entry_id" ATTR_DATA = "data" +ATTR_DURATION = "duration" ATTR_FAVORITE = "favorite" ATTR_LIMIT = "limit" ATTR_LIMIT_AFTER = "limit_after" @@ -37,6 +43,7 @@ ATTR_MEDIA_IMAGE = "media_image" ATTR_MEDIA_TITLE = "media_title" ATTR_OFFSET = "offset" +ATTR_PAGE = "page" ATTR_PLAYER_ENTITY = "entity" ATTR_URI = "uri" ATTR_PROVIDERS = "providers" diff --git a/custom_components/mass_queue/controller.py b/custom_components/mass_queue/controller.py index f4250b4..276c126 100644 --- a/custom_components/mass_queue/controller.py +++ b/custom_components/mass_queue/controller.py @@ -304,7 +304,9 @@ def update(self, player_id: str, queue_id: str): def send_ha_event(self, event_data): """Send event to Home Assistant.""" - LOGGER.debug(f"Sending event type {MASS_QUEUE_EVENT_DOMAIN}, data {event_data}") + LOGGER.debug( + f"Sending event type {MASS_QUEUE_EVENT_DOMAIN}, data {event_data}", + ) self._hass.bus.async_fire(MASS_QUEUE_EVENT_DOMAIN, event_data) @@ -358,7 +360,9 @@ def remove(self, queue_id): def send_ha_event(self, event_data): """Send event to Home Assistant.""" - LOGGER.debug(f"Sending event type {MASS_QUEUE_EVENT_DOMAIN}, data {event_data}") + LOGGER.debug( + f"Sending event type {MASS_QUEUE_EVENT_DOMAIN}, data {event_data}", + ) self._hass.bus.async_fire(MASS_QUEUE_EVENT_DOMAIN, event_data) async def process_image_single_item(self, queue_item: dict): diff --git a/custom_components/mass_queue/icons.json b/custom_components/mass_queue/icons.json index 49f5f47..da0e38b 100644 --- a/custom_components/mass_queue/icons.json +++ b/custom_components/mass_queue/icons.json @@ -1,5 +1,10 @@ { "services": { + "get_album": { "service": "mdi:album"}, + "get_album_tracks": { "service": "mdi:album"}, + "get_artist": { "service": "mdi:account-music"}, + "get_artist_tracks": { "service": "mdi:account-music"}, + "get_playlist": { "service": "mdi:playlist-music"}, "get_playlist_tracks": { "service": "mdi:playlist-music"}, "get_queue_items": { "service": "mdi:playlist-music" }, "move_queue_item_down": { "service": "mdi:arrow-down" }, diff --git a/custom_components/mass_queue/schemas.py b/custom_components/mass_queue/schemas.py index ee456f4..815e8d5 100644 --- a/custom_components/mass_queue/schemas.py +++ b/custom_components/mass_queue/schemas.py @@ -9,6 +9,7 @@ ATTR_COMMAND, ATTR_CONFIG_ENTRY_ID, ATTR_DATA, + ATTR_DURATION, ATTR_FAVORITE, ATTR_LIMIT, ATTR_LIMIT_AFTER, @@ -20,6 +21,7 @@ ATTR_MEDIA_IMAGE, ATTR_MEDIA_TITLE, ATTR_OFFSET, + ATTR_PAGE, ATTR_PLAYER_ENTITY, ATTR_PROVIDERS, ATTR_QUEUE_ITEM_ID, @@ -53,7 +55,7 @@ }, ) -PLAYLIST_ITEM_SCHEMA = vol.Schema( +TRACK_ITEM_SCHEMA = vol.Schema( { vol.Required(ATTR_MEDIA_TITLE): str, vol.Required(ATTR_MEDIA_ALBUM_NAME): str, @@ -61,6 +63,7 @@ vol.Required(ATTR_MEDIA_CONTENT_ID): str, vol.Required(ATTR_MEDIA_IMAGE): str, vol.Required(ATTR_FAVORITE): bool, + vol.Required(ATTR_DURATION): vol.Any(int, None), vol.Optional(ATTR_LOCAL_IMAGE_ENCODED): str, }, ) @@ -122,7 +125,15 @@ }, ) -GET_PLAYLIST_TRACKS_SERVICE_SCHEMA = vol.Schema( +GET_TRACKS_SERVICE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): str, + vol.Required(ATTR_URI): str, + vol.Optional(ATTR_PAGE): int, + }, +) + +GET_DATA_SERVICE_SCHEMA = vol.Schema( { vol.Required(ATTR_CONFIG_ENTRY_ID): str, vol.Required(ATTR_URI): str, diff --git a/custom_components/mass_queue/services.py b/custom_components/mass_queue/services.py index 5546030..8489d61 100644 --- a/custom_components/mass_queue/services.py +++ b/custom_components/mass_queue/services.py @@ -16,7 +16,12 @@ DOMAIN, LOGGER, SERVICE_CLEAR_QUEUE_FROM_HERE, + SERVICE_GET_ALBUM, + SERVICE_GET_ALBUM_TRACKS, + SERVICE_GET_ARTIST, + SERVICE_GET_ARTIST_TRACKS, SERVICE_GET_GROUP_VOLUME, + SERVICE_GET_PLAYLIST, SERVICE_GET_PLAYLIST_TRACKS, SERVICE_GET_QUEUE_ITEMS, SERVICE_GET_RECOMMENDATIONS, @@ -31,9 +36,10 @@ ) from .schemas import ( CLEAR_QUEUE_FROM_HERE_SERVICE_SCHEMA, + GET_DATA_SERVICE_SCHEMA, GET_GROUP_VOLUME_SERVICE_SCHEMA, - GET_PLAYLIST_TRACKS_SERVICE_SCHEMA, GET_RECOMMENDATIONS_SERVICE_SCHEMA, + GET_TRACKS_SERVICE_SCHEMA, MOVE_QUEUE_ITEM_DOWN_SERVICE_SCHEMA, MOVE_QUEUE_ITEM_NEXT_SERVICE_SCHEMA, MOVE_QUEUE_ITEM_UP_SERVICE_SCHEMA, @@ -138,7 +144,42 @@ def register_actions(hass) -> None: DOMAIN, SERVICE_GET_PLAYLIST_TRACKS, get_playlist_tracks, - schema=GET_PLAYLIST_TRACKS_SERVICE_SCHEMA, + schema=GET_TRACKS_SERVICE_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) + hass.services.async_register( + DOMAIN, + SERVICE_GET_ALBUM_TRACKS, + get_album_tracks, + schema=GET_TRACKS_SERVICE_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) + hass.services.async_register( + DOMAIN, + SERVICE_GET_ARTIST_TRACKS, + get_artist_tracks, + schema=GET_TRACKS_SERVICE_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) + hass.services.async_register( + DOMAIN, + SERVICE_GET_ALBUM, + get_album, + schema=GET_DATA_SERVICE_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) + hass.services.async_register( + DOMAIN, + SERVICE_GET_ARTIST, + get_artist, + schema=GET_DATA_SERVICE_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) + hass.services.async_register( + DOMAIN, + SERVICE_GET_PLAYLIST, + get_playlist, + schema=GET_DATA_SERVICE_SCHEMA, supports_response=SupportsResponse.ONLY, ) @@ -264,6 +305,30 @@ async def clear_queue_from_here(call: ServiceCall): await client.player_queues.queue_command_delete(queue_id, queue_item_id) +async def get_album_tracks(call: ServiceCall): + """Gets all tracks in an album.""" + config_entry = call.data[ATTR_CONFIG_ENTRY_ID] + uri = call.data[ATTR_URI] + hass = call.hass + entry = hass.config_entries.async_get_entry(config_entry) + actions = entry.runtime_data.actions + return { + "tracks": await actions.get_album_tracks(uri), + } + + +async def get_artist_tracks(call: ServiceCall): + """Gets all tracks for an artist.""" + config_entry = call.data[ATTR_CONFIG_ENTRY_ID] + uri = call.data[ATTR_URI] + hass = call.hass + entry = hass.config_entries.async_get_entry(config_entry) + actions = entry.runtime_data.actions + return { + "tracks": await actions.get_artist_tracks(uri), + } + + async def get_playlist_tracks(call: ServiceCall): """Gets all tracks in a playlist.""" config_entry = call.data[ATTR_CONFIG_ENTRY_ID] @@ -272,5 +337,35 @@ async def get_playlist_tracks(call: ServiceCall): entry = hass.config_entries.async_get_entry(config_entry) actions = entry.runtime_data.actions return { - "tracks": await actions.get_playlist_items(uri), + "tracks": await actions.get_playlist_tracks(uri), } + + +async def get_album(call: ServiceCall): + """Returns the details about an album from the server.""" + config_entry = call.data[ATTR_CONFIG_ENTRY_ID] + uri = call.data[ATTR_URI] + hass = call.hass + entry = hass.config_entries.async_get_entry(config_entry) + actions = entry.runtime_data.actions + return (await actions.get_album_details(uri)).to_dict() + + +async def get_artist(call: ServiceCall): + """Returns the details about an artist from the server.""" + config_entry = call.data[ATTR_CONFIG_ENTRY_ID] + uri = call.data[ATTR_URI] + hass = call.hass + entry = hass.config_entries.async_get_entry(config_entry) + actions = entry.runtime_data.actions + return (await actions.get_artist_details(uri)).to_dict() + + +async def get_playlist(call: ServiceCall): + """Returns the details about a playlist from the server.""" + config_entry = call.data[ATTR_CONFIG_ENTRY_ID] + uri = call.data[ATTR_URI] + hass = call.hass + entry = hass.config_entries.async_get_entry(config_entry) + actions = entry.runtime_data.actions + return (await actions.get_playlist_details(uri)).to_dict() diff --git a/custom_components/mass_queue/services.yaml b/custom_components/mass_queue/services.yaml index 438b9ef..86c39dd 100644 --- a/custom_components/mass_queue/services.yaml +++ b/custom_components/mass_queue/services.yaml @@ -232,3 +232,105 @@ get_playlist_tracks: text: example: "library://playlist/12" required: true + page: + selector: + number: + min: 0 + max: 1000 + step: 1 + mode: box + required: false + example: 0 +get_album_tracks: + fields: + config_entry_id: + name: Config Entry ID + required: true + selector: + config_entry: + integration: mass_queue + uri: + name: Album URI + description: URI for the album + selector: + text: + example: "library://album/12" + required: true + page: + selector: + number: + min: 0 + max: 1000 + step: 1 + mode: box + required: false + example: 0 +get_artist_tracks: + fields: + config_entry_id: + name: Config Entry ID + required: true + selector: + config_entry: + integration: mass_queue + uri: + name: Artist URI + description: URI for the artist + selector: + text: + example: "library://artist/12" + required: true + page: + selector: + number: + min: 0 + max: 1000 + step: 1 + mode: box + required: false + example: 0 +get_album: + fields: + config_entry_id: + name: Config Entry ID + required: true + selector: + config_entry: + integration: mass_queue + uri: + name: Album URI + description: URI for the Album + selector: + text: + example: "library://album/12" + required: true +get_artist: + fields: + config_entry_id: + name: Config Entry ID + required: true + selector: + config_entry: + integration: mass_queue + uri: + name: Artist URI + description: URI for the artist + selector: + text: + example: "library://artist/12" + required: true +get_playlist: + fields: + config_entry_id: + name: Config Entry ID + required: true + selector: + config_entry: + integration: mass_queue + uri: + name: Playlist URI + description: URI for the playlist + selector: + text: + example: "library://playlist/12" + required: true diff --git a/custom_components/mass_queue/strings.json b/custom_components/mass_queue/strings.json index 7914a32..8ac262f 100644 --- a/custom_components/mass_queue/strings.json +++ b/custom_components/mass_queue/strings.json @@ -254,6 +254,88 @@ "uri": { "name": "Playlist URI", "description": "URI for the playlist." + }, + "page": { + "name": "Page", + "description": "Page of results to return. If not provided, returns all." + } + } + }, + "get_album_tracks": { + "name": "Get Album Tracks", + "description": "Returns all tracks for the album given.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "uri": { + "name": "Album URI", + "description": "URI for the album." + }, + "page": { + "name": "Page", + "description": "Page of results to return. If not provided, returns all." + } + } + }, + "get_artist_tracks": { + "name": "Get Artist Tracks", + "description": "Returns all tracks for the artist given.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "uri": { + "name": "Artist URI", + "description": "URI for the artist." + }, + "page": { + "name": "Page", + "description": "Page of results to return. If not provided, returns all." + } + } + }, + "get_playlist": { + "name": "Get Playlist", + "description": "Returns information about a playlist from the server.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "uri": { + "name": "Playlist URI", + "description": "URI for the playlist." + } + } + }, + "get_album": { + "name": "Get Album", + "description": "Returns information about an album from the server.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "uri": { + "name": "Album URI", + "description": "URI for the album." + } + } + }, + "get_artist": { + "name": "Get Artist", + "description": "Returns information about an artist from the server.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "uri": { + "name": "Artist URI", + "description": "URI for the artist." } } } diff --git a/custom_components/mass_queue/translations/en.json b/custom_components/mass_queue/translations/en.json index efb1b55..21f103c 100644 --- a/custom_components/mass_queue/translations/en.json +++ b/custom_components/mass_queue/translations/en.json @@ -230,6 +230,88 @@ "uri": { "name": "Playlist URI", "description": "URI for the playlist." + }, + "page": { + "name": "Page", + "description": "Page of results to return. If not provided, returns all." + } + } + }, + "get_album_tracks": { + "name": "Get Album Tracks", + "description": "Returns all tracks for the album given.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "uri": { + "name": "Album URI", + "description": "URI for the album." + }, + "page": { + "name": "Page", + "description": "Page of results to return. If not provided, returns all." + } + } + }, + "get_artist_tracks": { + "name": "Get Artist Tracks", + "description": "Returns all tracks for the artist given.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "uri": { + "name": "Artist URI", + "description": "URI for the artist." + }, + "page": { + "name": "Page", + "description": "Page of results to return. If not provided, returns all." + } + } + }, + "get_playlist": { + "name": "Get Playlist", + "description": "Returns information about a playlist from the server.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "uri": { + "name": "Playlist URI", + "description": "URI for the playlist." + } + } + }, + "get_album": { + "name": "Get Album", + "description": "Returns information about an album from the server.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "uri": { + "name": "Album URI", + "description": "URI for the album." + } + } + }, + "get_artist": { + "name": "Get Artist", + "description": "Returns information about an artist from the server.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "uri": { + "name": "Artist URI", + "description": "URI for the artist." } } } diff --git a/custom_components/mass_queue/websocket_commands.py b/custom_components/mass_queue/websocket_commands.py index 33e1eb1..4b10dd0 100644 --- a/custom_components/mass_queue/websocket_commands.py +++ b/custom_components/mass_queue/websocket_commands.py @@ -54,7 +54,6 @@ async def api_download_and_encode_image( """Download images and return them as b64 encoded.""" LOGGER.debug(f"Got message: {msg}") url = msg["url"] - LOGGER.debug(f"URL: {url}") result = await download_and_encode_image(url, hass) connection.send_result(msg["id"], result) @@ -76,8 +75,6 @@ async def api_download_images( LOGGER.debug(f"Received message: {msg}") session = aiohttp_client.async_get_clientsession(hass) images = msg["images"] - LOGGER.debug("Pulled images from message") - LOGGER.debug(images) result = [] entity_id = msg["entity_id"] for image in images: