From bcbaf9238a74b20c97686bee1f388f2d8071ef08 Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Wed, 7 Jan 2026 12:02:02 -0500 Subject: [PATCH 1/5] Add `get_podcast` and `get_podcast_episodes` --- custom_components/mass_queue/actions.py | 49 ++++++++++++++----- custom_components/mass_queue/const.py | 2 + custom_components/mass_queue/icons.json | 2 + custom_components/mass_queue/manifest.json | 2 +- custom_components/mass_queue/schemas.py | 11 ++++- custom_components/mass_queue/services.py | 46 ++++++++++++++++- custom_components/mass_queue/services.yaml | 15 ++++++ custom_components/mass_queue/strings.json | 28 +++++++++++ .../mass_queue/translations/en.json | 28 +++++++++++ 9 files changed, 165 insertions(+), 18 deletions(-) diff --git a/custom_components/mass_queue/actions.py b/custom_components/mass_queue/actions.py index 8290260..d0a31d3 100644 --- a/custom_components/mass_queue/actions.py +++ b/custom_components/mass_queue/actions.py @@ -365,6 +365,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 +405,15 @@ 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 = await self._client.music.get_podcast_episodes(item_id, provider) + return [self.format_podcast_episode(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) @@ -418,25 +433,33 @@ 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_track_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.""" + return self.format_item(podcast_episode) + + 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..53ece58 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" 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..a2c1341 100644 --- a/custom_components/mass_queue/services.yaml +++ b/custom_components/mass_queue/services.yaml @@ -334,6 +334,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.", From ecdd25bcc4cf6b49056d3afa3c0ae16361062290 Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Wed, 7 Jan 2026 13:44:32 -0500 Subject: [PATCH 2/5] Add `get_podcast_tracks` to services.yaml --- custom_components/mass_queue/services.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/custom_components/mass_queue/services.yaml b/custom_components/mass_queue/services.yaml index a2c1341..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: From fdd30e107ae4262e0131ab985c17cd25dfceb2a6 Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Wed, 7 Jan 2026 14:02:05 -0500 Subject: [PATCH 3/5] Add release date --- custom_components/mass_queue/actions.py | 13 ++++++++++--- custom_components/mass_queue/const.py | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/custom_components/mass_queue/actions.py b/custom_components/mass_queue/actions.py index d0a31d3..30bf6eb 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, @@ -411,8 +412,10 @@ async def get_podcast_episodes(self, podcast_uri): LOGGER.debug( f"Getting podcast episodes for provider {provider}, item_id {item_id}", ) - resp = await self._client.music.get_podcast_episodes(item_id, provider) - return [self.format_podcast_episode(item.to_dict()) for item in resp] + resp: list = await self._client.music.get_podcast_episodes(item_id, provider) + formatted = [self.format_playlist_track(item.to_dict()) for item in resp] + formatted.sort(key=lambda x: x.release_date) + return formatted async def get_playlist_tracks(self, playlist_uri: str, page: int | None = None): """Retrieves all playlist items.""" @@ -447,7 +450,11 @@ def format_track_item(self, track_item: dict) -> TRACK_ITEM_SCHEMA: def format_podcast_episode(self, podcast_episode: dict) -> TRACK_ITEM_SCHEMA: """Process an individual track item.""" - return self.format_item(podcast_episode) + 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.""" diff --git a/custom_components/mass_queue/const.py b/custom_components/mass_queue/const.py index 53ece58..51da7c4 100644 --- a/custom_components/mass_queue/const.py +++ b/custom_components/mass_queue/const.py @@ -56,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" From e86db19e344a793c7f950b43c6f2bcffc732398e Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Thu, 8 Jan 2026 09:07:02 -0500 Subject: [PATCH 4/5] Fix: Use correct functions --- custom_components/mass_queue/actions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/mass_queue/actions.py b/custom_components/mass_queue/actions.py index 30bf6eb..92af01a 100644 --- a/custom_components/mass_queue/actions.py +++ b/custom_components/mass_queue/actions.py @@ -413,7 +413,7 @@ async def get_podcast_episodes(self, podcast_uri): 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_playlist_track(item.to_dict()) for item in resp] + formatted = [self.format_podcast_episode(item.to_dict()) for item in resp] formatted.sort(key=lambda x: x.release_date) return formatted @@ -438,7 +438,7 @@ def format_playlist_track(self, playlist_track: dict) -> TRACK_ITEM_SCHEMA: def format_track_item(self, track_item: dict) -> TRACK_ITEM_SCHEMA: """Process an individual track item.""" - result = self.format_track_item(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", "") artists = track_item["artists"] From e19c79510214f79fda381f2c16732d528043b8f7 Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Thu, 8 Jan 2026 09:07:22 -0500 Subject: [PATCH 5/5] Fix: Correct key --- custom_components/mass_queue/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/mass_queue/actions.py b/custom_components/mass_queue/actions.py index 92af01a..1dd8bbf 100644 --- a/custom_components/mass_queue/actions.py +++ b/custom_components/mass_queue/actions.py @@ -414,7 +414,7 @@ async def get_podcast_episodes(self, podcast_uri): ) 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.release_date) + 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):