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.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.issue_registry import (
|
from homeassistant.helpers.issue_registry import (
|
||||||
IssueSeverity,
|
IssueSeverity,
|
||||||
async_create_issue,
|
async_create_issue,
|
||||||
async_delete_issue,
|
async_delete_issue,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .actions import register_actions
|
||||||
from .const import DOMAIN, LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from music_assistant_models.event import MassEvent
|
from music_assistant_models.event import MassEvent
|
||||||
|
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||||
|
|
||||||
CONNECT_TIMEOUT = 10
|
CONNECT_TIMEOUT = 10
|
||||||
LISTEN_READY_TIMEOUT = 30
|
LISTEN_READY_TIMEOUT = 30
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
type MusicAssistantConfigEntry = ConfigEntry[MusicAssistantEntryData]
|
type MusicAssistantConfigEntry = ConfigEntry[MusicAssistantEntryData]
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,6 +50,12 @@ class MusicAssistantEntryData:
|
||||||
listen_task: asyncio.Task
|
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(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: MusicAssistantConfigEntry
|
hass: HomeAssistant, entry: MusicAssistantConfigEntry
|
||||||
) -> bool:
|
) -> 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_MASS_PLAYER_TYPE = "mass_player_type"
|
||||||
ATTR_ACTIVE_QUEUE = "active_queue"
|
ATTR_ACTIVE_QUEUE = "active_queue"
|
||||||
ATTR_STREAM_TITLE = "stream_title"
|
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__)
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
|
@ -2,6 +2,9 @@
|
||||||
"services": {
|
"services": {
|
||||||
"play_media": { "service": "mdi:play" },
|
"play_media": { "service": "mdi:play" },
|
||||||
"play_announcement": { "service": "mdi:bullhorn" },
|
"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,
|
RepeatMode,
|
||||||
async_process_play_media_url,
|
async_process_play_media_url,
|
||||||
)
|
)
|
||||||
from homeassistant.const import STATE_OFF
|
from homeassistant.const import ATTR_NAME, STATE_OFF
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, ServiceResponse, SupportsResponse
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
import homeassistant.helpers.config_validation as cv
|
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 homeassistant.util.dt import utc_from_timestamp
|
||||||
|
|
||||||
from . import MusicAssistantConfigEntry
|
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 .entity import MusicAssistantEntity
|
||||||
from .media_browser import async_browse_media
|
from .media_browser import async_browse_media
|
||||||
|
from .schemas import QUEUE_DETAILS_SCHEMA, queue_item_dict_from_mass_item
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from music_assistant_client import MusicAssistantClient
|
from music_assistant_client import MusicAssistantClient
|
||||||
|
@ -89,16 +113,7 @@ QUEUE_OPTION_MAP = {
|
||||||
SERVICE_PLAY_MEDIA_ADVANCED = "play_media"
|
SERVICE_PLAY_MEDIA_ADVANCED = "play_media"
|
||||||
SERVICE_PLAY_ANNOUNCEMENT = "play_announcement"
|
SERVICE_PLAY_ANNOUNCEMENT = "play_announcement"
|
||||||
SERVICE_TRANSFER_QUEUE = "transfer_queue"
|
SERVICE_TRANSFER_QUEUE = "transfer_queue"
|
||||||
ATTR_RADIO_MODE = "radio_mode"
|
SERVICE_GET_QUEUE = "get_queue"
|
||||||
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"
|
|
||||||
|
|
||||||
|
|
||||||
def catch_musicassistant_error[_R, **P](
|
def catch_musicassistant_error[_R, **P](
|
||||||
|
@ -179,6 +194,12 @@ async def async_setup_entry(
|
||||||
},
|
},
|
||||||
"_async_handle_transfer_queue",
|
"_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):
|
class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||||
|
@ -513,6 +534,32 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity):
|
||||||
source_queue_id, target_queue_id, auto_play
|
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(
|
async def async_browse_media(
|
||||||
self,
|
self,
|
||||||
media_content_type: MediaType | str | None = None,
|
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"
|
example: "true"
|
||||||
selector:
|
selector:
|
||||||
boolean:
|
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."
|
"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": {
|
"selector": {
|
||||||
|
@ -119,6 +199,37 @@
|
||||||
"playlist": "Playlist",
|
"playlist": "Playlist",
|
||||||
"radio": "Radio"
|
"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(
|
async def setup_integration_from_fixtures(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
music_assistant_client: MagicMock,
|
music_assistant_client: MagicMock,
|
||||||
) -> None:
|
) -> MockConfigEntry:
|
||||||
"""Set up MusicAssistant integration with fixture data."""
|
"""Set up MusicAssistant integration with fixture data."""
|
||||||
players = create_players_from_fixture()
|
players = create_players_from_fixture()
|
||||||
music_assistant_client.players._players = {x.player_id: x for x in players}
|
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)
|
config_entry.add_to_hass(hass)
|
||||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
return config_entry
|
||||||
|
|
||||||
|
|
||||||
def create_players_from_fixture() -> list[Player]:
|
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',
|
'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
|
from music_assistant_models.media_items import Track
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy import SnapshotAssertion
|
from syrupy import SnapshotAssertion
|
||||||
|
from syrupy.filters import paths
|
||||||
|
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
ATTR_GROUP_MEMBERS,
|
ATTR_GROUP_MEMBERS,
|
||||||
|
@ -32,6 +33,7 @@ from homeassistant.components.music_assistant.media_player import (
|
||||||
ATTR_SOURCE_PLAYER,
|
ATTR_SOURCE_PLAYER,
|
||||||
ATTR_URL,
|
ATTR_URL,
|
||||||
ATTR_USE_PRE_ANNOUNCE,
|
ATTR_USE_PRE_ANNOUNCE,
|
||||||
|
SERVICE_GET_QUEUE,
|
||||||
SERVICE_PLAY_ANNOUNCEMENT,
|
SERVICE_PLAY_ANNOUNCEMENT,
|
||||||
SERVICE_PLAY_MEDIA_ADVANCED,
|
SERVICE_PLAY_MEDIA_ADVANCED,
|
||||||
SERVICE_TRANSFER_QUEUE,
|
SERVICE_TRANSFER_QUEUE,
|
||||||
|
@ -583,3 +585,25 @@ async def test_media_player_transfer_queue_action(
|
||||||
auto_play=None,
|
auto_play=None,
|
||||||
require_schema=25,
|
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