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