diff --git a/custom_components/mass_queue/actions.py b/custom_components/mass_queue/actions.py index 8290260..1dd8bbf 100644 --- a/custom_components/mass_queue/actions.py +++ b/custom_components/mass_queue/actions.py @@ -40,6 +40,7 @@ ATTR_PROVIDERS, ATTR_QUEUE_ID, ATTR_QUEUE_ITEM_ID, + ATTR_RELEASE_DATE, ATTR_VOLUME_LEVEL, CONF_DOWNLOAD_LOCAL, DEFAULT_QUEUE_ITEMS_LIMIT, @@ -365,6 +366,12 @@ async def get_playlist_details(self, playlist_uri): LOGGER.debug(f"Getting album details for provider {provider}") return await self._client.music.get_playlist(item_id, provider) + async def get_podcast_details(self, podcast_uri): + """Retrieves the details for a podcast.""" + provider, item_id = parse_uri(podcast_uri) + LOGGER.debug(f"Getting podcast details for provider {provider}") + return await self._client.music.get_podcast(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) @@ -399,6 +406,17 @@ async def get_album_tracks(self, album_uri: str, page: int | None = None): ) return [self.format_track_item(item.to_dict()) for item in resp] + async def get_podcast_episodes(self, podcast_uri): + """Retrieves all episodes for a podcast.""" + provider, item_id = parse_uri(podcast_uri) + LOGGER.debug( + f"Getting podcast episodes for provider {provider}, item_id {item_id}", + ) + resp: list = await self._client.music.get_podcast_episodes(item_id, provider) + formatted = [self.format_podcast_episode(item.to_dict()) for item in resp] + formatted.sort(key=lambda x: x[ATTR_RELEASE_DATE], reverse=True) + return formatted + async def get_playlist_tracks(self, playlist_uri: str, page: int | None = None): """Retrieves all playlist items.""" provider, item_id = parse_uri(playlist_uri) @@ -418,25 +436,37 @@ def format_playlist_track(self, playlist_track: dict) -> TRACK_ITEM_SCHEMA: result[ATTR_POSITION] = playlist_track["position"] return result - 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" + def format_track_item(self, track_item: dict) -> TRACK_ITEM_SCHEMA: + """Process an individual track item.""" + result = self.format_item(track_item) + media_album = track_item.get("album") or "N/A" media_album_name = "" if media_album is None else media_album.get("name", "") - media_content_id = playlist_item["uri"] - 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"] + artists = track_item["artists"] artist_names = [artist["name"] for artist in artists] media_artist = ", ".join(artist_names) + result[ATTR_MEDIA_ALBUM_NAME] = media_album_name + result[ATTR_MEDIA_ARTIST] = media_artist + return result + + def format_podcast_episode(self, podcast_episode: dict) -> TRACK_ITEM_SCHEMA: + """Process an individual track item.""" + result = self.format_item(podcast_episode) + result[ATTR_RELEASE_DATE] = podcast_episode.get("metadata", {}).get( + "release_date", + ) + return result + + def format_item(self, media_item: dict) -> TRACK_ITEM_SCHEMA: + """Processes the individual items in a playlist.""" + media_title = media_item.get("name") or "N/A" + media_content_id = media_item["uri"] + media_image = find_image(media_item) or "" + local_image_encoded = media_item.get(ATTR_LOCAL_IMAGE_ENCODED) + favorite = media_item["favorite"] + duration = media_item["duration"] or 0 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, diff --git a/custom_components/mass_queue/const.py b/custom_components/mass_queue/const.py index 728cba3..51da7c4 100644 --- a/custom_components/mass_queue/const.py +++ b/custom_components/mass_queue/const.py @@ -17,6 +17,8 @@ SERVICE_GET_ARTIST_TRACKS = "get_artist_tracks" SERVICE_GET_PLAYLIST = "get_playlist" SERVICE_GET_PLAYLIST_TRACKS = "get_playlist_tracks" +SERVICE_GET_PODCAST = "get_podcast" +SERVICE_GET_PODCAST_EPISODES = "get_podcast_episodes" SERVICE_GET_QUEUE_ITEMS = "get_queue_items" SERVICE_GET_RECOMMENDATIONS = "get_recommendations" SERVICE_PLAY_QUEUE_ITEM = "play_queue_item" @@ -54,6 +56,7 @@ ATTR_QUEUE_ID = "active_queue" ATTR_QUEUE_ITEM_ID = "queue_item_id" ATTR_QUEUE_ITEMS = "queue_items" +ATTR_RELEASE_DATE = "release_date" ATTR_VOLUME_LEVEL = "volume_level" CONF_DOWNLOAD_LOCAL = "download_local" diff --git a/custom_components/mass_queue/icons.json b/custom_components/mass_queue/icons.json index d52c0a5..355d425 100644 --- a/custom_components/mass_queue/icons.json +++ b/custom_components/mass_queue/icons.json @@ -6,6 +6,8 @@ "get_artist_tracks": { "service": "mdi:account-music"}, "get_playlist": { "service": "mdi:playlist-music"}, "get_playlist_tracks": { "service": "mdi:playlist-music"}, + "get_podcast": { "service": "mdi:podcast"}, + "get_podcast_episodes": { "service": "mdi:podcast"}, "get_queue_items": { "service": "mdi:playlist-music" }, "move_queue_item_down": { "service": "mdi:arrow-down" }, "move_queue_item_next": { "service": "mdi:arrow-collapse-up" }, diff --git a/custom_components/mass_queue/manifest.json b/custom_components/mass_queue/manifest.json index 18e2904..e9b076d 100644 --- a/custom_components/mass_queue/manifest.json +++ b/custom_components/mass_queue/manifest.json @@ -11,6 +11,6 @@ "issue_tracker": "https://github.com/droans/mass_queue/issues", "requirements": ["music-assistant-client"], "ssdp": [], - "version": "0.9.2", + "version": "0.10.0-b.2", "zeroconf": ["_mass._tcp.local."] } diff --git a/custom_components/mass_queue/schemas.py b/custom_components/mass_queue/schemas.py index 7676063..90bc999 100644 --- a/custom_components/mass_queue/schemas.py +++ b/custom_components/mass_queue/schemas.py @@ -61,8 +61,8 @@ TRACK_ITEM_SCHEMA = vol.Schema( { vol.Required(ATTR_MEDIA_TITLE): str, - vol.Required(ATTR_MEDIA_ALBUM_NAME): str, - vol.Required(ATTR_MEDIA_ARTIST): str, + vol.Optional(ATTR_MEDIA_ALBUM_NAME): str, + vol.Optional(ATTR_MEDIA_ARTIST): str, vol.Required(ATTR_MEDIA_CONTENT_ID): str, vol.Required(ATTR_MEDIA_IMAGE): str, vol.Required(ATTR_FAVORITE): bool, @@ -137,6 +137,13 @@ }, ) +GET_PODCAST_EPISODES_SERVICE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): str, + vol.Required(ATTR_URI): str, + }, +) + GET_DATA_SERVICE_SCHEMA = vol.Schema( { vol.Required(ATTR_CONFIG_ENTRY_ID): str, diff --git a/custom_components/mass_queue/services.py b/custom_components/mass_queue/services.py index a47fbf6..dad0569 100644 --- a/custom_components/mass_queue/services.py +++ b/custom_components/mass_queue/services.py @@ -10,6 +10,7 @@ from .const import ( ATTR_CONFIG_ENTRY_ID, + ATTR_PAGE, ATTR_PLAYER_ENTITY, ATTR_PLAYLIST_ID, ATTR_POSITIONS_TO_REMOVE, @@ -25,6 +26,8 @@ SERVICE_GET_GROUP_VOLUME, SERVICE_GET_PLAYLIST, SERVICE_GET_PLAYLIST_TRACKS, + SERVICE_GET_PODCAST, + SERVICE_GET_PODCAST_EPISODES, SERVICE_GET_QUEUE_ITEMS, SERVICE_GET_RECOMMENDATIONS, SERVICE_MOVE_QUEUE_ITEM_DOWN, @@ -41,6 +44,7 @@ CLEAR_QUEUE_FROM_HERE_SERVICE_SCHEMA, GET_DATA_SERVICE_SCHEMA, GET_GROUP_VOLUME_SERVICE_SCHEMA, + GET_PODCAST_EPISODES_SERVICE_SCHEMA, GET_RECOMMENDATIONS_SERVICE_SCHEMA, GET_TRACKS_SERVICE_SCHEMA, MOVE_QUEUE_ITEM_DOWN_SERVICE_SCHEMA, @@ -165,6 +169,13 @@ def register_actions(hass) -> None: schema=GET_TRACKS_SERVICE_SCHEMA, supports_response=SupportsResponse.ONLY, ) + hass.services.async_register( + DOMAIN, + SERVICE_GET_PODCAST_EPISODES, + get_podcast_episodes, + schema=GET_PODCAST_EPISODES_SERVICE_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) hass.services.async_register( DOMAIN, SERVICE_GET_ALBUM, @@ -186,6 +197,13 @@ def register_actions(hass) -> None: schema=GET_DATA_SERVICE_SCHEMA, supports_response=SupportsResponse.ONLY, ) + hass.services.async_register( + DOMAIN, + SERVICE_GET_PODCAST, + get_podcast, + schema=GET_DATA_SERVICE_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) hass.services.async_register( DOMAIN, SERVICE_REMOVE_PLAYLIST_TRACKS, @@ -320,11 +338,12 @@ 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] + page = call.data.get(ATTR_PAGE) 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), + "tracks": await actions.get_album_tracks(uri, page), } @@ -344,11 +363,24 @@ async def get_playlist_tracks(call: ServiceCall): """Gets all tracks in a playlist.""" config_entry = call.data[ATTR_CONFIG_ENTRY_ID] uri = call.data[ATTR_URI] + page = call.data.get(ATTR_PAGE) hass = call.hass entry = hass.config_entries.async_get_entry(config_entry) actions = entry.runtime_data.actions return { - "tracks": await actions.get_playlist_tracks(uri), + "tracks": await actions.get_playlist_tracks(uri, page), + } + + +async def get_podcast_episodes(call: ServiceCall): + """Gets all episodes for a podcast.""" + 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 { + "episodes": await actions.get_podcast_episodes(uri), } @@ -382,6 +414,16 @@ async def get_playlist(call: ServiceCall): return (await actions.get_playlist_details(uri)).to_dict() +async def get_podcast(call: ServiceCall): + """Returns the details about a podcast 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_podcast_details(uri)).to_dict() + + async def remove_playlist_tracks(call: ServiceCall): """Removes one or more items from a playlist.""" config_entry = call.data[ATTR_CONFIG_ENTRY_ID] diff --git a/custom_components/mass_queue/services.yaml b/custom_components/mass_queue/services.yaml index b835174..d556809 100644 --- a/custom_components/mass_queue/services.yaml +++ b/custom_components/mass_queue/services.yaml @@ -289,6 +289,21 @@ get_artist_tracks: mode: box required: false example: 0 +get_podcast_episodes: + fields: + config_entry_id: + name: Config Entry ID + required: true + selector: + config_entry: + integration: mass_queue + uri: + name: Podcast URI + description: URI for the podcast + selector: + text: + example: "library://podcast/12" + required: true get_album: fields: config_entry_id: @@ -334,6 +349,21 @@ get_playlist: text: example: "library://playlist/12" required: true +get_podcast: + fields: + config_entry_id: + name: Config Entry ID + required: true + selector: + config_entry: + integration: mass_queue + uri: + name: Podcast URI + description: URI for the podcast + selector: + text: + example: "library://podcast/12" + required: true remove_playlist_tracks: fields: config_entry_id: diff --git a/custom_components/mass_queue/strings.json b/custom_components/mass_queue/strings.json index 20da226..23eaf74 100644 --- a/custom_components/mass_queue/strings.json +++ b/custom_components/mass_queue/strings.json @@ -297,6 +297,20 @@ } } }, + "get_podcast_episodes": { + "name": "Get Podcast Episodes", + "description": "Returns all episodes for the podcast given.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "uri": { + "name": "Podcast URI", + "description": "URI for the podcast." + } + } + }, "get_playlist": { "name": "Get Playlist", "description": "Returns information about a playlist from the server.", @@ -339,6 +353,20 @@ } } }, + "get_podcast": { + "name": "Get Podcast", + "description": "Returns information about a podcast from the server.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "uri": { + "name": "Podcast URI", + "description": "URI for the podcast." + } + } + }, "remove_playlist_tracks": { "name": "Remove Playlist Tracks", "description": "Removes one or more tracks from a playlist based on position.", diff --git a/custom_components/mass_queue/translations/en.json b/custom_components/mass_queue/translations/en.json index fe25692..1e04102 100644 --- a/custom_components/mass_queue/translations/en.json +++ b/custom_components/mass_queue/translations/en.json @@ -273,6 +273,20 @@ } } }, + "get_podcast_episodes": { + "name": "Get Podcast Episodes", + "description": "Returns all episodes for the podcast given.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "uri": { + "name": "Podcast URI", + "description": "URI for the podcast." + } + } + }, "get_playlist": { "name": "Get Playlist", "description": "Returns information about a playlist from the server.", @@ -315,6 +329,20 @@ } } }, + "get_podcast": { + "name": "Get Podcast", + "description": "Returns information about a podcast from the server.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "uri": { + "name": "Podcast URI", + "description": "URI for the podcast." + } + } + }, "remove_playlist_tracks": { "name": "Remove Playlist Tracks", "description": "Removes one or more tracks from a playlist based on position.",