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
82 changes: 67 additions & 15 deletions custom_components/mass_queue/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand All @@ -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,
},
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 @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand Down
8 changes: 6 additions & 2 deletions custom_components/mass_queue/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down Expand Up @@ -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):
Expand Down
5 changes: 5 additions & 0 deletions custom_components/mass_queue/icons.json
Original file line number Diff line number Diff line change
@@ -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" },
Expand Down
15 changes: 13 additions & 2 deletions custom_components/mass_queue/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
ATTR_COMMAND,
ATTR_CONFIG_ENTRY_ID,
ATTR_DATA,
ATTR_DURATION,
ATTR_FAVORITE,
ATTR_LIMIT,
ATTR_LIMIT_AFTER,
Expand All @@ -20,6 +21,7 @@
ATTR_MEDIA_IMAGE,
ATTR_MEDIA_TITLE,
ATTR_OFFSET,
ATTR_PAGE,
ATTR_PLAYER_ENTITY,
ATTR_PROVIDERS,
ATTR_QUEUE_ITEM_ID,
Expand Down Expand Up @@ -53,14 +55,15 @@
},
)

PLAYLIST_ITEM_SCHEMA = vol.Schema(
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.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,
},
)
Expand Down Expand Up @@ -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,
Expand Down
101 changes: 98 additions & 3 deletions custom_components/mass_queue/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
)

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