diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 0000000..6f48d18 --- /dev/null +++ b/.github/workflows/pre-commit.yaml @@ -0,0 +1,14 @@ +name: pre-commit + +on: + pull_request: + push: + branches: [main, dev] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: pre-commit/action@v3.0.1 diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..a6ddd64 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,58 @@ +# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml + +target-version = "py310" +[lint] +select = ["ALL"] + +# All the ones without a comment were the ones that are currently violated +# by the codebase. The plan is to fix them all (when sensible) and then enable them. +ignore = [ + "ANN", + "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {name} + "D401", # First line of docstring should be in imperative mood + "E501", # line too long + "FBT001", # Boolean positional arg in function definition + "FBT002", # Boolean default value in function definition + "FIX004", # Line contains HACK, consider resolving the issue + "PD901", # df is a bad variable name. Be kinder to your future self. + "PERF203", # `try`-`except` within a loop incurs performance overhead + "PLR0913", # Too many arguments to function call (N > 5) + "PLR2004", # Magic value used in comparison, consider replacing X with a constant variable + "S101", # Use of assert detected + "SLF001", # Private member accessed + "RUF015", # Prefer `next(...)` over single element slices +] + +[lint.per-file-ignores] +"tests/*.py" = [ + "ARG001", # Unused function argument: `call` + "D100", # Missing docstring in public module + "D103", # Missing docstring in public function + "D205", # 1 blank line required between summary line and description + "D400", # First line should end with a period + "D415", # First line should end with a period, question mark, or + "DTZ001", # The use of `datetime.datetime()` without `tzinfo` + "ERA001", # Found commented-out code + "FBT003", # Boolean positional value in function call + "FIX002", # Line contains TODO, consider resolving the issue + "G004", # Logging statement uses f-string + "PLR0915", # Too many statements (94 > 50) + "PT004", # Fixture `cleanup` does not return anything, add leading underscore + "PT007", # Wrong values type in `@pytest.mark.parametrize` expected `list` of + "S311", # Standard pseudo-random generators are not suitable for cryptographic + "TD002", # Missing author in TODO; try: `# TODO(): ...` or `# TODO + "TD003", # Missing issue link on the line following this TODO +] +".github/*py" = ["INP001"] +"webapp/homeassistant_util_color.py" = ["ALL"] +"webapp/app.py" = ["INP001", "DTZ011", "A002"] +"custom_components/adaptive_lighting/homeassistant_util_color.py" = ["ALL"] + +[lint.flake8-pytest-style] +fixture-parentheses = false + +[lint.pyupgrade] +keep-runtime-typing = true + +[lint.mccabe] +max-complexity = 25 diff --git a/custom_components/mass_queue/__init__.py b/custom_components/mass_queue/__init__.py index 0fcd9e5..4b0c104 100644 --- a/custom_components/mass_queue/__init__.py +++ b/custom_components/mass_queue/__init__.py @@ -1,32 +1,33 @@ +"""Initialize component.""" + from __future__ import annotations import asyncio from dataclasses import dataclass from typing import TYPE_CHECKING -from music_assistant_client import MusicAssistantClient -from music_assistant_client.exceptions import CannotConnect, InvalidServerVersion -from music_assistant_models.errors import ActionUnavailable, MusicAssistantError - from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, device_registry as dr +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.issue_registry import ( IssueSeverity, async_create_issue, async_delete_issue, ) +from music_assistant_client import MusicAssistantClient +from music_assistant_client.exceptions import CannotConnect, InvalidServerVersion +from music_assistant_models.errors import ActionUnavailable, MusicAssistantError from .actions import get_music_assistant_client, setup_controller_and_actions from .const import DOMAIN, LOGGER if TYPE_CHECKING: - from homeassistant.helpers.typing import ConfigType + from homeassistant.core import HomeAssistant + from homeassistant.helpers.typing import ConfigType, Event -# PLATFORMS = [Platform.MEDIA_PLAYER] PLATFORMS = [] CONNECT_TIMEOUT = 10 @@ -45,14 +46,15 @@ class MusicAssistantEntryData: listen_task: asyncio.Task -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: ARG001 """Set up the Music Assistant component.""" setup_controller_and_actions(hass) return True async def async_setup_entry( - hass: HomeAssistant, entry: MusicAssistantConfigEntry + hass: HomeAssistant, + entry: MusicAssistantConfigEntry, ) -> bool: """Set up Music Assistant from a config entry.""" http_session = async_get_clientsession(hass, verify_ssl=False) @@ -63,8 +65,9 @@ async def async_setup_entry( async with asyncio.timeout(CONNECT_TIMEOUT): await mass.connect() except (TimeoutError, CannotConnect) as err: + exc = f"Failed to connect to music assistant server {mass_url}" raise ConfigEntryNotReady( - f"Failed to connect to music assistant server {mass_url}" + exc, ) from err except InvalidServerVersion as err: async_create_issue( @@ -75,21 +78,23 @@ async def async_setup_entry( severity=IssueSeverity.ERROR, translation_key="invalid_server_version", ) - raise ConfigEntryNotReady(f"Invalid server version: {err}") from err + exc = f"Invalid server version: {err}" + raise ConfigEntryNotReady(exc) from err except MusicAssistantError as err: LOGGER.exception("Failed to connect to music assistant server", exc_info=err) + exc = f"Unknown error connecting to the Music Assistant server {mass_url}" raise ConfigEntryNotReady( - f"Unknown error connecting to the Music Assistant server {mass_url}" + exc, ) from err async_delete_issue(hass, DOMAIN, "invalid_server_version") - async def on_hass_stop(event: Event) -> None: + async def on_hass_stop(event: Event) -> None: # noqa: ARG001 """Handle incoming stop event from Home Assistant.""" await mass.disconnect() entry.async_on_unload( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop), ) # launch the music assistant client listen task in the background @@ -102,7 +107,8 @@ async def on_hass_stop(event: Event) -> None: await init_ready.wait() except TimeoutError as err: listen_task.cancel() - raise ConfigEntryNotReady("Music Assistant client not ready") from err + exc = "Music Assistant client not ready" + raise ConfigEntryNotReady(exc) from err # store the listen task and mass client in the entry data entry.runtime_data = MusicAssistantEntryData(mass, listen_task) @@ -158,7 +164,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_remove_config_entry_device( - hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry + hass: HomeAssistant, + config_entry: ConfigEntry, + device_entry: dr.DeviceEntry, ) -> bool: """Remove a config entry from a device.""" player_id = next( diff --git a/custom_components/mass_queue/actions.py b/custom_components/mass_queue/actions.py index 9545cab..8469164 100644 --- a/custom_components/mass_queue/actions.py +++ b/custom_components/mass_queue/actions.py @@ -1,4 +1,7 @@ +"""Actions for integration.""" + from __future__ import annotations + from typing import TYPE_CHECKING from homeassistant.config_entries import ConfigEntryState @@ -9,60 +12,66 @@ SupportsResponse, callback, ) -from music_assistant_client import MusicAssistantClient from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er -from .controller import MassQueueController from .const import ( - DOMAIN, - SERVICE_GET_QUEUE_ITEMS, - SERVICE_PLAY_QUEUE_ITEM, - SERVICE_REMOVE_QUEUE_ITEM, - SERVICE_MOVE_QUEUE_ITEM_UP, - SERVICE_MOVE_QUEUE_ITEM_DOWN, - SERVICE_MOVE_QUEUE_ITEM_NEXT, - ATTR_QUEUE_ITEM_ID, - ATTR_MEDIA_TITLE, + ATTR_LIMIT, + ATTR_LIMIT_AFTER, + ATTR_LIMIT_BEFORE, ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ARTIST, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_IMAGE, - ATTR_PLAYER_ENTITY, - ATTR_LIMIT, + ATTR_MEDIA_TITLE, ATTR_OFFSET, - ATTR_LIMIT_BEFORE, - ATTR_LIMIT_AFTER, + ATTR_PLAYER_ENTITY, + ATTR_QUEUE_ITEM_ID, DEFAULT_QUEUE_ITEMS_LIMIT, DEFAULT_QUEUE_ITEMS_OFFSET, + DOMAIN, + SERVICE_GET_QUEUE_ITEMS, + SERVICE_MOVE_QUEUE_ITEM_DOWN, + SERVICE_MOVE_QUEUE_ITEM_NEXT, + SERVICE_MOVE_QUEUE_ITEM_UP, + SERVICE_PLAY_QUEUE_ITEM, + SERVICE_REMOVE_QUEUE_ITEM, ) +from .controller import MassQueueController from .schemas import ( + MOVE_QUEUE_ITEM_DOWN_SERVICE_SCHEMA, + MOVE_QUEUE_ITEM_NEXT_SERVICE_SCHEMA, + MOVE_QUEUE_ITEM_UP_SERVICE_SCHEMA, + PLAY_QUEUE_ITEM_SERVICE_SCHEMA, QUEUE_ITEM_SCHEMA, QUEUE_ITEMS_SERVICE_SCHEMA, - PLAY_QUEUE_ITEM_SERVICE_SCHEMA, REMOVE_QUEUE_ITEM_SERVICE_SCHEMA, - MOVE_QUEUE_ITEM_UP_SERVICE_SCHEMA, - MOVE_QUEUE_ITEM_DOWN_SERVICE_SCHEMA, - MOVE_QUEUE_ITEM_NEXT_SERVICE_SCHEMA, ) if TYPE_CHECKING: + from music_assistant_client import MusicAssistantClient + from . import MassQueueEntryData class MassQueueActions: + """Class to manage Music Assistant actions without passing `hass` and `mass_client` each time.""" + def __init__(self, hass: HomeAssistant, mass_client: MusicAssistantClient): + """Initialize class.""" self._hass: HomeAssistant = hass self._client: MusicAssistantClient = mass_client self._controller = MassQueueController(self._hass, self._client) def setup_controller(self): + """Setup Music Assistant controller.""" self._controller.update_players() self._controller.subscribe_events() self._hass.loop.create_task(self._controller.update_queues()) @callback def register_actions(self) -> None: + """Register actions with Home Assistant.""" self._hass.services.async_register( DOMAIN, SERVICE_GET_QUEUE_ITEMS, @@ -108,37 +117,33 @@ def register_actions(self) -> None: ) def get_queue_id(self, entity_id: str): + """Get the queue ID for a player.""" registry = er.async_get(self._hass) entity = registry.async_get(entity_id) return entity.unique_id async def get_queue_index(self, entity_id: str): + """Get the current index of the queue.""" active_queue = await self.get_active_queue(entity_id) - idx = active_queue.current_index - return idx + return active_queue.current_index async def get_active_queue(self, entity_id: str): + """Get active queue details.""" queue_id = self.get_queue_id(entity_id) - queue = await self._client.player_queues.get_active_queue(queue_id) - return queue + return await self._client.player_queues.get_active_queue(queue_id) def _format_queue_item(self, queue_item: dict) -> dict: + """Format list of queue items for response.""" queue_item = queue_item.to_dict() media = queue_item["media_item"] queue_item_id = queue_item["queue_item_id"] media_title = media["name"] media_album = media.get("album") - if media_album is None: - media_album_name = "" - else: - media_album_name = media_album.get("name", "") + media_album_name = "" if media_album is None else media_album.get("name", "") media_content_id = media["uri"] img = queue_item.get("image") - if img is None: - media_image = "" - else: - media_image = img.get("path", "") + media_image = "" if img is None else img.get("path", "") artists = media["artists"] artist_names = [artist["name"] for artist in artists] @@ -151,11 +156,12 @@ def _format_queue_item(self, queue_item: dict) -> dict: ATTR_MEDIA_ARTIST: media_artist, ATTR_MEDIA_CONTENT_ID: media_content_id, ATTR_MEDIA_IMAGE: media_image, - } + }, ) return response async def get_queue_items(self, call: ServiceCall) -> ServiceResponse: + """Get all items in queue.""" entity_id = call.data[ATTR_PLAYER_ENTITY] queue_id = self.get_queue_id(entity_id) offset = call.data.get(ATTR_OFFSET) @@ -166,10 +172,7 @@ async def get_queue_items(self, call: ServiceCall) -> ServiceResponse: if limit_before: offset = idx - limit_before if limit_after: - if limit_before: - limit = limit_before + limit_after + 1 - else: - limit = limit_after + 1 + limit = limit_before + limit_after + 1 if limit_before else limit_after + 1 if offset is None: offset = idx + DEFAULT_QUEUE_ITEMS_OFFSET if limit is None: @@ -177,49 +180,59 @@ async def get_queue_items(self, call: ServiceCall) -> ServiceResponse: offset = max(offset, 0) queue_items = await self._controller.player_queue(queue_id, limit, offset) response: ServiceResponse = { - entity_id: [self._format_queue_item(item) for item in queue_items] + entity_id: [self._format_queue_item(item) for item in queue_items], } return response async def play_queue_item(self, call: ServiceCall) -> ServiceResponse: + """Play selected item in queue.""" entity_id = call.data[ATTR_PLAYER_ENTITY] queue_item_id = call.data[ATTR_QUEUE_ITEM_ID] queue_id = self.get_queue_id(entity_id) await self._client.send_command( - "player_queues/play_index", queue_id=queue_id, index=queue_item_id + "player_queues/play_index", + queue_id=queue_id, + index=queue_item_id, ) async def remove_queue_item(self, call: ServiceCall) -> ServiceResponse: + """Remove selected item from queue.""" entity_id = call.data[ATTR_PLAYER_ENTITY] queue_item_id = call.data[ATTR_QUEUE_ITEM_ID] queue_id = self.get_queue_id(entity_id) await self._client.player_queues.queue_command_delete(queue_id, queue_item_id) async def move_queue_item_up(self, call: ServiceCall) -> ServiceResponse: + """Move selected item up in queue.""" entity_id = call.data[ATTR_PLAYER_ENTITY] queue_item_id = call.data[ATTR_QUEUE_ITEM_ID] queue_id = self.get_queue_id(entity_id) await self._client.player_queues.queue_command_move_up(queue_id, queue_item_id) async def move_queue_item_down(self, call: ServiceCall) -> ServiceResponse: + """Move selected item down in queue.""" entity_id = call.data[ATTR_PLAYER_ENTITY] queue_item_id = call.data[ATTR_QUEUE_ITEM_ID] queue_id = self.get_queue_id(entity_id) await self._client.player_queues.queue_command_move_down( - queue_id, queue_item_id + queue_id, + queue_item_id, ) async def move_queue_item_next(self, call: ServiceCall) -> ServiceResponse: + """Move selected item next in queue.""" entity_id = call.data[ATTR_PLAYER_ENTITY] queue_item_id = call.data[ATTR_QUEUE_ITEM_ID] queue_id = self.get_queue_id(entity_id) await self._client.player_queues.queue_command_move_next( - queue_id, queue_item_id + queue_id, + queue_item_id, ) @callback def get_music_assistant_client_boostrap(hass: HomeAssistant) -> MusicAssistantClient: + """Get Music Assistant Client by finding its domain.""" mass_domain = "music_assistant" entries = hass.config_entries.async_entries() config_entry = [entry for entry in entries if entry.domain == mass_domain][0] @@ -228,8 +241,10 @@ def get_music_assistant_client_boostrap(hass: HomeAssistant) -> MusicAssistantCl @callback def get_music_assistant_client( - hass: HomeAssistant, entity_id: str + hass: HomeAssistant, + entity_id: str, ) -> MusicAssistantClient: + """Get Music Assistant client from entity_id.""" registry = er.async_get(hass) entity = registry.async_get(entity_id) config_entry_id = entity.config_entry_id @@ -238,20 +253,26 @@ def get_music_assistant_client( @callback def _get_music_assistant_client( - hass: HomeAssistant, config_entry_id: str + hass: HomeAssistant, + config_entry_id: str, ) -> MusicAssistantClient: + """Get Music Assistant Client from config_entry_id.""" entry: MassQueueEntryData | None if not (entry := hass.config_entries.async_get_entry(config_entry_id)): - raise ServiceValidationError("Entry not found") + exc = "Entry not found." + raise ServiceValidationError(exc) if entry.state is not ConfigEntryState.LOADED: - raise ServiceValidationError("Entry not loaded") + exc = "Entry not loaded" + raise ServiceValidationError(exc) return entry.runtime_data.mass @callback def setup_controller_and_actions( - hass: HomeAssistant, mass_client: MusicAssistantClient | None = None + hass: HomeAssistant, + mass_client: MusicAssistantClient | None = None, ) -> MassQueueActions: + """Initialize client and actions class, add actions to Home Assistant.""" if mass_client is None: mass_client = get_music_assistant_client_boostrap(hass) actions = MassQueueActions(hass, mass_client) diff --git a/custom_components/mass_queue/config_flow.py b/custom_components/mass_queue/config_flow.py index ada0fc7..f67a3fa 100644 --- a/custom_components/mass_queue/config_flow.py +++ b/custom_components/mass_queue/config_flow.py @@ -1,24 +1,28 @@ +"""Config flow for integration.""" + from __future__ import annotations from typing import TYPE_CHECKING, Any +import voluptuous as vol +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_URL +from homeassistant.helpers import aiohttp_client from music_assistant_client import MusicAssistantClient from music_assistant_client.exceptions import ( CannotConnect, InvalidServerVersion, MusicAssistantClientException, ) - -import voluptuous as vol from music_assistant_models.api import ServerInfoMessage -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_URL -from homeassistant.core import HomeAssistant -from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from .const import DOMAIN, LOGGER +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant + from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo + + DEFAULT_URL = "http://mass.local:8095" DEFAULT_TITLE = "Music Assistant Queue Items" @@ -29,14 +33,15 @@ def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema: return vol.Schema( { vol.Required(CONF_URL, default=default_url): str, - } + }, ) async def get_server_info(hass: HomeAssistant, url: str) -> ServerInfoMessage: """Validate the user input allows us to connect.""" async with MusicAssistantClient( - url, aiohttp_client.async_get_clientsession(hass) + url, + aiohttp_client.async_get_clientsession(hass), ) as client: if TYPE_CHECKING: assert client.server_info is not None @@ -53,17 +58,20 @@ def __init__(self) -> None: self.server_info: ServerInfoMessage | None = None async def async_step_user( - self, user_input: dict[str, Any] | None = None + self, + user_input: dict[str, Any] | None = None, ) -> ConfigFlowResult: """Handle a manual configuration.""" errors: dict[str, str] = {} if user_input is not None: try: self.server_info = await get_server_info( - self.hass, user_input[CONF_URL] + self.hass, + user_input[CONF_URL], ) await self.async_set_unique_id( - self.server_info.server_id, raise_on_progress=False + self.server_info.server_id, + raise_on_progress=False, ) self._abort_if_unique_id_configured( updates={CONF_URL: self.server_info.base_url}, @@ -85,13 +93,16 @@ async def async_step_user( ) return self.async_show_form( - step_id="user", data_schema=get_manual_schema(user_input), errors=errors + step_id="user", + data_schema=get_manual_schema(user_input), + errors=errors, ) return self.async_show_form(step_id="user", data_schema=get_manual_schema({})) async def async_step_zeroconf( - self, discovery_info: ZeroconfServiceInfo + self, + discovery_info: ZeroconfServiceInfo, ) -> ConfigFlowResult: """Handle a discovered Mass server. @@ -116,7 +127,8 @@ async def async_step_zeroconf( return await self.async_step_discovery_confirm() async def async_step_discovery_confirm( - self, user_input: dict[str, Any] | None = None + self, + user_input: dict[str, Any] | None = None, ) -> ConfigFlowResult: """Handle user-confirmation of discovered server.""" if TYPE_CHECKING: diff --git a/custom_components/mass_queue/controller.py b/custom_components/mass_queue/controller.py index b2750a1..07a2c4b 100644 --- a/custom_components/mass_queue/controller.py +++ b/custom_components/mass_queue/controller.py @@ -1,20 +1,28 @@ +"""Controller for queues, players cache.""" + from __future__ import annotations -from homeassistant.core import HomeAssistant +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from homeassistant.core import HomeAssistant from music_assistant_models.enums import EventType from .const import ( DEFAULT_QUEUE_ITEMS_LIMIT, DEFAULT_QUEUE_ITEMS_OFFSET, LOGGER, - MUSIC_ASSISTANT_EVENT_DOMAIN, MASS_QUEUE_EVENT_DOMAIN, + MUSIC_ASSISTANT_EVENT_DOMAIN, ) -from .utils import get_queue_id_from_player_data, format_queue_updated_event_data +from .utils import format_queue_updated_event_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): + """Initialize class.""" self._client = mass_client self._hass = hass self.players = Players(hass) @@ -22,21 +30,23 @@ def __init__(self, hass: HomeAssistant, mass_client): # 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.on_queue_items_update_event, + EventType.QUEUE_ITEMS_UPDATED, ) self._client.subscribe(self.on_player_event, EventType.PLAYER_UPDATED) - return 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}" + f"Sending event type {MUSIC_ASSISTANT_EVENT_DOMAIN}, data {event_data}", ) self._hass.bus.async_fire(MUSIC_ASSISTANT_EVENT_DOMAIN, event_data) - return 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 @@ -51,6 +61,7 @@ def on_queue_update_event(self, event): 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 @@ -65,6 +76,7 @@ def on_queue_items_update_event(self, event): 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 @@ -82,6 +94,7 @@ def on_player_event(self, event): # All players def get_all_players(self): + """Get all Music Assistant players.""" players = self._client.players.players result = {} for player_data in players: @@ -91,33 +104,35 @@ def get_all_players(self): 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) - return 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) - result = await self.get_queue(queue_id) - return result + 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] - result = {queue_id: await self.get_queue(queue_id) for queue_id in queue_ids} - return result + 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) @@ -129,21 +144,21 @@ async def player_queue( 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 Exception: + except IndexError: offset = 0 offset = max(offset, 0) - result = queue[offset : offset + limit] - return result + return queue[offset : offset + limit] 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) - return async def get_queue( self, @@ -151,36 +166,43 @@ async def get_queue( 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 Exception: + except IndexError: offset = 0 offset = max(offset, 0) - queue_items = await self._client.player_queues.get_player_queue_items( - queue_id=queue_id, limit=limit, offset=offset + return await self._client.player_queues.get_player_queue_items( + queue_id=queue_id, + limit=limit, + offset=offset, ) - return queue_items async def get_active_queue(self, queue_id: str): - result = await self._client.get_active_queue(queue_id) - return result + """Get the active queue for a single queue.""" + return await self._client.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) - idx = active_queue.current_index - return idx + return active_queue.current_index class Players: - def __init__(self, hass: HomeAssistant, players: dict = {}): - self.players = 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", @@ -189,12 +211,14 @@ def add(self, player_id: str, queue_id: str | None): 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 = { @@ -206,6 +230,7 @@ def remove(self, player_id: str): 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] @@ -219,37 +244,44 @@ def update(self, player_id: str, queue_id: str): 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) - return class Queues: - def __init__(self, hass: HomeAssistant, queues: dict = {}): - self.queues = queues + """Class to hold all queue caches.""" + + def __init__(self, hass: HomeAssistant, queues: dict | None = None): + """Initialize class.""" + self.queues = queues if queues else {} self._hass = hass - return def get(self, queue_id): + """Returns cached queue records.""" return self.queues[queue_id] def add(self, queue_id: str, queue_items: int): + """Adds a single queue.""" self.queues[queue_id] = queue_items 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.queues[k] = v 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] = queue_items 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) @@ -257,6 +289,6 @@ def remove(self, 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) - return diff --git a/custom_components/mass_queue/schemas.py b/custom_components/mass_queue/schemas.py index 4eb3f76..1fa5f9b 100644 --- a/custom_components/mass_queue/schemas.py +++ b/custom_components/mass_queue/schemas.py @@ -1,21 +1,23 @@ +"""Schemas.""" + from __future__ import annotations import voluptuous as vol from homeassistant.helpers import config_validation as cv from .const import ( - ATTR_QUEUE_ITEM_ID, - ATTR_MEDIA_TITLE, + ATTR_LIMIT, + ATTR_LIMIT_AFTER, + ATTR_LIMIT_BEFORE, ATTR_MEDIA_ALBUM_NAME, ATTR_MEDIA_ARTIST, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_IMAGE, - ATTR_QUEUE_ITEMS, - ATTR_PLAYER_ENTITY, + ATTR_MEDIA_TITLE, ATTR_OFFSET, - ATTR_LIMIT, - ATTR_LIMIT_BEFORE, - ATTR_LIMIT_AFTER, + ATTR_PLAYER_ENTITY, + ATTR_QUEUE_ITEM_ID, + ATTR_QUEUE_ITEMS, ) QUEUE_ITEM_SCHEMA = vol.Schema( @@ -26,15 +28,16 @@ vol.Required(ATTR_MEDIA_ARTIST): str, vol.Required(ATTR_MEDIA_CONTENT_ID): str, vol.Required(ATTR_MEDIA_IMAGE): str, - } + }, ) QUEUE_DETAILS_SCHEMA = vol.Schema( { vol.Required(ATTR_QUEUE_ITEMS): vol.All( - cv.ensure_list, [vol.Schema(QUEUE_ITEM_SCHEMA)] - ) - } + cv.ensure_list, + [vol.Schema(QUEUE_ITEM_SCHEMA)], + ), + }, ) QUEUE_ITEMS_SERVICE_SCHEMA = vol.Schema( @@ -44,35 +47,35 @@ vol.Optional(ATTR_LIMIT): int, vol.Optional(ATTR_LIMIT_BEFORE): int, vol.Optional(ATTR_LIMIT_AFTER): int, - } + }, ) PLAY_QUEUE_ITEM_SERVICE_SCHEMA = vol.Schema( { vol.Required(ATTR_PLAYER_ENTITY): str, vol.Required(ATTR_QUEUE_ITEM_ID): str, - } + }, ) REMOVE_QUEUE_ITEM_SERVICE_SCHEMA = vol.Schema( { vol.Required(ATTR_PLAYER_ENTITY): str, vol.Required(ATTR_QUEUE_ITEM_ID): str, - } + }, ) MOVE_QUEUE_ITEM_UP_SERVICE_SCHEMA = vol.Schema( { vol.Required(ATTR_PLAYER_ENTITY): str, vol.Required(ATTR_QUEUE_ITEM_ID): str, - } + }, ) MOVE_QUEUE_ITEM_DOWN_SERVICE_SCHEMA = vol.Schema( { vol.Required(ATTR_PLAYER_ENTITY): str, vol.Required(ATTR_QUEUE_ITEM_ID): str, - } + }, ) MOVE_QUEUE_ITEM_NEXT_SERVICE_SCHEMA = vol.Schema( { vol.Required(ATTR_PLAYER_ENTITY): str, vol.Required(ATTR_QUEUE_ITEM_ID): str, - } + }, ) diff --git a/custom_components/mass_queue/utils.py b/custom_components/mass_queue/utils.py index 146e6e7..7e1e061 100644 --- a/custom_components/mass_queue/utils.py +++ b/custom_components/mass_queue/utils.py @@ -1,4 +1,8 @@ +"""Utilities.""" + + def format_event_data_queue_item(queue_item): + """Format event data results for usage by controller.""" if queue_item is None: return None if queue_item.get("queue_id") is None: @@ -11,23 +15,20 @@ def format_event_data_queue_item(queue_item): return item_cp -def format_queue_updated_event_data(event): +def format_queue_updated_event_data(event: dict): + """Format queue updated results for usage by controller.""" event_data = event.copy() event_data["current_item"] = format_event_data_queue_item( - event_data.get("current_item") + event_data.get("current_item"), ) event_data["next_item"] = format_event_data_queue_item(event_data.get("next_item")) return event_data def get_queue_id_from_player_data(player_data): - """Force as dict if not already""" - if type(player_data) is not dict: - data = player_data.to_dict() - else: - data = player_data + """Force as dict if not already.""" + data = player_data.to_dict() if type(player_data) is not dict else player_data current_media = data.get("current_media", None) if current_media is None: return None - queue_id = current_media.get("queue_id") - return queue_id + return current_media.get("queue_id")