Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 63 additions & 15 deletions custom_components/mass_queue/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand All @@ -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(
Expand Down
7 changes: 7 additions & 0 deletions custom_components/mass_queue/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
5 changes: 4 additions & 1 deletion custom_components/mass_queue/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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"}
}
}
2 changes: 1 addition & 1 deletion custom_components/mass_queue/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."]
}
23 changes: 21 additions & 2 deletions custom_components/mass_queue/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
},
)

Expand Down Expand Up @@ -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,
Expand All @@ -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]),
},
)
71 changes: 69 additions & 2 deletions custom_components/mass_queue/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -38,13 +44,15 @@
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,
MOVE_QUEUE_ITEM_NEXT_SERVICE_SCHEMA,
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,
Expand Down Expand Up @@ -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,
Expand All @@ -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):
Expand Down Expand Up @@ -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),
}


Expand All @@ -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),
}


Expand Down Expand Up @@ -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)
Loading
Loading