Files
Home-Assistant/custom_components/mass_queue/controller.py
T
2026-07-17 10:12:15 -04:00

412 lines
15 KiB
Python

# 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))