diff --git a/custom_components/mass_queue/actions.py b/custom_components/mass_queue/actions.py index 6840521..1dd8bbf 100644 --- a/custom_components/mass_queue/actions.py +++ b/custom_components/mass_queue/actions.py @@ -36,9 +36,11 @@ ATTR_MEDIA_TITLE, ATTR_OFFSET, ATTR_PLAYER_ENTITY, + ATTR_POSITION, ATTR_PROVIDERS, ATTR_QUEUE_ID, ATTR_QUEUE_ITEM_ID, + ATTR_RELEASE_DATE, ATTR_VOLUME_LEVEL, CONF_DOWNLOAD_LOCAL, DEFAULT_QUEUE_ITEMS_LIMIT, @@ -364,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) @@ -398,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) @@ -409,27 +428,45 @@ async def get_playlist_tracks(self, playlist_uri: str, page: int | None = None): 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" + return [self.format_playlist_track(item.to_dict()) for item in resp] + + def format_playlist_track(self, playlist_track: dict) -> TRACK_ITEM_SCHEMA: + """Processes individual playlist tracks using format_track_item and adds position.""" + result = self.format_track_item(playlist_track) + result[ATTR_POSITION] = playlist_track["position"] + return result + + 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, @@ -440,6 +477,17 @@ def format_track_item(self, playlist_item: dict) -> TRACK_ITEM_SCHEMA: response[ATTR_LOCAL_IMAGE_ENCODED] = local_image_encoded return response + async def remove_playlist_tracks( + self, + playlist_id: str | int, + positions_to_remove: list[int], + ): + """Removes one or more items from a playlist.""" + await self._client.music.remove_playlist_tracks( + playlist_id, + positions_to_remove, + ) + @callback def get_music_assistant_client( diff --git a/custom_components/mass_queue/const.py b/custom_components/mass_queue/const.py index 1d0ae11..51da7c4 100644 --- a/custom_components/mass_queue/const.py +++ b/custom_components/mass_queue/const.py @@ -17,12 +17,15 @@ 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" SERVICE_MOVE_QUEUE_ITEM_DOWN = "move_queue_item_down" SERVICE_MOVE_QUEUE_ITEM_NEXT = "move_queue_item_next" SERVICE_MOVE_QUEUE_ITEM_UP = "move_queue_item_up" +SERVICE_REMOVE_PLAYLIST_TRACKS = "remove_playlist_tracks" SERVICE_REMOVE_QUEUE_ITEM = "remove_queue_item" SERVICE_SEND_COMMAND = "send_command" SERVICE_SET_GROUP_VOLUME = "set_group_volume" @@ -45,11 +48,15 @@ ATTR_OFFSET = "offset" ATTR_PAGE = "page" ATTR_PLAYER_ENTITY = "entity" +ATTR_PLAYLIST_ID = "playlist_id" ATTR_URI = "uri" +ATTR_POSITION = "position" +ATTR_POSITIONS_TO_REMOVE = "positions_to_remove" ATTR_PROVIDERS = "providers" 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 da0e38b..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" }, @@ -17,6 +19,7 @@ "get_recommendations": { "service": "mdi:creation"}, "get_group_volume": { "service": "mdi:speaker-multiple"}, "set_group_volume": { "service": "mdi:speaker-multiple"}, - "clear_queue_from_here": { "service": "mdi:playlist-remove"} + "clear_queue_from_here": { "service": "mdi:playlist-remove"}, + "remove_playlist_tracks": { "service": "mdi:playlist-remove"} } } 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 815e8d5..90bc999 100644 --- a/custom_components/mass_queue/schemas.py +++ b/custom_components/mass_queue/schemas.py @@ -23,6 +23,9 @@ ATTR_OFFSET, ATTR_PAGE, ATTR_PLAYER_ENTITY, + ATTR_PLAYLIST_ID, + ATTR_POSITION, + ATTR_POSITIONS_TO_REMOVE, ATTR_PROVIDERS, ATTR_QUEUE_ITEM_ID, ATTR_QUEUE_ITEMS, @@ -58,13 +61,14 @@ 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, vol.Required(ATTR_DURATION): vol.Any(int, None), vol.Optional(ATTR_LOCAL_IMAGE_ENCODED): str, + vol.Optional(ATTR_POSITION): str, }, ) @@ -133,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, @@ -159,3 +170,11 @@ vol.Required(ATTR_VOLUME_LEVEL): int, }, ) + +REMOVE_PLAYLIST_TRACKS_SERVICE_SCHEMA = vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY_ID): str, + vol.Required(ATTR_PLAYLIST_ID): vol.Any(int, str), + vol.Required(ATTR_POSITIONS_TO_REMOVE): vol.Any(int, [int], str, [str]), + }, +) diff --git a/custom_components/mass_queue/services.py b/custom_components/mass_queue/services.py index 8489d61..dad0569 100644 --- a/custom_components/mass_queue/services.py +++ b/custom_components/mass_queue/services.py @@ -10,7 +10,10 @@ from .const import ( ATTR_CONFIG_ENTRY_ID, + ATTR_PAGE, ATTR_PLAYER_ENTITY, + ATTR_PLAYLIST_ID, + ATTR_POSITIONS_TO_REMOVE, ATTR_QUEUE_ITEM_ID, ATTR_URI, DOMAIN, @@ -23,12 +26,15 @@ 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, SERVICE_MOVE_QUEUE_ITEM_NEXT, SERVICE_MOVE_QUEUE_ITEM_UP, SERVICE_PLAY_QUEUE_ITEM, + SERVICE_REMOVE_PLAYLIST_TRACKS, SERVICE_REMOVE_QUEUE_ITEM, SERVICE_SEND_COMMAND, SERVICE_SET_GROUP_VOLUME, @@ -38,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, @@ -45,6 +52,7 @@ MOVE_QUEUE_ITEM_UP_SERVICE_SCHEMA, PLAY_QUEUE_ITEM_SERVICE_SCHEMA, QUEUE_ITEMS_SERVICE_SCHEMA, + REMOVE_PLAYLIST_TRACKS_SERVICE_SCHEMA, REMOVE_QUEUE_ITEM_SERVICE_SCHEMA, SEND_COMMAND_SERVICE_SCHEMA, SET_GROUP_VOLUME_SERVICE_SCHEMA, @@ -161,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, @@ -182,6 +197,20 @@ 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, + remove_playlist_tracks, + schema=REMOVE_PLAYLIST_TRACKS_SERVICE_SCHEMA, + supports_response=SupportsResponse.NONE, + ) async def get_queue_items(call: ServiceCall): @@ -309,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), } @@ -333,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), } @@ -369,3 +412,27 @@ async def get_playlist(call: ServiceCall): entry = hass.config_entries.async_get_entry(config_entry) actions = entry.runtime_data.actions 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] + playlist = call.data[ATTR_PLAYLIST_ID] + positions = call.data[ATTR_POSITIONS_TO_REMOVE] + if isinstance(positions, int): + positions = [positions] + positions = [int(position) for position in positions] + hass = call.hass + entry = hass.config_entries.async_get_entry(config_entry) + actions = entry.runtime_data.actions + await actions.remove_playlist_tracks(playlist, positions) diff --git a/custom_components/mass_queue/services.yaml b/custom_components/mass_queue/services.yaml index 86c39dd..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,3 +349,35 @@ 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: + name: Config Entry ID + required: true + selector: + config_entry: + integration: mass_queue + playlist_id: + required: true + selector: + text: + positions_to_remove: + required: true + selector: + text: + multiple: true diff --git a/custom_components/mass_queue/strings.json b/custom_components/mass_queue/strings.json index 8ac262f..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.", @@ -338,6 +352,38 @@ "description": "URI for the artist." } } + }, + "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.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "playlist_id": { + "name": "Playlist ID", + "description": "ID of the playlist." + }, + "positions_to_remove": { + "name": "Positions to remove", + "description": "Position(s) of items to remove from the playlist." + } + } } } } diff --git a/custom_components/mass_queue/translations/en.json b/custom_components/mass_queue/translations/en.json index 21f103c..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.", @@ -314,6 +328,38 @@ "description": "URI for the artist." } } + }, + "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.", + "fields": { + "config_entry_id": { + "name": "Config Entry ID", + "description": "Config Entry ID for the Music Assistant Queue Items instance." + }, + "playlist_id": { + "name": "Playlist ID", + "description": "ID of the playlist." + }, + "positions_to_remove": { + "name": "Positions to remove", + "description": "Position(s) of items to remove from the playlist." + } + } } } }