Added Alexa Music
This commit is contained in:
@@ -0,0 +1,411 @@
|
||||
# ty:ignore[unresolved-import]
|
||||
"""Controller for queues, players cache."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.core import callback
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from music_assistant_client import MusicAssistantClient
|
||||
|
||||
from music_assistant_models.enums import EventType
|
||||
|
||||
from .const import (
|
||||
CONF_DOWNLOAD_LOCAL,
|
||||
DEFAULT_QUEUE_ITEMS_LIMIT,
|
||||
DEFAULT_QUEUE_ITEMS_OFFSET,
|
||||
LOGGER,
|
||||
MASS_QUEUE_EVENT_DOMAIN,
|
||||
MUSIC_ASSISTANT_EVENT_DOMAIN,
|
||||
)
|
||||
from .utils import (
|
||||
download_and_encode_image,
|
||||
find_image,
|
||||
format_queue_updated_event_data,
|
||||
generate_image_url_from_image_data,
|
||||
get_queue_id_from_player_data,
|
||||
)
|
||||
|
||||
|
||||
class MassQueueController:
|
||||
"""Controller to hold methods, handle events, and control caches of players and queues."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
mass_client: MusicAssistantClient,
|
||||
config_entry: ConfigEntry,
|
||||
):
|
||||
"""Initialize class."""
|
||||
self._client = mass_client
|
||||
self._hass = hass
|
||||
self.players = Players(hass)
|
||||
self.queues = Queues(hass, mass_client, config_entry)
|
||||
self._config_entry = config_entry
|
||||
self._download_local = config_entry.options.get(CONF_DOWNLOAD_LOCAL)
|
||||
|
||||
# Events
|
||||
def subscribe_events(self):
|
||||
"""Subscribe to Music Assistant events."""
|
||||
self._client.subscribe(self.on_queue_update_event, EventType.QUEUE_UPDATED)
|
||||
self._client.subscribe(
|
||||
self.on_queue_items_update_event,
|
||||
EventType.QUEUE_ITEMS_UPDATED,
|
||||
)
|
||||
self._client.subscribe(self.on_player_event, EventType.PLAYER_UPDATED)
|
||||
|
||||
def send_ha_event(self, event_data):
|
||||
"""Send event to Home Assistant."""
|
||||
LOGGER.debug(
|
||||
f"Sending event type {MUSIC_ASSISTANT_EVENT_DOMAIN}, data {event_data}",
|
||||
)
|
||||
self._hass.bus.async_fire(MUSIC_ASSISTANT_EVENT_DOMAIN, event_data)
|
||||
|
||||
def on_queue_update_event(self, event):
|
||||
"""Callback when queue update event is received."""
|
||||
LOGGER.debug("Got updated queue.")
|
||||
event_type = event.event
|
||||
event_object_id = event.object_id
|
||||
event_data = event.data
|
||||
event_queue_id = event_data.get("queue_id")
|
||||
self._hass.loop.create_task(self.update_queue_items(event_queue_id))
|
||||
if event_data is None:
|
||||
LOGGER.error(f"Event data is empty! Event: {event}")
|
||||
return
|
||||
data = format_queue_updated_event_data(event_data)
|
||||
ha_event_data = {"type": event_type, "object_id": event_object_id, "data": data}
|
||||
self.send_ha_event(ha_event_data)
|
||||
|
||||
def on_queue_items_update_event(self, event):
|
||||
"""Callback when queue items update event is received."""
|
||||
LOGGER.debug("Got updated queue items.")
|
||||
event_type = event.event
|
||||
event_object_id = event.object_id
|
||||
event_data = event.data
|
||||
event_queue_id = event_data.get("queue_id")
|
||||
self._hass.loop.create_task(self.update_queue_items(event_queue_id))
|
||||
if event_data is None:
|
||||
LOGGER.error(f"Event data is empty! Event: {event}")
|
||||
return
|
||||
data = format_queue_updated_event_data(event_data)
|
||||
ha_event_data = {"type": event_type, "object_id": event_object_id, "data": data}
|
||||
self.send_ha_event(ha_event_data)
|
||||
|
||||
def on_player_event(self, event):
|
||||
"""Callback when player event is received."""
|
||||
event_type = event.event
|
||||
event_object_id = event.object_id
|
||||
event_data = event.data
|
||||
event_player = event_data["player_id"]
|
||||
self.update_player_queue(event_player)
|
||||
if event_data is None:
|
||||
LOGGER.error(f"Event data is empty! Event: {event}")
|
||||
return
|
||||
ha_event_data = {
|
||||
"type": event_type,
|
||||
"object_id": event_object_id,
|
||||
"data": event.data,
|
||||
}
|
||||
self.send_ha_event(ha_event_data)
|
||||
|
||||
# All players
|
||||
def get_all_players(self):
|
||||
"""Get all Music Assistant players."""
|
||||
players = self._client.players.players
|
||||
result = {}
|
||||
for player_data in players:
|
||||
player_id = player_data.player_id
|
||||
queue_id = get_queue_id_from_player_data(player_data)
|
||||
result[player_id] = queue_id
|
||||
return result
|
||||
|
||||
def update_players(self):
|
||||
"""Update all Music Assistant players."""
|
||||
LOGGER.debug("Updating all players.")
|
||||
players = self.get_all_players()
|
||||
self.players.batch_add(players)
|
||||
|
||||
# Individual players
|
||||
def update_player_queue(self, player_id: str):
|
||||
"""Update queue items for single Music Assistant queue."""
|
||||
LOGGER.debug(f"Updating player {player_id}.")
|
||||
player = self._client.players.get(player_id)
|
||||
if player is None:
|
||||
self.players.remove(player_id)
|
||||
queue_id = get_queue_id_from_player_data(player)
|
||||
self.players.update(player_id, queue_id)
|
||||
|
||||
async def send_command(self, command: str, data: dict | None = None):
|
||||
"""Sends command to Music Assistant and returns response."""
|
||||
data = data or {}
|
||||
return await self._client.send_command(command, require_schema=None, **data)
|
||||
|
||||
async def get_recommendations(self, providers: list | None = None):
|
||||
"""Pulls all recommendations."""
|
||||
recs = await self._client.music.recommendations()
|
||||
if not providers:
|
||||
return recs
|
||||
rec_providers = []
|
||||
for rec in recs:
|
||||
if rec.provider not in rec_providers:
|
||||
rec_providers.append(rec.provider)
|
||||
|
||||
used_rec_providers = [
|
||||
rec_provider
|
||||
for rec_provider in rec_providers
|
||||
for provider in providers
|
||||
if rec_provider.startswith(provider)
|
||||
]
|
||||
return [rec for rec in recs if rec.provider in used_rec_providers]
|
||||
|
||||
async def get_grouped_volume(self, player_id: str):
|
||||
"""Get the grouped volume for a given player."""
|
||||
return self._client.players.get(player_id).group_volume
|
||||
|
||||
async def set_grouped_volume(self, player_id: str, volume_level: int):
|
||||
"""Sets the grouped volume for a given player."""
|
||||
await self._client.players.set_player_group_volume(player_id, volume_level)
|
||||
|
||||
async def get_player_queue(self, player_id: str):
|
||||
"""Gets queue items for single Music Assistant queue."""
|
||||
player = self._client.players.get(player_id)
|
||||
queue_id = get_queue_id_from_player_data(player)
|
||||
return await self.get_queue(queue_id)
|
||||
|
||||
# All queues
|
||||
async def get_all_queues(self):
|
||||
"""Gets queue items for all Music Assistant queues."""
|
||||
queue_ids = [q.queue_id for q in self._client.player_queues.player_queues]
|
||||
return {queue_id: await self.get_queue(queue_id) for queue_id in queue_ids}
|
||||
|
||||
async def update_queues(self):
|
||||
"""Update queue items for all Music Assistant queues."""
|
||||
LOGGER.debug("Updating all queues.")
|
||||
queues = await self.get_all_queues()
|
||||
self.queues.batch_add(queues)
|
||||
|
||||
# Individual queues
|
||||
async def player_queue(
|
||||
self,
|
||||
queue_id: str,
|
||||
limit: int = DEFAULT_QUEUE_ITEMS_LIMIT,
|
||||
offset: int = DEFAULT_QUEUE_ITEMS_OFFSET,
|
||||
):
|
||||
"""Get the cached queue items for a single queue."""
|
||||
queue = self.queues.get(queue_id)
|
||||
if offset == -1:
|
||||
try:
|
||||
offset = await self.get_queue_index(queue_id) - 5
|
||||
except IndexError:
|
||||
offset = 0
|
||||
offset = max(offset, 0)
|
||||
return queue[offset : offset + limit] if queue else []
|
||||
|
||||
async def update_queue_items(self, queue_id: str):
|
||||
"""Update the queue items for a single queue."""
|
||||
LOGGER.debug(f"Updating queue {queue_id}.")
|
||||
queue = await self.get_queue(queue_id)
|
||||
self.queues.update(queue_id, queue)
|
||||
|
||||
async def get_queue(
|
||||
self,
|
||||
queue_id: str,
|
||||
limit: int = DEFAULT_QUEUE_ITEMS_LIMIT,
|
||||
offset: int = DEFAULT_QUEUE_ITEMS_OFFSET,
|
||||
):
|
||||
"""Get the queue items for a single queue."""
|
||||
if offset == -1:
|
||||
try:
|
||||
offset = await self.get_queue_index(queue_id) - 5
|
||||
except IndexError:
|
||||
offset = 0
|
||||
offset = max(offset, 0)
|
||||
# HA 2025.12 Fix: `get_player_queue_items` replaced with `get_queue_items`
|
||||
try:
|
||||
return await self._client.player_queues.get_queue_items(
|
||||
queue_id=queue_id,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
except AttributeError:
|
||||
return await self._client.player_queues.get_player_queue_items(
|
||||
queue_id=queue_id,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
async def get_active_queue(self, queue_id: str):
|
||||
"""Get the active queue for a single queue."""
|
||||
return await self._client.player_queues.get_active_queue(queue_id)
|
||||
|
||||
async def get_queue_index(self, queue_id: str):
|
||||
"""Get the active queue index for a single queue."""
|
||||
active_queue = await self.get_active_queue(queue_id)
|
||||
return active_queue.current_index
|
||||
|
||||
|
||||
class Players:
|
||||
"""Class to hold all player caches."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, players: dict | None = None):
|
||||
"""Initialize class."""
|
||||
self.players = players if players is not None else {}
|
||||
self._hass = hass
|
||||
|
||||
def get(self, player_id):
|
||||
"""Returns cached player records."""
|
||||
return self.players.get(player_id)
|
||||
|
||||
def add(self, player_id: str, queue_id: str | None):
|
||||
"""Adds a single player."""
|
||||
self.players[player_id] = queue_id
|
||||
event_data = {
|
||||
"type": "player_added",
|
||||
"data": {"player_id": player_id, "queue_id": queue_id},
|
||||
}
|
||||
self.send_ha_event(event_data)
|
||||
|
||||
def batch_add(self, players: dict):
|
||||
"""Adds multiple players at once."""
|
||||
for k, v in players.items():
|
||||
self.players[k] = v
|
||||
event_data = {"type": "player_added", "data": {"players": players}}
|
||||
self.send_ha_event(event_data)
|
||||
|
||||
def remove(self, player_id: str):
|
||||
"""Removes a single player."""
|
||||
if player_id in self.players:
|
||||
self.players.pop(player_id)
|
||||
event_data = {
|
||||
"type": "player_removed",
|
||||
"data": {
|
||||
"player_id": player_id,
|
||||
},
|
||||
}
|
||||
self.send_ha_event(event_data)
|
||||
|
||||
def update(self, player_id: str, queue_id: str):
|
||||
"""Updates the queue ID of a single player."""
|
||||
if player_id not in self.players:
|
||||
return
|
||||
current_queue_id = self.players[player_id]
|
||||
if current_queue_id == queue_id:
|
||||
pass
|
||||
self.players[player_id] = queue_id
|
||||
event_data = {
|
||||
"type": "player_updated",
|
||||
"data": {"player_id": player_id, "queue_id": queue_id},
|
||||
}
|
||||
self.send_ha_event(event_data)
|
||||
|
||||
def send_ha_event(self, event_data):
|
||||
"""Send event to Home Assistant."""
|
||||
LOGGER.debug(
|
||||
f"Sending event type {MASS_QUEUE_EVENT_DOMAIN}, data {event_data}",
|
||||
)
|
||||
self._hass.bus.async_fire(MASS_QUEUE_EVENT_DOMAIN, event_data)
|
||||
|
||||
|
||||
class Queues:
|
||||
"""Class to hold all queue caches."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
client: MusicAssistantClient,
|
||||
config_entry: ConfigEntry,
|
||||
queues: dict | None = None,
|
||||
):
|
||||
"""Initialize class."""
|
||||
self.queues = self.batch_add(queues) if queues else {}
|
||||
self._hass = hass
|
||||
self._config_entry = config_entry
|
||||
self._download_local = config_entry.options.get(CONF_DOWNLOAD_LOCAL)
|
||||
self._client = client
|
||||
|
||||
def get(self, queue_id):
|
||||
"""Returns cached queue records."""
|
||||
return self.queues.get(queue_id, [])
|
||||
|
||||
def add(self, queue_id: str, queue_items: int):
|
||||
"""Adds a single queue."""
|
||||
self.process_queue_images(queue_items, queue_id)
|
||||
event_data = {"type": "queue_added", "data": {"queue_id": queue_id}}
|
||||
self.send_ha_event(event_data)
|
||||
|
||||
def batch_add(self, queues):
|
||||
"""Adds multiple queues at once."""
|
||||
for k, v in queues.items():
|
||||
self.process_queue_images(v, k)
|
||||
event_data = {"type": "queues_added", "data": {"queue_id": list(queues.keys())}}
|
||||
self.send_ha_event(event_data)
|
||||
|
||||
def update(self, queue_id, queue_items):
|
||||
"""Updates queue items in record."""
|
||||
self.queues[queue_id] = self.process_queue_images(queue_items, queue_id)
|
||||
event_data = {"type": "queue_updated", "data": {"queue_id": queue_id}}
|
||||
self.send_ha_event(event_data)
|
||||
|
||||
def remove(self, queue_id):
|
||||
"""Removes queue from record."""
|
||||
if queue_id not in self.queues:
|
||||
return
|
||||
self.queues.pop(queue_id)
|
||||
event_data = {"type": "queue_removed", "data": {"queue_id": queue_id}}
|
||||
self.send_ha_event(event_data)
|
||||
|
||||
def send_ha_event(self, event_data):
|
||||
"""Send event to Home Assistant."""
|
||||
LOGGER.debug(
|
||||
f"Sending event type {MASS_QUEUE_EVENT_DOMAIN}, data {event_data}",
|
||||
)
|
||||
self._hass.bus.async_fire(MASS_QUEUE_EVENT_DOMAIN, event_data)
|
||||
|
||||
async def process_image_single_item(self, queue_item: dict):
|
||||
"""Processes the images from a single item."""
|
||||
media_image = find_image(queue_item)
|
||||
if media_image:
|
||||
queue_item["media_image"] = media_image
|
||||
else:
|
||||
queue_item["media_image"] = ""
|
||||
if self._download_local:
|
||||
try:
|
||||
LOGGER.debug("Expected to download locally.")
|
||||
img_data = queue_item["media_item"]["metadata"]["images"][0]
|
||||
url = generate_image_url_from_image_data(img_data, self._client)
|
||||
LOGGER.debug(f"Downloading URL {url}")
|
||||
result = await download_and_encode_image(url)
|
||||
LOGGER.debug("Downloaded and setting")
|
||||
queue_item["local_image_encoded"] = result
|
||||
except Exception as e: # noqa: BLE001
|
||||
LOGGER.debug(
|
||||
f"Received error {e} when downloading image for queue item: {queue_item}",
|
||||
)
|
||||
else:
|
||||
LOGGER.debug("No media image found but not expected to download.")
|
||||
return queue_item
|
||||
|
||||
async def _process_queue_images(self, queue_items: list, queue_id: str):
|
||||
"""Helper to process all images in a given queue."""
|
||||
items = [item if type(item) is dict else item.to_dict() for item in queue_items]
|
||||
try:
|
||||
result = await asyncio.gather(
|
||||
*[self.process_image_single_item(item) for item in items],
|
||||
)
|
||||
except: # noqa: E722
|
||||
LOGGER.error(f"Unable to process queue items {items}!")
|
||||
result = items
|
||||
self.queues[queue_id] = result
|
||||
return result
|
||||
|
||||
@callback
|
||||
def process_queue_images(self, queue_items: list, queue_id: str):
|
||||
"""Processes all images in a given queue."""
|
||||
loop = self._hass.loop
|
||||
LOGGER.debug(f"Processing queue: {queue_items}")
|
||||
loop.create_task(self._process_queue_images(queue_items, queue_id))
|
||||
Reference in New Issue
Block a user