Add actions with response values to Music Assistant (#133521)
Co-authored-by: Franck Nijhof <git@frenck.dev> Co-authored-by: OzGav <gavnosp@hotmail.com> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>pull/133823/head
parent
1f8f85d6eb
commit
83f5ca5a30
|
@ -17,22 +17,28 @@ from homeassistant.core import Event, HomeAssistant
|
|||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
|
||||
from .actions import register_actions
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from music_assistant_models.event import MassEvent
|
||||
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
CONNECT_TIMEOUT = 10
|
||||
LISTEN_READY_TIMEOUT = 30
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
type MusicAssistantConfigEntry = ConfigEntry[MusicAssistantEntryData]
|
||||
|
||||
|
||||
|
@ -44,6 +50,12 @@ class MusicAssistantEntryData:
|
|||
listen_task: asyncio.Task
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Music Assistant component."""
|
||||
register_actions(hass)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: MusicAssistantConfigEntry
|
||||
) -> bool:
|
||||
|
|
|
@ -0,0 +1,212 @@
|
|||
"""Custom actions (previously known as services) for the Music Assistant integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from music_assistant_models.enums import MediaType
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
ServiceResponse,
|
||||
SupportsResponse,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import (
|
||||
ATTR_ALBUM_ARTISTS_ONLY,
|
||||
ATTR_ALBUM_TYPE,
|
||||
ATTR_ALBUMS,
|
||||
ATTR_ARTISTS,
|
||||
ATTR_CONFIG_ENTRY_ID,
|
||||
ATTR_FAVORITE,
|
||||
ATTR_ITEMS,
|
||||
ATTR_LIBRARY_ONLY,
|
||||
ATTR_LIMIT,
|
||||
ATTR_MEDIA_TYPE,
|
||||
ATTR_OFFSET,
|
||||
ATTR_ORDER_BY,
|
||||
ATTR_PLAYLISTS,
|
||||
ATTR_RADIO,
|
||||
ATTR_SEARCH,
|
||||
ATTR_SEARCH_ALBUM,
|
||||
ATTR_SEARCH_ARTIST,
|
||||
ATTR_SEARCH_NAME,
|
||||
ATTR_TRACKS,
|
||||
DOMAIN,
|
||||
)
|
||||
from .schemas import (
|
||||
LIBRARY_RESULTS_SCHEMA,
|
||||
SEARCH_RESULT_SCHEMA,
|
||||
media_item_dict_from_mass_item,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from music_assistant_client import MusicAssistantClient
|
||||
|
||||
from . import MusicAssistantConfigEntry
|
||||
|
||||
SERVICE_SEARCH = "search"
|
||||
SERVICE_GET_LIBRARY = "get_library"
|
||||
DEFAULT_OFFSET = 0
|
||||
DEFAULT_LIMIT = 25
|
||||
DEFAULT_SORT_ORDER = "name"
|
||||
|
||||
|
||||
@callback
|
||||
def get_music_assistant_client(
|
||||
hass: HomeAssistant, config_entry_id: str
|
||||
) -> MusicAssistantClient:
|
||||
"""Get the Music Assistant client for the given config entry."""
|
||||
entry: MusicAssistantConfigEntry | None
|
||||
if not (entry := hass.config_entries.async_get_entry(config_entry_id)):
|
||||
raise ServiceValidationError("Entry not found")
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError("Entry not loaded")
|
||||
return entry.runtime_data.mass
|
||||
|
||||
|
||||
@callback
|
||||
def register_actions(hass: HomeAssistant) -> None:
|
||||
"""Register custom actions."""
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SEARCH,
|
||||
handle_search,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CONFIG_ENTRY_ID): str,
|
||||
vol.Required(ATTR_SEARCH_NAME): cv.string,
|
||||
vol.Optional(ATTR_MEDIA_TYPE): vol.All(
|
||||
cv.ensure_list, [vol.Coerce(MediaType)]
|
||||
),
|
||||
vol.Optional(ATTR_SEARCH_ARTIST): cv.string,
|
||||
vol.Optional(ATTR_SEARCH_ALBUM): cv.string,
|
||||
vol.Optional(ATTR_LIMIT, default=5): vol.Coerce(int),
|
||||
vol.Optional(ATTR_LIBRARY_ONLY, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_GET_LIBRARY,
|
||||
handle_get_library,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CONFIG_ENTRY_ID): str,
|
||||
vol.Required(ATTR_MEDIA_TYPE): vol.Coerce(MediaType),
|
||||
vol.Optional(ATTR_FAVORITE): cv.boolean,
|
||||
vol.Optional(ATTR_SEARCH): cv.string,
|
||||
vol.Optional(ATTR_LIMIT): cv.positive_int,
|
||||
vol.Optional(ATTR_OFFSET): int,
|
||||
vol.Optional(ATTR_ORDER_BY): cv.string,
|
||||
vol.Optional(ATTR_ALBUM_TYPE): list[MediaType],
|
||||
vol.Optional(ATTR_ALBUM_ARTISTS_ONLY): cv.boolean,
|
||||
}
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
|
||||
async def handle_search(call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle queue_command action."""
|
||||
mass = get_music_assistant_client(call.hass, call.data[ATTR_CONFIG_ENTRY_ID])
|
||||
search_name = call.data[ATTR_SEARCH_NAME]
|
||||
search_artist = call.data.get(ATTR_SEARCH_ARTIST)
|
||||
search_album = call.data.get(ATTR_SEARCH_ALBUM)
|
||||
if search_album and search_artist:
|
||||
search_name = f"{search_artist} - {search_album} - {search_name}"
|
||||
elif search_album:
|
||||
search_name = f"{search_album} - {search_name}"
|
||||
elif search_artist:
|
||||
search_name = f"{search_artist} - {search_name}"
|
||||
search_results = await mass.music.search(
|
||||
search_query=search_name,
|
||||
media_types=call.data.get(ATTR_MEDIA_TYPE, MediaType.ALL),
|
||||
limit=call.data[ATTR_LIMIT],
|
||||
library_only=call.data[ATTR_LIBRARY_ONLY],
|
||||
)
|
||||
response: ServiceResponse = SEARCH_RESULT_SCHEMA(
|
||||
{
|
||||
ATTR_ARTISTS: [
|
||||
media_item_dict_from_mass_item(mass, item)
|
||||
for item in search_results.artists
|
||||
],
|
||||
ATTR_ALBUMS: [
|
||||
media_item_dict_from_mass_item(mass, item)
|
||||
for item in search_results.albums
|
||||
],
|
||||
ATTR_TRACKS: [
|
||||
media_item_dict_from_mass_item(mass, item)
|
||||
for item in search_results.tracks
|
||||
],
|
||||
ATTR_PLAYLISTS: [
|
||||
media_item_dict_from_mass_item(mass, item)
|
||||
for item in search_results.playlists
|
||||
],
|
||||
ATTR_RADIO: [
|
||||
media_item_dict_from_mass_item(mass, item)
|
||||
for item in search_results.radio
|
||||
],
|
||||
}
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
async def handle_get_library(call: ServiceCall) -> ServiceResponse:
|
||||
"""Handle get_library action."""
|
||||
mass = get_music_assistant_client(call.hass, call.data[ATTR_CONFIG_ENTRY_ID])
|
||||
media_type = call.data[ATTR_MEDIA_TYPE]
|
||||
limit = call.data.get(ATTR_LIMIT, DEFAULT_LIMIT)
|
||||
offset = call.data.get(ATTR_OFFSET, DEFAULT_OFFSET)
|
||||
order_by = call.data.get(ATTR_ORDER_BY, DEFAULT_SORT_ORDER)
|
||||
base_params = {
|
||||
"favorite": call.data.get(ATTR_FAVORITE),
|
||||
"search": call.data.get(ATTR_SEARCH),
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"order_by": order_by,
|
||||
}
|
||||
if media_type == MediaType.ALBUM:
|
||||
library_result = await mass.music.get_library_albums(
|
||||
**base_params,
|
||||
album_types=call.data.get(ATTR_ALBUM_TYPE),
|
||||
)
|
||||
elif media_type == MediaType.ARTIST:
|
||||
library_result = await mass.music.get_library_artists(
|
||||
**base_params,
|
||||
album_artists_only=call.data.get(ATTR_ALBUM_ARTISTS_ONLY),
|
||||
)
|
||||
elif media_type == MediaType.TRACK:
|
||||
library_result = await mass.music.get_library_tracks(
|
||||
**base_params,
|
||||
)
|
||||
elif media_type == MediaType.RADIO:
|
||||
library_result = await mass.music.get_library_radios(
|
||||
**base_params,
|
||||
)
|
||||
elif media_type == MediaType.PLAYLIST:
|
||||
library_result = await mass.music.get_library_playlists(
|
||||
**base_params,
|
||||
)
|
||||
else:
|
||||
raise ServiceValidationError(f"Unsupported media type {media_type}")
|
||||
|
||||
response: ServiceResponse = LIBRARY_RESULTS_SCHEMA(
|
||||
{
|
||||
ATTR_ITEMS: [
|
||||
media_item_dict_from_mass_item(mass, item) for item in library_result
|
||||
],
|
||||
ATTR_LIMIT: limit,
|
||||
ATTR_OFFSET: offset,
|
||||
ATTR_ORDER_BY: order_by,
|
||||
ATTR_MEDIA_TYPE: media_type,
|
||||
}
|
||||
)
|
||||
return response
|
|
@ -14,5 +14,55 @@ ATTR_GROUP_PARENTS = "group_parents"
|
|||
ATTR_MASS_PLAYER_TYPE = "mass_player_type"
|
||||
ATTR_ACTIVE_QUEUE = "active_queue"
|
||||
ATTR_STREAM_TITLE = "stream_title"
|
||||
ATTR_MEDIA_TYPE = "media_type"
|
||||
ATTR_SEARCH_NAME = "name"
|
||||
ATTR_SEARCH_ARTIST = "artist"
|
||||
ATTR_SEARCH_ALBUM = "album"
|
||||
ATTR_LIMIT = "limit"
|
||||
ATTR_LIBRARY_ONLY = "library_only"
|
||||
ATTR_FAVORITE = "favorite"
|
||||
ATTR_SEARCH = "search"
|
||||
ATTR_OFFSET = "offset"
|
||||
ATTR_ORDER_BY = "order_by"
|
||||
ATTR_ALBUM_TYPE = "album_type"
|
||||
ATTR_ALBUM_ARTISTS_ONLY = "album_artists_only"
|
||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||
ATTR_URI = "uri"
|
||||
ATTR_IMAGE = "image"
|
||||
ATTR_VERSION = "version"
|
||||
ATTR_ARTISTS = "artists"
|
||||
ATTR_ALBUMS = "albums"
|
||||
ATTR_TRACKS = "tracks"
|
||||
ATTR_PLAYLISTS = "playlists"
|
||||
ATTR_RADIO = "radio"
|
||||
ATTR_ITEMS = "items"
|
||||
ATTR_RADIO_MODE = "radio_mode"
|
||||
ATTR_MEDIA_ID = "media_id"
|
||||
ATTR_ARTIST = "artist"
|
||||
ATTR_ALBUM = "album"
|
||||
ATTR_URL = "url"
|
||||
ATTR_USE_PRE_ANNOUNCE = "use_pre_announce"
|
||||
ATTR_ANNOUNCE_VOLUME = "announce_volume"
|
||||
ATTR_SOURCE_PLAYER = "source_player"
|
||||
ATTR_AUTO_PLAY = "auto_play"
|
||||
ATTR_QUEUE_ID = "queue_id"
|
||||
ATTR_ACTIVE = "active"
|
||||
ATTR_SHUFFLE_ENABLED = "shuffle_enabled"
|
||||
ATTR_REPEAT_MODE = "repeat_mode"
|
||||
ATTR_CURRENT_INDEX = "current_index"
|
||||
ATTR_ELAPSED_TIME = "elapsed_time"
|
||||
ATTR_CURRENT_ITEM = "current_item"
|
||||
ATTR_NEXT_ITEM = "next_item"
|
||||
ATTR_QUEUE_ITEM_ID = "queue_item_id"
|
||||
ATTR_DURATION = "duration"
|
||||
ATTR_MEDIA_ITEM = "media_item"
|
||||
ATTR_STREAM_DETAILS = "stream_details"
|
||||
ATTR_CONTENT_TYPE = "content_type"
|
||||
ATTR_SAMPLE_RATE = "sample_rate"
|
||||
ATTR_BIT_DEPTH = "bit_depth"
|
||||
ATTR_STREAM_TITLE = "stream_title"
|
||||
ATTR_PROVIDER = "provider"
|
||||
ATTR_ITEM_ID = "item_id"
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
"services": {
|
||||
"play_media": { "service": "mdi:play" },
|
||||
"play_announcement": { "service": "mdi:bullhorn" },
|
||||
"transfer_queue": { "service": "mdi:transfer" }
|
||||
"transfer_queue": { "service": "mdi:transfer" },
|
||||
"search": { "service": "mdi:magnify" },
|
||||
"get_queue": { "service": "mdi:playlist-music" },
|
||||
"get_library": { "service": "mdi:music-box-multiple" }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,8 +36,8 @@ from homeassistant.components.media_player import (
|
|||
RepeatMode,
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.const import STATE_OFF
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import ATTR_NAME, STATE_OFF
|
||||
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
@ -48,9 +48,33 @@ from homeassistant.helpers.entity_platform import (
|
|||
from homeassistant.util.dt import utc_from_timestamp
|
||||
|
||||
from . import MusicAssistantConfigEntry
|
||||
from .const import ATTR_ACTIVE_QUEUE, ATTR_MASS_PLAYER_TYPE, DOMAIN
|
||||
from .const import (
|
||||
ATTR_ACTIVE,
|
||||
ATTR_ACTIVE_QUEUE,
|
||||
ATTR_ALBUM,
|
||||
ATTR_ANNOUNCE_VOLUME,
|
||||
ATTR_ARTIST,
|
||||
ATTR_AUTO_PLAY,
|
||||
ATTR_CURRENT_INDEX,
|
||||
ATTR_CURRENT_ITEM,
|
||||
ATTR_ELAPSED_TIME,
|
||||
ATTR_ITEMS,
|
||||
ATTR_MASS_PLAYER_TYPE,
|
||||
ATTR_MEDIA_ID,
|
||||
ATTR_MEDIA_TYPE,
|
||||
ATTR_NEXT_ITEM,
|
||||
ATTR_QUEUE_ID,
|
||||
ATTR_RADIO_MODE,
|
||||
ATTR_REPEAT_MODE,
|
||||
ATTR_SHUFFLE_ENABLED,
|
||||
ATTR_SOURCE_PLAYER,
|
||||
ATTR_URL,
|
||||
ATTR_USE_PRE_ANNOUNCE,
|
||||
DOMAIN,
|
||||
)
|
||||
from .entity import MusicAssistantEntity
|
||||
from .media_browser import async_browse_media
|
||||
from .schemas import QUEUE_DETAILS_SCHEMA, queue_item_dict_from_mass_item
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from music_assistant_client import MusicAssistantClient
|
||||
|
@ -89,16 +113,7 @@ QUEUE_OPTION_MAP = {
|
|||
SERVICE_PLAY_MEDIA_ADVANCED = "play_media"
|
||||
SERVICE_PLAY_ANNOUNCEMENT = "play_announcement"
|
||||
SERVICE_TRANSFER_QUEUE = "transfer_queue"
|
||||
ATTR_RADIO_MODE = "radio_mode"
|
||||
ATTR_MEDIA_ID = "media_id"
|
||||
ATTR_MEDIA_TYPE = "media_type"
|
||||
ATTR_ARTIST = "artist"
|
||||
ATTR_ALBUM = "album"
|
||||
ATTR_URL = "url"
|
||||
ATTR_USE_PRE_ANNOUNCE = "use_pre_announce"
|
||||
ATTR_ANNOUNCE_VOLUME = "announce_volume"
|
||||
ATTR_SOURCE_PLAYER = "source_player"
|
||||
ATTR_AUTO_PLAY = "auto_play"
|
||||
SERVICE_GET_QUEUE = "get_queue"
|
||||
|
||||
|
||||
def catch_musicassistant_error[_R, **P](
|
||||
|
@ -179,6 +194,12 @@ async def async_setup_entry(
|
|||
},
|
||||
"_async_handle_transfer_queue",
|
||||
)
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_GET_QUEUE,
|
||||
schema=None,
|
||||
func="_async_handle_get_queue",
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
|
||||
class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||
|
@ -513,6 +534,32 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
|||
source_queue_id, target_queue_id, auto_play
|
||||
)
|
||||
|
||||
@catch_musicassistant_error
|
||||
async def _async_handle_get_queue(self) -> ServiceResponse:
|
||||
"""Handle get_queue action."""
|
||||
if not self.active_queue:
|
||||
raise HomeAssistantError("No active queue found")
|
||||
active_queue = self.active_queue
|
||||
response: ServiceResponse = QUEUE_DETAILS_SCHEMA(
|
||||
{
|
||||
ATTR_QUEUE_ID: active_queue.queue_id,
|
||||
ATTR_ACTIVE: active_queue.active,
|
||||
ATTR_NAME: active_queue.display_name,
|
||||
ATTR_ITEMS: active_queue.items,
|
||||
ATTR_SHUFFLE_ENABLED: active_queue.shuffle_enabled,
|
||||
ATTR_REPEAT_MODE: active_queue.repeat_mode.value,
|
||||
ATTR_CURRENT_INDEX: active_queue.current_index,
|
||||
ATTR_ELAPSED_TIME: active_queue.corrected_elapsed_time,
|
||||
ATTR_CURRENT_ITEM: queue_item_dict_from_mass_item(
|
||||
self.mass, active_queue.current_item
|
||||
),
|
||||
ATTR_NEXT_ITEM: queue_item_dict_from_mass_item(
|
||||
self.mass, active_queue.next_item
|
||||
),
|
||||
}
|
||||
)
|
||||
return response
|
||||
|
||||
async def async_browse_media(
|
||||
self,
|
||||
media_content_type: MediaType | str | None = None,
|
||||
|
|
|
@ -0,0 +1,182 @@
|
|||
"""Voluptuous schemas for Music Assistant integration service responses."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from music_assistant_models.enums import MediaType
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import (
|
||||
ATTR_ACTIVE,
|
||||
ATTR_ALBUM,
|
||||
ATTR_ALBUMS,
|
||||
ATTR_ARTISTS,
|
||||
ATTR_BIT_DEPTH,
|
||||
ATTR_CONTENT_TYPE,
|
||||
ATTR_CURRENT_INDEX,
|
||||
ATTR_CURRENT_ITEM,
|
||||
ATTR_DURATION,
|
||||
ATTR_ELAPSED_TIME,
|
||||
ATTR_IMAGE,
|
||||
ATTR_ITEM_ID,
|
||||
ATTR_ITEMS,
|
||||
ATTR_LIMIT,
|
||||
ATTR_MEDIA_ITEM,
|
||||
ATTR_MEDIA_TYPE,
|
||||
ATTR_NEXT_ITEM,
|
||||
ATTR_OFFSET,
|
||||
ATTR_ORDER_BY,
|
||||
ATTR_PLAYLISTS,
|
||||
ATTR_PROVIDER,
|
||||
ATTR_QUEUE_ID,
|
||||
ATTR_QUEUE_ITEM_ID,
|
||||
ATTR_RADIO,
|
||||
ATTR_REPEAT_MODE,
|
||||
ATTR_SAMPLE_RATE,
|
||||
ATTR_SHUFFLE_ENABLED,
|
||||
ATTR_STREAM_DETAILS,
|
||||
ATTR_STREAM_TITLE,
|
||||
ATTR_TRACKS,
|
||||
ATTR_URI,
|
||||
ATTR_VERSION,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from music_assistant_client import MusicAssistantClient
|
||||
from music_assistant_models.media_items import ItemMapping, MediaItemType
|
||||
from music_assistant_models.queue_item import QueueItem
|
||||
|
||||
MEDIA_ITEM_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_MEDIA_TYPE): vol.Coerce(MediaType),
|
||||
vol.Required(ATTR_URI): cv.string,
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
vol.Required(ATTR_VERSION): cv.string,
|
||||
vol.Optional(ATTR_IMAGE, default=None): vol.Any(None, cv.string),
|
||||
vol.Optional(ATTR_ARTISTS): [vol.Self],
|
||||
vol.Optional(ATTR_ALBUM): vol.Self,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def media_item_dict_from_mass_item(
|
||||
mass: MusicAssistantClient,
|
||||
item: MediaItemType | ItemMapping | None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Parse a Music Assistant MediaItem."""
|
||||
if not item:
|
||||
return None
|
||||
base = {
|
||||
ATTR_MEDIA_TYPE: item.media_type,
|
||||
ATTR_URI: item.uri,
|
||||
ATTR_NAME: item.name,
|
||||
ATTR_VERSION: item.version,
|
||||
ATTR_IMAGE: mass.get_media_item_image_url(item),
|
||||
}
|
||||
if artists := getattr(item, "artists", None):
|
||||
base[ATTR_ARTISTS] = [media_item_dict_from_mass_item(mass, x) for x in artists]
|
||||
if album := getattr(item, "album", None):
|
||||
base[ATTR_ALBUM] = media_item_dict_from_mass_item(mass, album)
|
||||
return base
|
||||
|
||||
|
||||
SEARCH_RESULT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ARTISTS): vol.All(
|
||||
cv.ensure_list, [vol.Schema(MEDIA_ITEM_SCHEMA)]
|
||||
),
|
||||
vol.Required(ATTR_ALBUMS): vol.All(
|
||||
cv.ensure_list, [vol.Schema(MEDIA_ITEM_SCHEMA)]
|
||||
),
|
||||
vol.Required(ATTR_TRACKS): vol.All(
|
||||
cv.ensure_list, [vol.Schema(MEDIA_ITEM_SCHEMA)]
|
||||
),
|
||||
vol.Required(ATTR_PLAYLISTS): vol.All(
|
||||
cv.ensure_list, [vol.Schema(MEDIA_ITEM_SCHEMA)]
|
||||
),
|
||||
vol.Required(ATTR_RADIO): vol.All(
|
||||
cv.ensure_list, [vol.Schema(MEDIA_ITEM_SCHEMA)]
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
LIBRARY_RESULTS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_ITEMS): vol.All(
|
||||
cv.ensure_list, [vol.Schema(MEDIA_ITEM_SCHEMA)]
|
||||
),
|
||||
vol.Required(ATTR_LIMIT): int,
|
||||
vol.Required(ATTR_OFFSET): int,
|
||||
vol.Required(ATTR_ORDER_BY): str,
|
||||
vol.Required(ATTR_MEDIA_TYPE): vol.Coerce(MediaType),
|
||||
}
|
||||
)
|
||||
|
||||
AUDIO_FORMAT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CONTENT_TYPE): str,
|
||||
vol.Required(ATTR_SAMPLE_RATE): int,
|
||||
vol.Required(ATTR_BIT_DEPTH): int,
|
||||
vol.Required(ATTR_PROVIDER): str,
|
||||
vol.Required(ATTR_ITEM_ID): str,
|
||||
}
|
||||
)
|
||||
|
||||
QUEUE_ITEM_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_QUEUE_ITEM_ID): cv.string,
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
vol.Optional(ATTR_DURATION, default=None): vol.Any(None, int),
|
||||
vol.Optional(ATTR_MEDIA_ITEM, default=None): vol.Any(
|
||||
None, vol.Schema(MEDIA_ITEM_SCHEMA)
|
||||
),
|
||||
vol.Optional(ATTR_STREAM_DETAILS): vol.Schema(AUDIO_FORMAT_SCHEMA),
|
||||
vol.Optional(ATTR_STREAM_TITLE, default=None): vol.Any(None, cv.string),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def queue_item_dict_from_mass_item(
|
||||
mass: MusicAssistantClient,
|
||||
item: QueueItem | None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Parse a Music Assistant QueueItem."""
|
||||
if not item:
|
||||
return None
|
||||
base = {
|
||||
ATTR_QUEUE_ITEM_ID: item.queue_item_id,
|
||||
ATTR_NAME: item.name,
|
||||
ATTR_DURATION: item.duration,
|
||||
ATTR_MEDIA_ITEM: media_item_dict_from_mass_item(mass, item.media_item),
|
||||
}
|
||||
if streamdetails := item.streamdetails:
|
||||
base[ATTR_STREAM_TITLE] = streamdetails.stream_title
|
||||
base[ATTR_STREAM_DETAILS] = {
|
||||
ATTR_CONTENT_TYPE: streamdetails.audio_format.content_type.value,
|
||||
ATTR_SAMPLE_RATE: streamdetails.audio_format.sample_rate,
|
||||
ATTR_BIT_DEPTH: streamdetails.audio_format.bit_depth,
|
||||
ATTR_PROVIDER: streamdetails.provider,
|
||||
ATTR_ITEM_ID: streamdetails.item_id,
|
||||
}
|
||||
|
||||
return base
|
||||
|
||||
|
||||
QUEUE_DETAILS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_QUEUE_ID): str,
|
||||
vol.Required(ATTR_ACTIVE): bool,
|
||||
vol.Required(ATTR_NAME): str,
|
||||
vol.Required(ATTR_ITEMS): int,
|
||||
vol.Required(ATTR_SHUFFLE_ENABLED): bool,
|
||||
vol.Required(ATTR_REPEAT_MODE): str,
|
||||
vol.Required(ATTR_CURRENT_INDEX): vol.Any(None, int),
|
||||
vol.Required(ATTR_ELAPSED_TIME): vol.Coerce(int),
|
||||
vol.Required(ATTR_CURRENT_ITEM): vol.Any(None, QUEUE_ITEM_SCHEMA),
|
||||
vol.Required(ATTR_NEXT_ITEM): vol.Any(None, QUEUE_ITEM_SCHEMA),
|
||||
}
|
||||
)
|
|
@ -88,3 +88,146 @@ transfer_queue:
|
|||
example: "true"
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
get_queue:
|
||||
target:
|
||||
entity:
|
||||
domain: media_player
|
||||
integration: music_assistant
|
||||
supported_features:
|
||||
- media_player.MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
|
||||
search:
|
||||
fields:
|
||||
config_entry_id:
|
||||
required: true
|
||||
selector:
|
||||
config_entry:
|
||||
integration: music_assistant
|
||||
name:
|
||||
required: true
|
||||
example: "We Are The Champions"
|
||||
selector:
|
||||
text:
|
||||
media_type:
|
||||
example: "playlist"
|
||||
selector:
|
||||
select:
|
||||
multiple: true
|
||||
translation_key: media_type
|
||||
options:
|
||||
- artist
|
||||
- album
|
||||
- playlist
|
||||
- track
|
||||
- radio
|
||||
artist:
|
||||
example: "Queen"
|
||||
selector:
|
||||
text:
|
||||
album:
|
||||
example: "News of the world"
|
||||
selector:
|
||||
text:
|
||||
limit:
|
||||
advanced: true
|
||||
example: 25
|
||||
default: 5
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 100
|
||||
step: 1
|
||||
library_only:
|
||||
example: "true"
|
||||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
get_library:
|
||||
fields:
|
||||
config_entry_id:
|
||||
required: true
|
||||
selector:
|
||||
config_entry:
|
||||
integration: music_assistant
|
||||
media_type:
|
||||
required: true
|
||||
example: "playlist"
|
||||
selector:
|
||||
select:
|
||||
translation_key: media_type
|
||||
options:
|
||||
- artist
|
||||
- album
|
||||
- playlist
|
||||
- track
|
||||
- radio
|
||||
favorite:
|
||||
example: "true"
|
||||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
search:
|
||||
example: "We Are The Champions"
|
||||
selector:
|
||||
text:
|
||||
limit:
|
||||
advanced: true
|
||||
example: 25
|
||||
default: 25
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 500
|
||||
step: 1
|
||||
offset:
|
||||
advanced: true
|
||||
example: 25
|
||||
default: 0
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 1000000
|
||||
step: 1
|
||||
order_by:
|
||||
example: "random"
|
||||
selector:
|
||||
select:
|
||||
translation_key: order_by
|
||||
options:
|
||||
- name
|
||||
- name_desc
|
||||
- sort_name
|
||||
- sort_name_desc
|
||||
- timestamp_added
|
||||
- timestamp_added_desc
|
||||
- last_played
|
||||
- last_played_desc
|
||||
- play_count
|
||||
- play_count_desc
|
||||
- year
|
||||
- year_desc
|
||||
- position
|
||||
- position_desc
|
||||
- artist_name
|
||||
- artist_name_desc
|
||||
- random
|
||||
- random_play_count
|
||||
album_type:
|
||||
example: "single"
|
||||
selector:
|
||||
select:
|
||||
multiple: true
|
||||
translation_key: album_type
|
||||
options:
|
||||
- album
|
||||
- single
|
||||
- compilation
|
||||
- ep
|
||||
- unknown
|
||||
album_artists_only:
|
||||
example: "true"
|
||||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
|
|
|
@ -99,6 +99,86 @@
|
|||
"description": "Start playing the queue on the target player. Omit to use the default behavior."
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_queue": {
|
||||
"name": "Get playerQueue details (advanced)",
|
||||
"description": "Get the details of the currently active queue of a Music Assistant player."
|
||||
},
|
||||
"search": {
|
||||
"name": "Search Music Assistant",
|
||||
"description": "Perform a global search on the Music Assistant library and all providers.",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"name": "Music Assistant instance",
|
||||
"description": "Select the Music Assistant instance to perform the search on."
|
||||
},
|
||||
"name": {
|
||||
"name": "Search name",
|
||||
"description": "The name/title to search for."
|
||||
},
|
||||
"media_type": {
|
||||
"name": "Media type(s)",
|
||||
"description": "The type of the content to search. Such as artist, album, track, radio, or playlist. All types if omitted."
|
||||
},
|
||||
"artist": {
|
||||
"name": "Artist name",
|
||||
"description": "When specifying a track or album name in the name field, you can optionally restrict results by this artist name."
|
||||
},
|
||||
"album": {
|
||||
"name": "Album name",
|
||||
"description": "When specifying a track name in the name field, you can optionally restrict results by this album name."
|
||||
},
|
||||
"limit": {
|
||||
"name": "Limit",
|
||||
"description": "Maximum number of items to return (per media type)."
|
||||
},
|
||||
"library_only": {
|
||||
"name": "Only library items",
|
||||
"description": "Only include results that are in the library."
|
||||
}
|
||||
}
|
||||
},
|
||||
"get_library": {
|
||||
"name": "Get Library items",
|
||||
"description": "Get items from a Music Assistant library.",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"name": "[%key:component::music_assistant::services::search::fields::config_entry_id::name%]",
|
||||
"description": "[%key:component::music_assistant::services::search::fields::config_entry_id::description%]"
|
||||
},
|
||||
"media_type": {
|
||||
"name": "Media type",
|
||||
"description": "The media type for which to request details for."
|
||||
},
|
||||
"favorite": {
|
||||
"name": "Favorites only",
|
||||
"description": "Filter items so only favorites items are returned."
|
||||
},
|
||||
"search": {
|
||||
"name": "Search",
|
||||
"description": "Optional search string to search through this library."
|
||||
},
|
||||
"limit": {
|
||||
"name": "Limit",
|
||||
"description": "Maximum number of items to return."
|
||||
},
|
||||
"offset": {
|
||||
"name": "Offset",
|
||||
"description": "Offset to start the list from."
|
||||
},
|
||||
"order_by": {
|
||||
"name": "Order By",
|
||||
"description": "Sort the list by this field."
|
||||
},
|
||||
"album_type": {
|
||||
"name": "Album type filter (albums library only)",
|
||||
"description": "Filter albums by type."
|
||||
},
|
||||
"album_artists_only": {
|
||||
"name": "Enable album artists filter (only for artist library)",
|
||||
"description": "Only return Album Artists when listing the Artists library items."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
|
@ -119,6 +199,37 @@
|
|||
"playlist": "Playlist",
|
||||
"radio": "Radio"
|
||||
}
|
||||
},
|
||||
"order_by": {
|
||||
"options": {
|
||||
"name": "Name",
|
||||
"name_desc": "Name (desc)",
|
||||
"sort_name": "Sort name",
|
||||
"sort_name_desc": "Sort name (desc)",
|
||||
"timestamp_added": "Added",
|
||||
"timestamp_added_desc": "Added (desc)",
|
||||
"last_played": "Last played",
|
||||
"last_played_desc": "Last played (desc)",
|
||||
"play_count": "Play count",
|
||||
"play_count_desc": "Play count (desc)",
|
||||
"year": "Year",
|
||||
"year_desc": "Year (desc)",
|
||||
"position": "Position",
|
||||
"position_desc": "Position (desc)",
|
||||
"artist_name": "Artist name",
|
||||
"artist_name_desc": "Artist name (desc)",
|
||||
"random": "Random",
|
||||
"random_play_count": "Random + least played"
|
||||
}
|
||||
},
|
||||
"album_type": {
|
||||
"options": {
|
||||
"album": "Album",
|
||||
"single": "Single",
|
||||
"ep": "EP",
|
||||
"compilation": "Compilation",
|
||||
"unknown": "Unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ def load_and_parse_fixture(fixture: str) -> dict[str, Any]:
|
|||
async def setup_integration_from_fixtures(
|
||||
hass: HomeAssistant,
|
||||
music_assistant_client: MagicMock,
|
||||
) -> None:
|
||||
) -> MockConfigEntry:
|
||||
"""Set up MusicAssistant integration with fixture data."""
|
||||
players = create_players_from_fixture()
|
||||
music_assistant_client.players._players = {x.player_id: x for x in players}
|
||||
|
@ -65,6 +65,7 @@ async def setup_integration_from_fixtures(
|
|||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
return config_entry
|
||||
|
||||
|
||||
def create_players_from_fixture() -> list[Player]:
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
# serializer version: 1
|
||||
# name: test_get_library_action
|
||||
dict({
|
||||
'items': list([
|
||||
dict({
|
||||
'album': dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ALBUM: 'album'>,
|
||||
'name': 'Traveller',
|
||||
'uri': 'library://album/463',
|
||||
'version': '',
|
||||
}),
|
||||
'artists': list([
|
||||
dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ARTIST: 'artist'>,
|
||||
'name': 'Chris Stapleton',
|
||||
'uri': 'library://artist/433',
|
||||
'version': '',
|
||||
}),
|
||||
]),
|
||||
'image': None,
|
||||
'media_type': <MediaType.TRACK: 'track'>,
|
||||
'name': 'Tennessee Whiskey',
|
||||
'uri': 'library://track/456',
|
||||
'version': '',
|
||||
}),
|
||||
dict({
|
||||
'album': dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ALBUM: 'album'>,
|
||||
'name': 'Thelma + Louise',
|
||||
'uri': 'library://album/471',
|
||||
'version': '',
|
||||
}),
|
||||
'artists': list([
|
||||
dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ARTIST: 'artist'>,
|
||||
'name': 'Bastille',
|
||||
'uri': 'library://artist/81',
|
||||
'version': '',
|
||||
}),
|
||||
]),
|
||||
'image': None,
|
||||
'media_type': <MediaType.TRACK: 'track'>,
|
||||
'name': 'Thelma + Louise',
|
||||
'uri': 'library://track/467',
|
||||
'version': '',
|
||||
}),
|
||||
dict({
|
||||
'album': dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ALBUM: 'album'>,
|
||||
'name': 'HIStory - PAST, PRESENT AND FUTURE - BOOK I',
|
||||
'uri': 'library://album/486',
|
||||
'version': '',
|
||||
}),
|
||||
'artists': list([
|
||||
dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ARTIST: 'artist'>,
|
||||
'name': 'Michael Jackson',
|
||||
'uri': 'library://artist/30',
|
||||
'version': '',
|
||||
}),
|
||||
]),
|
||||
'image': None,
|
||||
'media_type': <MediaType.TRACK: 'track'>,
|
||||
'name': "They Don't Care About Us",
|
||||
'uri': 'library://track/485',
|
||||
'version': '',
|
||||
}),
|
||||
dict({
|
||||
'album': dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ALBUM: 'album'>,
|
||||
'name': 'Better Dayz',
|
||||
'uri': 'library://album/487',
|
||||
'version': '',
|
||||
}),
|
||||
'artists': list([
|
||||
dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ARTIST: 'artist'>,
|
||||
'name': '2Pac',
|
||||
'uri': 'library://artist/159',
|
||||
'version': '',
|
||||
}),
|
||||
dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ARTIST: 'artist'>,
|
||||
'name': 'The Outlawz',
|
||||
'uri': 'library://artist/451',
|
||||
'version': '',
|
||||
}),
|
||||
]),
|
||||
'image': None,
|
||||
'media_type': <MediaType.TRACK: 'track'>,
|
||||
'name': "They Don't Give A F**** About Us",
|
||||
'uri': 'library://track/486',
|
||||
'version': '',
|
||||
}),
|
||||
dict({
|
||||
'album': dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ALBUM: 'album'>,
|
||||
'name': 'Things We Lost In The Fire',
|
||||
'uri': 'library://album/488',
|
||||
'version': '',
|
||||
}),
|
||||
'artists': list([
|
||||
dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ARTIST: 'artist'>,
|
||||
'name': 'Bastille',
|
||||
'uri': 'library://artist/81',
|
||||
'version': '',
|
||||
}),
|
||||
]),
|
||||
'image': None,
|
||||
'media_type': <MediaType.TRACK: 'track'>,
|
||||
'name': 'Things We Lost In The Fire',
|
||||
'uri': 'library://track/487',
|
||||
'version': 'TORN Remix',
|
||||
}),
|
||||
dict({
|
||||
'album': dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ALBUM: 'album'>,
|
||||
'name': 'Doom Days',
|
||||
'uri': 'library://album/489',
|
||||
'version': '',
|
||||
}),
|
||||
'artists': list([
|
||||
dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ARTIST: 'artist'>,
|
||||
'name': 'Bastille',
|
||||
'uri': 'library://artist/81',
|
||||
'version': '',
|
||||
}),
|
||||
]),
|
||||
'image': None,
|
||||
'media_type': <MediaType.TRACK: 'track'>,
|
||||
'name': 'Those Nights',
|
||||
'uri': 'library://track/488',
|
||||
'version': '',
|
||||
}),
|
||||
]),
|
||||
'limit': 25,
|
||||
'media_type': <MediaType.TRACK: 'track'>,
|
||||
'offset': 0,
|
||||
'order_by': 'name',
|
||||
})
|
||||
# ---
|
||||
# name: test_search_action
|
||||
dict({
|
||||
'albums': list([
|
||||
dict({
|
||||
'artists': list([
|
||||
dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ARTIST: 'artist'>,
|
||||
'name': 'A Space Love Adventure',
|
||||
'uri': 'library://artist/289',
|
||||
'version': '',
|
||||
}),
|
||||
]),
|
||||
'image': None,
|
||||
'media_type': <MediaType.ALBUM: 'album'>,
|
||||
'name': 'Synth Punk EP',
|
||||
'uri': 'library://album/396',
|
||||
'version': '',
|
||||
}),
|
||||
dict({
|
||||
'artists': list([
|
||||
dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ARTIST: 'artist'>,
|
||||
'name': 'Various Artists',
|
||||
'uri': 'library://artist/96',
|
||||
'version': '',
|
||||
}),
|
||||
]),
|
||||
'image': None,
|
||||
'media_type': <MediaType.ALBUM: 'album'>,
|
||||
'name': 'Synthwave (The 80S Revival)',
|
||||
'uri': 'library://album/95',
|
||||
'version': 'The 80S Revival',
|
||||
}),
|
||||
]),
|
||||
'artists': list([
|
||||
]),
|
||||
'playlists': list([
|
||||
]),
|
||||
'radio': list([
|
||||
]),
|
||||
'tracks': list([
|
||||
]),
|
||||
})
|
||||
# ---
|
|
@ -188,3 +188,88 @@
|
|||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_media_player_get_queue_action
|
||||
dict({
|
||||
'media_player.test_group_player_1': dict({
|
||||
'active': True,
|
||||
'current_index': 26,
|
||||
'current_item': dict({
|
||||
'duration': 536,
|
||||
'media_item': dict({
|
||||
'album': dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ALBUM: 'album'>,
|
||||
'name': 'Use Your Illusion I',
|
||||
'uri': 'spotify://album/0CxPbTRARqKUYighiEY9Sz',
|
||||
'version': '',
|
||||
}),
|
||||
'artists': list([
|
||||
dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ARTIST: 'artist'>,
|
||||
'name': "Guns N' Roses",
|
||||
'uri': 'spotify://artist/3qm84nBOXUEQ2vnTfUTTFC',
|
||||
'version': '',
|
||||
}),
|
||||
]),
|
||||
'image': None,
|
||||
'media_type': <MediaType.TRACK: 'track'>,
|
||||
'name': 'November Rain',
|
||||
'uri': 'spotify://track/3YRCqOhFifThpSRFJ1VWFM',
|
||||
'version': '',
|
||||
}),
|
||||
'name': "Guns N' Roses - November Rain",
|
||||
'queue_item_id': '5d95dc5be77e4f7eb4939f62cfef527b',
|
||||
'stream_details': dict({
|
||||
'bit_depth': 16,
|
||||
'content_type': 'ogg',
|
||||
'item_id': '3YRCqOhFifThpSRFJ1VWFM',
|
||||
'provider': 'spotify',
|
||||
'sample_rate': 44100,
|
||||
}),
|
||||
'stream_title': None,
|
||||
}),
|
||||
'items': 1094,
|
||||
'name': 'Test Group Player 1',
|
||||
'next_item': dict({
|
||||
'duration': 207,
|
||||
'media_item': dict({
|
||||
'album': dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ALBUM: 'album'>,
|
||||
'name': 'La Folie',
|
||||
'uri': 'qobuz://album/0724353468859',
|
||||
'version': '',
|
||||
}),
|
||||
'artists': list([
|
||||
dict({
|
||||
'image': None,
|
||||
'media_type': <MediaType.ARTIST: 'artist'>,
|
||||
'name': 'The Stranglers',
|
||||
'uri': 'qobuz://artist/26779',
|
||||
'version': '',
|
||||
}),
|
||||
]),
|
||||
'image': None,
|
||||
'media_type': <MediaType.TRACK: 'track'>,
|
||||
'name': 'Golden Brown',
|
||||
'uri': 'qobuz://track/1004735',
|
||||
'version': '',
|
||||
}),
|
||||
'name': 'The Stranglers - Golden Brown',
|
||||
'queue_item_id': '990ae8f29cdf4fb588d679b115621f55',
|
||||
'stream_details': dict({
|
||||
'bit_depth': 16,
|
||||
'content_type': 'flac',
|
||||
'item_id': '1004735',
|
||||
'provider': 'qobuz',
|
||||
'sample_rate': 44100,
|
||||
}),
|
||||
'stream_title': None,
|
||||
}),
|
||||
'queue_id': 'test_group_player_1',
|
||||
'repeat_mode': 'all',
|
||||
'shuffle_enabled': True,
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
"""Test Music Assistant actions."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from music_assistant_models.media_items import SearchResults
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.music_assistant.actions import (
|
||||
SERVICE_GET_LIBRARY,
|
||||
SERVICE_SEARCH,
|
||||
)
|
||||
from homeassistant.components.music_assistant.const import (
|
||||
ATTR_CONFIG_ENTRY_ID,
|
||||
ATTR_FAVORITE,
|
||||
ATTR_MEDIA_TYPE,
|
||||
ATTR_SEARCH_NAME,
|
||||
DOMAIN as MASS_DOMAIN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .common import create_library_albums_from_fixture, setup_integration_from_fixtures
|
||||
|
||||
|
||||
async def test_search_action(
|
||||
hass: HomeAssistant,
|
||||
music_assistant_client: MagicMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test music assistant search action."""
|
||||
entry = await setup_integration_from_fixtures(hass, music_assistant_client)
|
||||
|
||||
music_assistant_client.music.search = AsyncMock(
|
||||
return_value=SearchResults(
|
||||
albums=create_library_albums_from_fixture(),
|
||||
)
|
||||
)
|
||||
response = await hass.services.async_call(
|
||||
MASS_DOMAIN,
|
||||
SERVICE_SEARCH,
|
||||
{
|
||||
ATTR_CONFIG_ENTRY_ID: entry.entry_id,
|
||||
ATTR_SEARCH_NAME: "test",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == snapshot
|
||||
|
||||
|
||||
async def test_get_library_action(
|
||||
hass: HomeAssistant,
|
||||
music_assistant_client: MagicMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test music assistant get_library action."""
|
||||
entry = await setup_integration_from_fixtures(hass, music_assistant_client)
|
||||
response = await hass.services.async_call(
|
||||
MASS_DOMAIN,
|
||||
SERVICE_GET_LIBRARY,
|
||||
{
|
||||
ATTR_CONFIG_ENTRY_ID: entry.entry_id,
|
||||
ATTR_FAVORITE: False,
|
||||
ATTR_MEDIA_TYPE: "track",
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
assert response == snapshot
|
|
@ -6,6 +6,7 @@ from music_assistant_models.enums import MediaType, QueueOption
|
|||
from music_assistant_models.media_items import Track
|
||||
import pytest
|
||||
from syrupy import SnapshotAssertion
|
||||
from syrupy.filters import paths
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_GROUP_MEMBERS,
|
||||
|
@ -32,6 +33,7 @@ from homeassistant.components.music_assistant.media_player import (
|
|||
ATTR_SOURCE_PLAYER,
|
||||
ATTR_URL,
|
||||
ATTR_USE_PRE_ANNOUNCE,
|
||||
SERVICE_GET_QUEUE,
|
||||
SERVICE_PLAY_ANNOUNCEMENT,
|
||||
SERVICE_PLAY_MEDIA_ADVANCED,
|
||||
SERVICE_TRANSFER_QUEUE,
|
||||
|
@ -583,3 +585,25 @@ async def test_media_player_transfer_queue_action(
|
|||
auto_play=None,
|
||||
require_schema=25,
|
||||
)
|
||||
|
||||
|
||||
async def test_media_player_get_queue_action(
|
||||
hass: HomeAssistant,
|
||||
music_assistant_client: MagicMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test media_player get_queue action."""
|
||||
await setup_integration_from_fixtures(hass, music_assistant_client)
|
||||
entity_id = "media_player.test_group_player_1"
|
||||
response = await hass.services.async_call(
|
||||
MASS_DOMAIN,
|
||||
SERVICE_GET_QUEUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
# no call is made, this info comes from the cached queue data
|
||||
assert music_assistant_client.send_command.call_count == 0
|
||||
assert response == snapshot(exclude=paths(f"{entity_id}.elapsed_time"))
|
||||
|
|
Loading…
Reference in New Issue