Skip to content

Commit d3bac6c

Browse files
authored
Merge pull request #88 from droans/dev
v0.10.0-b.1
2 parents 11f4278 + 20327c2 commit d3bac6c

10 files changed

Lines changed: 462 additions & 25 deletions

File tree

custom_components/mass_queue/actions.py

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717
from music_assistant_models.errors import (
1818
InvalidCommand,
1919
MediaNotFoundError,
20+
ProviderUnavailableError,
2021
)
2122

2223
from .const import (
2324
ATTR_COMMAND,
2425
ATTR_DATA,
26+
ATTR_DURATION,
2527
ATTR_FAVORITE,
2628
ATTR_LIMIT,
2729
ATTR_LIMIT_AFTER,
@@ -62,12 +64,12 @@
6264
MOVE_QUEUE_ITEM_NEXT_SERVICE_SCHEMA,
6365
MOVE_QUEUE_ITEM_UP_SERVICE_SCHEMA,
6466
PLAY_QUEUE_ITEM_SERVICE_SCHEMA,
65-
PLAYLIST_ITEM_SCHEMA,
6667
QUEUE_ITEM_SCHEMA,
6768
QUEUE_ITEMS_SERVICE_SCHEMA,
6869
REMOVE_QUEUE_ITEM_SERVICE_SCHEMA,
6970
SEND_COMMAND_SERVICE_SCHEMA,
7071
SET_GROUP_VOLUME_SERVICE_SCHEMA,
72+
TRACK_ITEM_SCHEMA,
7173
)
7274
from .utils import (
7375
find_image,
@@ -196,7 +198,6 @@ async def get_active_queue(self, entity_id: str):
196198

197199
async def _format_queue_item(self, queue_item: dict) -> dict:
198200
"""Format list of queue items for response."""
199-
LOGGER.debug(f"Got queue item with keys {queue_item.keys()}")
200201
media = queue_item["media_item"]
201202

202203
queue_item_id = queue_item["queue_item_id"]
@@ -224,7 +225,6 @@ async def _format_queue_item(self, queue_item: dict) -> dict:
224225
)
225226
if local_image_encoded:
226227
response[ATTR_LOCAL_IMAGE_ENCODED] = local_image_encoded
227-
LOGGER.debug(f"Sending back response with keys {response.keys()}")
228228
return response
229229

230230
async def send_command(self, call: ServiceCall) -> ServiceResponse:
@@ -346,22 +346,72 @@ async def unfavorite_item(self, call: ServiceCall) -> ServiceResponse:
346346
library_item_id=item_id,
347347
)
348348

349-
async def get_playlist_items(self, playlist_uri: str):
349+
async def get_artist_details(self, artist_uri):
350+
"""Retrieves the details for an artist."""
351+
provider, item_id = parse_uri(artist_uri)
352+
LOGGER.debug(f"Getting artist details for provider {provider}")
353+
return await self._client.music.get_artist(item_id, provider)
354+
355+
async def get_album_details(self, album_uri):
356+
"""Retrieves the details for an album."""
357+
provider, item_id = parse_uri(album_uri)
358+
LOGGER.debug(f"Getting album details for provider {provider}")
359+
return await self._client.music.get_album(item_id, provider)
360+
361+
async def get_playlist_details(self, playlist_uri):
362+
"""Retrieves the details for a playlist."""
363+
provider, item_id = parse_uri(playlist_uri)
364+
LOGGER.debug(f"Getting album details for provider {provider}")
365+
return await self._client.music.get_playlist(item_id, provider)
366+
367+
async def get_artist_tracks(self, artist_uri: str, page: int | None = None):
368+
"""Retrieves a limited number of tracks from an artist."""
369+
details = await self.get_artist_details(artist_uri)
370+
mappings = list(details.provider_mappings)
371+
if not len(mappings) > 0:
372+
msg = f"URI {artist_uri} returned no results!"
373+
raise ProviderUnavailableError(msg)
374+
mapping = mappings[0]
375+
item_id = mapping.item_id
376+
provider = mapping.provider_domain
377+
resp = (
378+
await self._client.music.get_artist_tracks(item_id, provider)
379+
if not page
380+
else await self._client.music.get_artist_tracks(item_id, provider, page)
381+
)
382+
return [self.format_track_item(item.to_dict()) for item in resp]
383+
384+
async def get_album_tracks(self, album_uri: str, page: int | None = None):
385+
"""Retrieves all tracks from an album."""
386+
details = await self.get_album_details(album_uri)
387+
mappings = list(details.provider_mappings)
388+
if not len(mappings) > 0:
389+
msg = f"URI {album_uri} returned no results!"
390+
raise ProviderUnavailableError(msg)
391+
mapping = mappings[0]
392+
item_id = mapping.item_id
393+
provider = mapping.provider_domain
394+
resp = (
395+
await self._client.music.get_album_tracks(item_id, provider)
396+
if not page
397+
else await self._client.music.get_album_tracks(item_id, provider, page)
398+
)
399+
return [self.format_track_item(item.to_dict()) for item in resp]
400+
401+
async def get_playlist_tracks(self, playlist_uri: str, page: int | None = None):
350402
"""Retrieves all playlist items."""
351403
provider, item_id = parse_uri(playlist_uri)
352404
LOGGER.debug(
353405
f"Getting playlist items for provider {provider}, item_id {item_id}",
354406
)
355-
resp = await self._client.music.get_playlist_tracks(item_id, provider)
356-
LOGGER.debug(f"Got response with {len(resp) if resp else 0} items")
357-
result = [self.format_playlist_item(item.to_dict()) for item in resp]
358-
msg = f"Got response {result[0]}"
359-
if len(msg) > 200:
360-
msg = f"{msg[180]}..." + "}"
361-
LOGGER.debug(msg)
362-
return result
363-
364-
def format_playlist_item(self, playlist_item: dict) -> dict:
407+
resp = (
408+
await self._client.music.get_playlist_tracks(item_id, provider)
409+
if not page
410+
else await self._client.music.get_playlist_tracks(item_id, provider, page)
411+
)
412+
return [self.format_track_item(item.to_dict()) for item in resp]
413+
414+
def format_track_item(self, playlist_item: dict) -> TRACK_ITEM_SCHEMA:
365415
"""Processes the individual items in a playlist."""
366416
media_title = playlist_item.get("name") or "N/A"
367417
media_album = playlist_item.get("album") or "N/A"
@@ -370,16 +420,18 @@ def format_playlist_item(self, playlist_item: dict) -> dict:
370420
media_image = find_image(playlist_item) or ""
371421
local_image_encoded = playlist_item.get(ATTR_LOCAL_IMAGE_ENCODED)
372422
favorite = playlist_item["favorite"]
423+
duration = playlist_item["duration"] or 0
373424

374425
artists = playlist_item["artists"]
375426
artist_names = [artist["name"] for artist in artists]
376427
media_artist = ", ".join(artist_names)
377-
response: ServiceResponse = PLAYLIST_ITEM_SCHEMA(
428+
response: ServiceResponse = TRACK_ITEM_SCHEMA(
378429
{
379430
ATTR_MEDIA_TITLE: media_title,
380431
ATTR_MEDIA_ALBUM_NAME: media_album_name,
381432
ATTR_MEDIA_ARTIST: media_artist,
382433
ATTR_MEDIA_CONTENT_ID: media_content_id,
434+
ATTR_DURATION: duration,
383435
ATTR_MEDIA_IMAGE: media_image,
384436
ATTR_FAVORITE: favorite,
385437
},

custom_components/mass_queue/const.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
DEFAULT_NAME = "Music Assistant Queue Items"
1212
SERVICE_CLEAR_QUEUE_FROM_HERE = "clear_queue_from_here"
1313
SERVICE_GET_GROUP_VOLUME = "get_group_volume"
14+
SERVICE_GET_ALBUM = "get_album"
15+
SERVICE_GET_ALBUM_TRACKS = "get_album_tracks"
16+
SERVICE_GET_ARTIST = "get_artist"
17+
SERVICE_GET_ARTIST_TRACKS = "get_artist_tracks"
18+
SERVICE_GET_PLAYLIST = "get_playlist"
1419
SERVICE_GET_PLAYLIST_TRACKS = "get_playlist_tracks"
1520
SERVICE_GET_QUEUE_ITEMS = "get_queue_items"
1621
SERVICE_GET_RECOMMENDATIONS = "get_recommendations"
@@ -26,6 +31,7 @@
2631
ATTR_COMMAND = "command"
2732
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
2833
ATTR_DATA = "data"
34+
ATTR_DURATION = "duration"
2935
ATTR_FAVORITE = "favorite"
3036
ATTR_LIMIT = "limit"
3137
ATTR_LIMIT_AFTER = "limit_after"
@@ -37,6 +43,7 @@
3743
ATTR_MEDIA_IMAGE = "media_image"
3844
ATTR_MEDIA_TITLE = "media_title"
3945
ATTR_OFFSET = "offset"
46+
ATTR_PAGE = "page"
4047
ATTR_PLAYER_ENTITY = "entity"
4148
ATTR_URI = "uri"
4249
ATTR_PROVIDERS = "providers"

custom_components/mass_queue/controller.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,9 @@ def update(self, player_id: str, queue_id: str):
304304

305305
def send_ha_event(self, event_data):
306306
"""Send event to Home Assistant."""
307-
LOGGER.debug(f"Sending event type {MASS_QUEUE_EVENT_DOMAIN}, data {event_data}")
307+
LOGGER.debug(
308+
f"Sending event type {MASS_QUEUE_EVENT_DOMAIN}, data {event_data}",
309+
)
308310
self._hass.bus.async_fire(MASS_QUEUE_EVENT_DOMAIN, event_data)
309311

310312

@@ -358,7 +360,9 @@ def remove(self, queue_id):
358360

359361
def send_ha_event(self, event_data):
360362
"""Send event to Home Assistant."""
361-
LOGGER.debug(f"Sending event type {MASS_QUEUE_EVENT_DOMAIN}, data {event_data}")
363+
LOGGER.debug(
364+
f"Sending event type {MASS_QUEUE_EVENT_DOMAIN}, data {event_data}",
365+
)
362366
self._hass.bus.async_fire(MASS_QUEUE_EVENT_DOMAIN, event_data)
363367

364368
async def process_image_single_item(self, queue_item: dict):

custom_components/mass_queue/icons.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
{
22
"services": {
3+
"get_album": { "service": "mdi:album"},
4+
"get_album_tracks": { "service": "mdi:album"},
5+
"get_artist": { "service": "mdi:account-music"},
6+
"get_artist_tracks": { "service": "mdi:account-music"},
7+
"get_playlist": { "service": "mdi:playlist-music"},
38
"get_playlist_tracks": { "service": "mdi:playlist-music"},
49
"get_queue_items": { "service": "mdi:playlist-music" },
510
"move_queue_item_down": { "service": "mdi:arrow-down" },

custom_components/mass_queue/schemas.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
ATTR_COMMAND,
1010
ATTR_CONFIG_ENTRY_ID,
1111
ATTR_DATA,
12+
ATTR_DURATION,
1213
ATTR_FAVORITE,
1314
ATTR_LIMIT,
1415
ATTR_LIMIT_AFTER,
@@ -20,6 +21,7 @@
2021
ATTR_MEDIA_IMAGE,
2122
ATTR_MEDIA_TITLE,
2223
ATTR_OFFSET,
24+
ATTR_PAGE,
2325
ATTR_PLAYER_ENTITY,
2426
ATTR_PROVIDERS,
2527
ATTR_QUEUE_ITEM_ID,
@@ -53,14 +55,15 @@
5355
},
5456
)
5557

56-
PLAYLIST_ITEM_SCHEMA = vol.Schema(
58+
TRACK_ITEM_SCHEMA = vol.Schema(
5759
{
5860
vol.Required(ATTR_MEDIA_TITLE): str,
5961
vol.Required(ATTR_MEDIA_ALBUM_NAME): str,
6062
vol.Required(ATTR_MEDIA_ARTIST): str,
6163
vol.Required(ATTR_MEDIA_CONTENT_ID): str,
6264
vol.Required(ATTR_MEDIA_IMAGE): str,
6365
vol.Required(ATTR_FAVORITE): bool,
66+
vol.Required(ATTR_DURATION): vol.Any(int, None),
6467
vol.Optional(ATTR_LOCAL_IMAGE_ENCODED): str,
6568
},
6669
)
@@ -122,7 +125,15 @@
122125
},
123126
)
124127

125-
GET_PLAYLIST_TRACKS_SERVICE_SCHEMA = vol.Schema(
128+
GET_TRACKS_SERVICE_SCHEMA = vol.Schema(
129+
{
130+
vol.Required(ATTR_CONFIG_ENTRY_ID): str,
131+
vol.Required(ATTR_URI): str,
132+
vol.Optional(ATTR_PAGE): int,
133+
},
134+
)
135+
136+
GET_DATA_SERVICE_SCHEMA = vol.Schema(
126137
{
127138
vol.Required(ATTR_CONFIG_ENTRY_ID): str,
128139
vol.Required(ATTR_URI): str,

custom_components/mass_queue/services.py

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
DOMAIN,
1717
LOGGER,
1818
SERVICE_CLEAR_QUEUE_FROM_HERE,
19+
SERVICE_GET_ALBUM,
20+
SERVICE_GET_ALBUM_TRACKS,
21+
SERVICE_GET_ARTIST,
22+
SERVICE_GET_ARTIST_TRACKS,
1923
SERVICE_GET_GROUP_VOLUME,
24+
SERVICE_GET_PLAYLIST,
2025
SERVICE_GET_PLAYLIST_TRACKS,
2126
SERVICE_GET_QUEUE_ITEMS,
2227
SERVICE_GET_RECOMMENDATIONS,
@@ -31,9 +36,10 @@
3136
)
3237
from .schemas import (
3338
CLEAR_QUEUE_FROM_HERE_SERVICE_SCHEMA,
39+
GET_DATA_SERVICE_SCHEMA,
3440
GET_GROUP_VOLUME_SERVICE_SCHEMA,
35-
GET_PLAYLIST_TRACKS_SERVICE_SCHEMA,
3641
GET_RECOMMENDATIONS_SERVICE_SCHEMA,
42+
GET_TRACKS_SERVICE_SCHEMA,
3743
MOVE_QUEUE_ITEM_DOWN_SERVICE_SCHEMA,
3844
MOVE_QUEUE_ITEM_NEXT_SERVICE_SCHEMA,
3945
MOVE_QUEUE_ITEM_UP_SERVICE_SCHEMA,
@@ -138,7 +144,42 @@ def register_actions(hass) -> None:
138144
DOMAIN,
139145
SERVICE_GET_PLAYLIST_TRACKS,
140146
get_playlist_tracks,
141-
schema=GET_PLAYLIST_TRACKS_SERVICE_SCHEMA,
147+
schema=GET_TRACKS_SERVICE_SCHEMA,
148+
supports_response=SupportsResponse.ONLY,
149+
)
150+
hass.services.async_register(
151+
DOMAIN,
152+
SERVICE_GET_ALBUM_TRACKS,
153+
get_album_tracks,
154+
schema=GET_TRACKS_SERVICE_SCHEMA,
155+
supports_response=SupportsResponse.ONLY,
156+
)
157+
hass.services.async_register(
158+
DOMAIN,
159+
SERVICE_GET_ARTIST_TRACKS,
160+
get_artist_tracks,
161+
schema=GET_TRACKS_SERVICE_SCHEMA,
162+
supports_response=SupportsResponse.ONLY,
163+
)
164+
hass.services.async_register(
165+
DOMAIN,
166+
SERVICE_GET_ALBUM,
167+
get_album,
168+
schema=GET_DATA_SERVICE_SCHEMA,
169+
supports_response=SupportsResponse.ONLY,
170+
)
171+
hass.services.async_register(
172+
DOMAIN,
173+
SERVICE_GET_ARTIST,
174+
get_artist,
175+
schema=GET_DATA_SERVICE_SCHEMA,
176+
supports_response=SupportsResponse.ONLY,
177+
)
178+
hass.services.async_register(
179+
DOMAIN,
180+
SERVICE_GET_PLAYLIST,
181+
get_playlist,
182+
schema=GET_DATA_SERVICE_SCHEMA,
142183
supports_response=SupportsResponse.ONLY,
143184
)
144185

@@ -264,6 +305,30 @@ async def clear_queue_from_here(call: ServiceCall):
264305
await client.player_queues.queue_command_delete(queue_id, queue_item_id)
265306

266307

308+
async def get_album_tracks(call: ServiceCall):
309+
"""Gets all tracks in an album."""
310+
config_entry = call.data[ATTR_CONFIG_ENTRY_ID]
311+
uri = call.data[ATTR_URI]
312+
hass = call.hass
313+
entry = hass.config_entries.async_get_entry(config_entry)
314+
actions = entry.runtime_data.actions
315+
return {
316+
"tracks": await actions.get_album_tracks(uri),
317+
}
318+
319+
320+
async def get_artist_tracks(call: ServiceCall):
321+
"""Gets all tracks for an artist."""
322+
config_entry = call.data[ATTR_CONFIG_ENTRY_ID]
323+
uri = call.data[ATTR_URI]
324+
hass = call.hass
325+
entry = hass.config_entries.async_get_entry(config_entry)
326+
actions = entry.runtime_data.actions
327+
return {
328+
"tracks": await actions.get_artist_tracks(uri),
329+
}
330+
331+
267332
async def get_playlist_tracks(call: ServiceCall):
268333
"""Gets all tracks in a playlist."""
269334
config_entry = call.data[ATTR_CONFIG_ENTRY_ID]
@@ -272,5 +337,35 @@ async def get_playlist_tracks(call: ServiceCall):
272337
entry = hass.config_entries.async_get_entry(config_entry)
273338
actions = entry.runtime_data.actions
274339
return {
275-
"tracks": await actions.get_playlist_items(uri),
340+
"tracks": await actions.get_playlist_tracks(uri),
276341
}
342+
343+
344+
async def get_album(call: ServiceCall):
345+
"""Returns the details about an album from the server."""
346+
config_entry = call.data[ATTR_CONFIG_ENTRY_ID]
347+
uri = call.data[ATTR_URI]
348+
hass = call.hass
349+
entry = hass.config_entries.async_get_entry(config_entry)
350+
actions = entry.runtime_data.actions
351+
return (await actions.get_album_details(uri)).to_dict()
352+
353+
354+
async def get_artist(call: ServiceCall):
355+
"""Returns the details about an artist from the server."""
356+
config_entry = call.data[ATTR_CONFIG_ENTRY_ID]
357+
uri = call.data[ATTR_URI]
358+
hass = call.hass
359+
entry = hass.config_entries.async_get_entry(config_entry)
360+
actions = entry.runtime_data.actions
361+
return (await actions.get_artist_details(uri)).to_dict()
362+
363+
364+
async def get_playlist(call: ServiceCall):
365+
"""Returns the details about a playlist from the server."""
366+
config_entry = call.data[ATTR_CONFIG_ENTRY_ID]
367+
uri = call.data[ATTR_URI]
368+
hass = call.hass
369+
entry = hass.config_entries.async_get_entry(config_entry)
370+
actions = entry.runtime_data.actions
371+
return (await actions.get_playlist_details(uri)).to_dict()

0 commit comments

Comments
 (0)