From aeb0895ba1774ef7cd9407081fddad4c872654ed Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Fri, 15 Aug 2025 12:31:51 -0400 Subject: [PATCH 1/8] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a34a142..c679895 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ [activity]: https://github.com/droans/mass_card/commits/main [beta-shield]: https://img.shields.io/github/v/release/droans/mass_queue?include_prereleases&style=for-the-badge&filter=*b*&label=Pre-Release [beta]: https://github.com/droans/mass_queue/releases -[downloads-shield]: https://img.shields.io/github/downloads/custom-cards/button-card/total?style=for-the-badge +[downloads-shield]: https://img.shields.io/github/downloads/droans/mass_queue/total?style=for-the-badge # Music Assistant Queue Actions From 6d115566e5a6f7256249e3700c24329597db76c1 Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Fri, 15 Aug 2025 12:34:25 -0400 Subject: [PATCH 2/8] Remove downloads --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index c679895..ea13b3b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![GitHub Release][release-shield]][release] [![Beta][beta-shield]][beta] -![Downloads][downloads-shield] [![HACS][hacs-badge-shield]][hacs-badge] [![Maintainer][maintainer-shield]][maintainer] @@ -23,7 +22,6 @@ [activity]: https://github.com/droans/mass_card/commits/main [beta-shield]: https://img.shields.io/github/v/release/droans/mass_queue?include_prereleases&style=for-the-badge&filter=*b*&label=Pre-Release [beta]: https://github.com/droans/mass_queue/releases -[downloads-shield]: https://img.shields.io/github/downloads/droans/mass_queue/total?style=for-the-badge # Music Assistant Queue Actions From 05387f49ab54c35366392acc91c30571d7de83de Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Fri, 15 Aug 2025 15:18:26 -0400 Subject: [PATCH 3/8] Linting --- .github/workflows/hacs.yaml | 4 +- .github/workflows/hassfest.yaml | 2 +- .pre-commit-config.yaml | 18 + LICENSE | 2 +- README.md | 4 +- custom_components/mass_queue/__init__.py | 4 +- custom_components/mass_queue/actions.py | 449 +++++++++-------- custom_components/mass_queue/config_flow.py | 3 +- custom_components/mass_queue/const.py | 7 +- custom_components/mass_queue/controller.py | 532 ++++++++++---------- custom_components/mass_queue/manifest.json | 2 +- custom_components/mass_queue/schemas.py | 112 ++--- custom_components/mass_queue/services.yaml | 2 +- custom_components/mass_queue/utils.py | 58 +-- hacs.json | 2 +- 15 files changed, 603 insertions(+), 598 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/hacs.yaml b/.github/workflows/hacs.yaml index 241f862..4547222 100644 --- a/.github/workflows/hacs.yaml +++ b/.github/workflows/hacs.yaml @@ -10,5 +10,5 @@ jobs: steps: - uses: "actions/checkout@v2" - uses: "hacs/action@main" - with: - category: "integration" \ No newline at end of file + with: + category: "integration" diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml index 2126a09..d1c56cb 100644 --- a/.github/workflows/hassfest.yaml +++ b/.github/workflows/hassfest.yaml @@ -9,4 +9,4 @@ jobs: runs-on: "ubuntu-latest" steps: - uses: "actions/checkout@v2" - - uses: home-assistant/actions/hassfest@master \ No newline at end of file + - uses: home-assistant/actions/hassfest@master diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..020789a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,18 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-added-large-files + - id: trailing-whitespace + - id: end-of-file-fixer + - id: mixed-line-ending + args: ["--fix=lf"] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.13 + hooks: + - id: ruff + args: ["--fix"] + - repo: https://github.com/psf/black + rev: 25.1.0 + hooks: + - id: black \ No newline at end of file diff --git a/LICENSE b/LICENSE index c378809..613d2b1 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index ea13b3b..89d891f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ # Music Assistant Queue Actions -Adds new actions to control player queues for Music Assistant +Adds new actions to control player queues for Music Assistant Designed to work with [Music Assistant Queue Card](https://github.com/droans/mass_card) @@ -104,4 +104,4 @@ media_player.music_assistant_speaker: ## Configuration -The integration should automatically detect the active Music Assistant instance and integration. If it does not, add as you normally would from the "Devices & Services" section in the Home Assistant Settings. \ No newline at end of file +The integration should automatically detect the active Music Assistant instance and integration. If it does not, add as you normally would from the "Devices & Services" section in the Home Assistant Settings. diff --git a/custom_components/mass_queue/__init__.py b/custom_components/mass_queue/__init__.py index a03e1bc..8ff3867 100644 --- a/custom_components/mass_queue/__init__.py +++ b/custom_components/mass_queue/__init__.py @@ -122,6 +122,8 @@ async def on_hass_stop(event: Event) -> None: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True + + async def _client_listen( hass: HomeAssistant, entry: ConfigEntry, @@ -183,4 +185,4 @@ async def async_remove_config_entry_device( except ActionUnavailable: return False else: - return True \ No newline at end of file + return True diff --git a/custom_components/mass_queue/actions.py b/custom_components/mass_queue/actions.py index e37f55f..9545cab 100644 --- a/custom_components/mass_queue/actions.py +++ b/custom_components/mass_queue/actions.py @@ -11,237 +11,250 @@ ) from music_assistant_client import MusicAssistantClient from homeassistant.exceptions import ServiceValidationError -from homeassistant.helpers import config_validation as cv, entity_registry as er +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, - LOGGER, - ATTR_QUEUE_ITEM_ID, - ATTR_MEDIA_TITLE, - ATTR_MEDIA_ALBUM_NAME, - ATTR_MEDIA_ARTIST, - ATTR_MEDIA_CONTENT_ID, - ATTR_MEDIA_IMAGE, - ATTR_PLAYER_ENTITY, - ATTR_LIMIT, - ATTR_OFFSET, - ATTR_LIMIT_BEFORE, - ATTR_LIMIT_AFTER, - DEFAULT_QUEUE_ITEMS_LIMIT, - DEFAULT_QUEUE_ITEMS_OFFSET, + 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_MEDIA_ALBUM_NAME, + ATTR_MEDIA_ARTIST, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_IMAGE, + ATTR_PLAYER_ENTITY, + ATTR_LIMIT, + ATTR_OFFSET, + ATTR_LIMIT_BEFORE, + ATTR_LIMIT_AFTER, + DEFAULT_QUEUE_ITEMS_LIMIT, + DEFAULT_QUEUE_ITEMS_OFFSET, ) from .schemas import ( - 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, + 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 . import MassQueueEntryData - -class MassQueueActions(): - def __init__(self, hass: HomeAssistant, mass_client: MusicAssistantClient): - self._hass: HomeAssistant = hass - self._client: MusicAssistantClient = mass_client - self._controller = MassQueueController(self._hass, self._client) - - def setup_controller(self): - self._controller.update_players() - self._controller.subscribe_events() - self._hass.loop.create_task(self._controller.update_queues()) - - @callback - def register_actions(self) -> None: - self._hass.services.async_register( - DOMAIN, - SERVICE_GET_QUEUE_ITEMS, - self.get_queue_items, - schema=QUEUE_ITEMS_SERVICE_SCHEMA, - supports_response=SupportsResponse.ONLY, - ) - self._hass.services.async_register( - DOMAIN, - SERVICE_PLAY_QUEUE_ITEM, - self.play_queue_item, - schema=PLAY_QUEUE_ITEM_SERVICE_SCHEMA, - supports_response=SupportsResponse.NONE, - ) - - self._hass.services.async_register( - DOMAIN, - SERVICE_REMOVE_QUEUE_ITEM, - self.remove_queue_item, - schema=REMOVE_QUEUE_ITEM_SERVICE_SCHEMA, - supports_response=SupportsResponse.NONE, - ) - self._hass.services.async_register( - DOMAIN, - SERVICE_MOVE_QUEUE_ITEM_UP, - self.move_queue_item_up, - schema=MOVE_QUEUE_ITEM_UP_SERVICE_SCHEMA, - supports_response=SupportsResponse.NONE, - ) - self._hass.services.async_register( - DOMAIN, - SERVICE_MOVE_QUEUE_ITEM_DOWN, - self.move_queue_item_down, - schema=MOVE_QUEUE_ITEM_DOWN_SERVICE_SCHEMA, - supports_response=SupportsResponse.NONE, - ) - self._hass.services.async_register( - DOMAIN, - SERVICE_MOVE_QUEUE_ITEM_NEXT, - self.move_queue_item_next, - schema=MOVE_QUEUE_ITEM_NEXT_SERVICE_SCHEMA, - supports_response=SupportsResponse.NONE, - ) - - def get_queue_id(self, entity_id: str): - 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): - active_queue = await self.get_active_queue(entity_id) - idx = active_queue.current_index - return idx - - async def get_active_queue(self, entity_id: str): - queue_id = self.get_queue_id(entity_id) - queue = await self._client.player_queues.get_active_queue(queue_id) - return queue - - def _format_queue_item(self, queue_item: dict) -> dict: - 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_content_id = media['uri'] - img = queue_item.get('image') - if img is None: - media_image = '' - else: - media_image = img.get('path', '') - - artists = media['artists'] - artist_names = [artist['name'] for artist in artists] - media_artist = ', '.join(artist_names) - response: ServiceResponse = QUEUE_ITEM_SCHEMA( - { - ATTR_QUEUE_ITEM_ID: queue_item_id, - ATTR_MEDIA_TITLE: media_title, - ATTR_MEDIA_ALBUM_NAME: media_album_name, - 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: - entity_id = call.data[ATTR_PLAYER_ENTITY] - queue_id = self.get_queue_id(entity_id) - offset = call.data.get(ATTR_OFFSET) - limit = call.data.get(ATTR_LIMIT) - limit_before = call.data.get(ATTR_LIMIT_BEFORE) - limit_after = call.data.get(ATTR_LIMIT_AFTER) - idx = await self.get_queue_index(entity_id) - if limit_before: - offset = idx - limit_before - if limit_after: - if limit_before: - limit = limit_before + limit_after + 1 - else: - limit = limit_after + 1 - if offset is None: - offset = idx + DEFAULT_QUEUE_ITEMS_OFFSET - if limit is None: - limit = DEFAULT_QUEUE_ITEMS_LIMIT - 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] - } - return response - - async def play_queue_item(self, call: ServiceCall) -> ServiceResponse: - 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) - - async def remove_queue_item(self, call: ServiceCall) -> ServiceResponse: - 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: - 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: - 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) - - async def move_queue_item_next(self, call: ServiceCall) -> ServiceResponse: - 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) - + from . import MassQueueEntryData + + +class MassQueueActions: + def __init__(self, hass: HomeAssistant, mass_client: MusicAssistantClient): + self._hass: HomeAssistant = hass + self._client: MusicAssistantClient = mass_client + self._controller = MassQueueController(self._hass, self._client) + + def setup_controller(self): + self._controller.update_players() + self._controller.subscribe_events() + self._hass.loop.create_task(self._controller.update_queues()) + + @callback + def register_actions(self) -> None: + self._hass.services.async_register( + DOMAIN, + SERVICE_GET_QUEUE_ITEMS, + self.get_queue_items, + schema=QUEUE_ITEMS_SERVICE_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) + self._hass.services.async_register( + DOMAIN, + SERVICE_PLAY_QUEUE_ITEM, + self.play_queue_item, + schema=PLAY_QUEUE_ITEM_SERVICE_SCHEMA, + supports_response=SupportsResponse.NONE, + ) + + self._hass.services.async_register( + DOMAIN, + SERVICE_REMOVE_QUEUE_ITEM, + self.remove_queue_item, + schema=REMOVE_QUEUE_ITEM_SERVICE_SCHEMA, + supports_response=SupportsResponse.NONE, + ) + self._hass.services.async_register( + DOMAIN, + SERVICE_MOVE_QUEUE_ITEM_UP, + self.move_queue_item_up, + schema=MOVE_QUEUE_ITEM_UP_SERVICE_SCHEMA, + supports_response=SupportsResponse.NONE, + ) + self._hass.services.async_register( + DOMAIN, + SERVICE_MOVE_QUEUE_ITEM_DOWN, + self.move_queue_item_down, + schema=MOVE_QUEUE_ITEM_DOWN_SERVICE_SCHEMA, + supports_response=SupportsResponse.NONE, + ) + self._hass.services.async_register( + DOMAIN, + SERVICE_MOVE_QUEUE_ITEM_NEXT, + self.move_queue_item_next, + schema=MOVE_QUEUE_ITEM_NEXT_SERVICE_SCHEMA, + supports_response=SupportsResponse.NONE, + ) + + def get_queue_id(self, entity_id: str): + 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): + active_queue = await self.get_active_queue(entity_id) + idx = active_queue.current_index + return idx + + async def get_active_queue(self, entity_id: str): + queue_id = self.get_queue_id(entity_id) + queue = await self._client.player_queues.get_active_queue(queue_id) + return queue + + def _format_queue_item(self, queue_item: dict) -> dict: + 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_content_id = media["uri"] + img = queue_item.get("image") + if img is None: + media_image = "" + else: + media_image = img.get("path", "") + + artists = media["artists"] + artist_names = [artist["name"] for artist in artists] + media_artist = ", ".join(artist_names) + response: ServiceResponse = QUEUE_ITEM_SCHEMA( + { + ATTR_QUEUE_ITEM_ID: queue_item_id, + ATTR_MEDIA_TITLE: media_title, + ATTR_MEDIA_ALBUM_NAME: media_album_name, + 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: + entity_id = call.data[ATTR_PLAYER_ENTITY] + queue_id = self.get_queue_id(entity_id) + offset = call.data.get(ATTR_OFFSET) + limit = call.data.get(ATTR_LIMIT) + limit_before = call.data.get(ATTR_LIMIT_BEFORE) + limit_after = call.data.get(ATTR_LIMIT_AFTER) + idx = await self.get_queue_index(entity_id) + if limit_before: + offset = idx - limit_before + if limit_after: + if limit_before: + limit = limit_before + limit_after + 1 + else: + limit = limit_after + 1 + if offset is None: + offset = idx + DEFAULT_QUEUE_ITEMS_OFFSET + if limit is None: + limit = DEFAULT_QUEUE_ITEMS_LIMIT + 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] + } + return response + + async def play_queue_item(self, call: ServiceCall) -> ServiceResponse: + 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 + ) + + async def remove_queue_item(self, call: ServiceCall) -> ServiceResponse: + 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: + 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: + 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 + ) + + async def move_queue_item_next(self, call: ServiceCall) -> ServiceResponse: + 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 + ) + + @callback def get_music_assistant_client_boostrap(hass: HomeAssistant) -> MusicAssistantClient: - mass_domain = 'music_assistant' - entries = hass.config_entries.async_entries() - config_entry = [entry for entry in entries if entry.domain == mass_domain][0] - return config_entry.runtime_data.mass + mass_domain = "music_assistant" + entries = hass.config_entries.async_entries() + config_entry = [entry for entry in entries if entry.domain == mass_domain][0] + return config_entry.runtime_data.mass + @callback def get_music_assistant_client( - hass: HomeAssistant, - entity_id: str) -> MusicAssistantClient: - registry = er.async_get(hass) - entity = registry.async_get(entity_id) - config_entry_id = entity.config_entry_id - return _get_music_assistant_client(hass, config_entry_id) + hass: HomeAssistant, entity_id: str +) -> MusicAssistantClient: + registry = er.async_get(hass) + entity = registry.async_get(entity_id) + config_entry_id = entity.config_entry_id + return _get_music_assistant_client(hass, config_entry_id) + @callback def _get_music_assistant_client( - hass: HomeAssistant, - config_entry_id: str) -> MusicAssistantClient: - entry: MassQueueEntryData | 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 setup_controller_and_actions(hass: HomeAssistant, mass_client: MusicAssistantClient|None = None) -> MassQueueActions: - if mass_client is None: - mass_client = get_music_assistant_client_boostrap(hass) - actions = MassQueueActions(hass, mass_client) - actions.setup_controller() - actions.register_actions() - return actions \ No newline at end of file + hass: HomeAssistant, config_entry_id: str +) -> MusicAssistantClient: + entry: MassQueueEntryData | 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 setup_controller_and_actions( + hass: HomeAssistant, mass_client: MusicAssistantClient | None = None +) -> MassQueueActions: + if mass_client is None: + mass_client = get_music_assistant_client_boostrap(hass) + actions = MassQueueActions(hass, mass_client) + actions.setup_controller() + actions.register_actions() + return actions diff --git a/custom_components/mass_queue/config_flow.py b/custom_components/mass_queue/config_flow.py index 0b79c95..c90eb75 100644 --- a/custom_components/mass_queue/config_flow.py +++ b/custom_components/mass_queue/config_flow.py @@ -17,6 +17,7 @@ DEFAULT_URL = "http://mass.local:8095" DEFAULT_TITLE = "Music Assistant Queue Items" + def get_manual_schema(user_input: dict[str, Any]) -> vol.Schema: """Return a schema for the manual step.""" default_url = user_input.get(CONF_URL, DEFAULT_URL) @@ -126,4 +127,4 @@ async def async_step_discovery_confirm( return self.async_show_form( step_id="discovery_confirm", description_placeholders={"url": self.server_info.base_url}, - ) \ No newline at end of file + ) diff --git a/custom_components/mass_queue/const.py b/custom_components/mass_queue/const.py index 4f58476..619a8b9 100644 --- a/custom_components/mass_queue/const.py +++ b/custom_components/mass_queue/const.py @@ -1,4 +1,5 @@ """Constants for the NEW_NAME integration.""" + import logging DOMAIN = "mass_queue" @@ -15,8 +16,8 @@ ATTR_PLAYER_ENTITY = "entity" ATTR_OFFSET = "offset" ATTR_LIMIT = "limit" -ATTR_LIMIT_BEFORE = 'limit_before' -ATTR_LIMIT_AFTER = 'limit_after' +ATTR_LIMIT_BEFORE = "limit_before" +ATTR_LIMIT_AFTER = "limit_after" ATTR_QUEUE_ITEM_ID = "queue_item_id" ATTR_MEDIA_TITLE = "media_title" ATTR_MEDIA_ALBUM_NAME = "media_album_name" @@ -30,4 +31,4 @@ DEFAULT_QUEUE_ITEMS_OFFSET = -5 MUSIC_ASSISTANT_EVENT_DOMAIN = "mass_music_assistant" -MASS_QUEUE_EVENT_DOMAIN = "mass_queue" \ No newline at end of file +MASS_QUEUE_EVENT_DOMAIN = "mass_queue" diff --git a/custom_components/mass_queue/controller.py b/custom_components/mass_queue/controller.py index ca6520a..c431bdc 100644 --- a/custom_components/mass_queue/controller.py +++ b/custom_components/mass_queue/controller.py @@ -1,288 +1,262 @@ from __future__ import annotations -from homeassistant.core import ( - HomeAssistant -) +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 -) -from .utils import ( - get_queue_id_from_player_data, - format_queue_updated_event_data + DEFAULT_QUEUE_ITEMS_LIMIT, + DEFAULT_QUEUE_ITEMS_OFFSET, + LOGGER, + MUSIC_ASSISTANT_EVENT_DOMAIN, + MASS_QUEUE_EVENT_DOMAIN, ) +from .utils import get_queue_id_from_player_data, format_queue_updated_event_data + + +class MassQueueController: + def __init__(self, hass: HomeAssistant, mass_client): + self._client = mass_client + self._hass = hass + self.players = Players(hass) + self.queues = Queues(hass) + + # Events + def subscribe_events(self): + 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) + return + + def send_ha_event(self, event_data): + LOGGER.debug( + 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): + 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): + 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): + 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) -class MassQueueController(): - def __init__(self, hass: HomeAssistant, mass_client): - self._client = mass_client - self._hass = hass - self.players = Players(hass) - self.queues = Queues(hass) - - # Events - def subscribe_events(self): - 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) - return - - def send_ha_event(self, event_data): - LOGGER.debug(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): - LOGGER.debug(f'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): - LOGGER.debug(f'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): - 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): - 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): - LOGGER.debug(f'Updating all players.') - players = self.get_all_players() - self.players.batch_add(players) - - # Individual players - def update_player_queue(self, player_id: str): - 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): - 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 - - # All queues - async def get_all_queues(self): - 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 - - async def update_queues(self): - LOGGER.debug(f'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 + # All players + def get_all_players(self): + 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): + 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): + 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): + 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 + + # All queues + async def get_all_queues(self): + 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 + + async def update_queues(self): + 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, ): - queue = self.queues.get(queue_id) - if offset == -1: - try: - offset = await self.get_queue_index(queue_id) - 5 - except: - offset = 0 - offset = max(offset, 0) - result = queue[offset: offset + limit] - return result - - async def update_queue_items(self, queue_id: str): - 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, - queue_id: str, - limit: int = DEFAULT_QUEUE_ITEMS_LIMIT, - offset: int = DEFAULT_QUEUE_ITEMS_OFFSET + queue = self.queues.get(queue_id) + if offset == -1: + try: + offset = await self.get_queue_index(queue_id) - 5 + except: + offset = 0 + offset = max(offset, 0) + result = queue[offset : offset + limit] + return result + + async def update_queue_items(self, queue_id: str): + 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, + queue_id: str, + limit: int = DEFAULT_QUEUE_ITEMS_LIMIT, + offset: int = DEFAULT_QUEUE_ITEMS_OFFSET, ): - if offset == -1: - try: - offset = await self.get_queue_index(queue_id) - 5 - except: - 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 queue_items - - async def get_active_queue(self, queue_id: str): - result = await self._client.get_active_queue(queue_id) - return result - - async def get_queue_index(self, queue_id: str): - active_queue = await self.get_active_queue(queue_id) - idx = active_queue.current_index - return idx - -class Players(): - def __init__(self, hass: HomeAssistant, players: dict = {}): - self.players = players - self._hass = hass - def get(self, player_id): - return self.players.get(player_id) - def add(self, player_id: str, queue_id: str | None): - 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): - 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): - 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): - 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): - 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 - self._hass = hass - return - def get(self, queue_id): - return self.queues[queue_id] - def add(self, queue_id: str, queue_items: int): - 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): - 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): - 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): - 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): - 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 \ No newline at end of file + if offset == -1: + try: + offset = await self.get_queue_index(queue_id) - 5 + except: + 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 queue_items + + async def get_active_queue(self, queue_id: str): + result = await self._client.get_active_queue(queue_id) + return result + + async def get_queue_index(self, queue_id: str): + active_queue = await self.get_active_queue(queue_id) + idx = active_queue.current_index + return idx + + +class Players: + def __init__(self, hass: HomeAssistant, players: dict = {}): + self.players = players + self._hass = hass + + def get(self, player_id): + return self.players.get(player_id) + + def add(self, player_id: str, queue_id: str | None): + 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): + 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): + 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): + 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): + 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 + self._hass = hass + return + + def get(self, queue_id): + return self.queues[queue_id] + + def add(self, queue_id: str, queue_items: int): + 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): + 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): + 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): + 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): + 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/manifest.json b/custom_components/mass_queue/manifest.json index 1da85cf..9f9a927 100644 --- a/custom_components/mass_queue/manifest.json +++ b/custom_components/mass_queue/manifest.json @@ -13,4 +13,4 @@ "ssdp": [], "version": "0.3.0", "zeroconf": ["_mass._tcp.local."] -} \ No newline at end of file +} diff --git a/custom_components/mass_queue/schemas.py b/custom_components/mass_queue/schemas.py index ac92fda..4eb3f76 100644 --- a/custom_components/mass_queue/schemas.py +++ b/custom_components/mass_queue/schemas.py @@ -1,84 +1,78 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any import voluptuous as vol from homeassistant.helpers import config_validation as cv from .const import ( - ATTR_QUEUE_ITEM_ID, - ATTR_MEDIA_TITLE, - ATTR_MEDIA_ALBUM_NAME, - ATTR_MEDIA_ARTIST, - ATTR_MEDIA_CONTENT_ID, - ATTR_MEDIA_IMAGE, - ATTR_QUEUE_ITEMS, - ATTR_CONFIG_ENTRY_ID, - ATTR_PLAYER_ENTITY, - ATTR_OFFSET, - ATTR_LIMIT, - ATTR_LIMIT_BEFORE, - ATTR_LIMIT_AFTER + ATTR_QUEUE_ITEM_ID, + ATTR_MEDIA_TITLE, + ATTR_MEDIA_ALBUM_NAME, + ATTR_MEDIA_ARTIST, + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_IMAGE, + ATTR_QUEUE_ITEMS, + ATTR_PLAYER_ENTITY, + ATTR_OFFSET, + ATTR_LIMIT, + ATTR_LIMIT_BEFORE, + ATTR_LIMIT_AFTER, ) QUEUE_ITEM_SCHEMA = vol.Schema( - { - vol.Required(ATTR_QUEUE_ITEM_ID): str, - vol.Required(ATTR_MEDIA_TITLE): str, - vol.Required(ATTR_MEDIA_ALBUM_NAME): str, - vol.Required(ATTR_MEDIA_ARTIST): str, - vol.Required(ATTR_MEDIA_CONTENT_ID): str, - vol.Required(ATTR_MEDIA_IMAGE): str - } + { + vol.Required(ATTR_QUEUE_ITEM_ID): str, + vol.Required(ATTR_MEDIA_TITLE): str, + vol.Required(ATTR_MEDIA_ALBUM_NAME): str, + 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)]) - } + { + vol.Required(ATTR_QUEUE_ITEMS): vol.All( + cv.ensure_list, [vol.Schema(QUEUE_ITEM_SCHEMA)] + ) + } ) QUEUE_ITEMS_SERVICE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_PLAYER_ENTITY): str, - vol.Optional(ATTR_OFFSET): int, - vol.Optional(ATTR_LIMIT): int, - vol.Optional(ATTR_LIMIT_BEFORE): int, - vol.Optional(ATTR_LIMIT_AFTER): int, - - } + { + vol.Required(ATTR_PLAYER_ENTITY): str, + vol.Optional(ATTR_OFFSET): int, + 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, - - } + { + 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, - - } + { + 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, - - } + { + 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, - - } + { + 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, - - } -) \ No newline at end of file + { + vol.Required(ATTR_PLAYER_ENTITY): str, + vol.Required(ATTR_QUEUE_ITEM_ID): str, + } +) diff --git a/custom_components/mass_queue/services.yaml b/custom_components/mass_queue/services.yaml index 806442f..0812d4b 100644 --- a/custom_components/mass_queue/services.yaml +++ b/custom_components/mass_queue/services.yaml @@ -100,4 +100,4 @@ play_queue_item: selector: entity: domain: media_player - integration: music_assistant \ No newline at end of file + integration: music_assistant diff --git a/custom_components/mass_queue/utils.py b/custom_components/mass_queue/utils.py index ec2ed5f..c6e1b9c 100644 --- a/custom_components/mass_queue/utils.py +++ b/custom_components/mass_queue/utils.py @@ -1,33 +1,35 @@ -from .const import LOGGER - def format_event_data_queue_item(queue_item): - if queue_item is None: - return None - if queue_item.get('queue_id') is None: - return queue_item - item_cp = queue_item.copy() - if 'streamdetails' in item_cp: - item_cp.pop('streamdetails') - if 'media_item' in item_cp: - item_cp.pop('media_item') - return item_cp + if queue_item is None: + return None + if queue_item.get("queue_id") is None: + return queue_item + item_cp = queue_item.copy() + if "streamdetails" in item_cp: + item_cp.pop("streamdetails") + if "media_item" in item_cp: + item_cp.pop("media_item") + return item_cp + def format_queue_updated_event_data(event): - event_data = event.copy() - event_current_item = format_event_data_queue_item(event_data.get('current_item')) - event_next_item = format_event_data_queue_item(event_data.get('next_item')) - event_data['current_item'] = format_event_data_queue_item(event_data.get('current_item')) - event_data['next_item'] = format_event_data_queue_item(event_data.get('next_item')) - return event_data + event_data = event.copy() + event_current_item = format_event_data_queue_item(event_data.get("current_item")) + event_next_item = format_event_data_queue_item(event_data.get("next_item")) + event_data["current_item"] = format_event_data_queue_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): - try: - ''' Force as dict if not already ''' - data = player_data.to_dict() - except: - pass - current_media = data.get('current_media', None) - if current_media is None: - return None - queue_id = current_media.get('queue_id') - return queue_id \ No newline at end of file + try: + """Force as dict if not already""" + data = player_data.to_dict() + except: + pass + current_media = data.get("current_media", None) + if current_media is None: + return None + queue_id = current_media.get("queue_id") + return queue_id diff --git a/hacs.json b/hacs.json index b348f92..9819fbc 100644 --- a/hacs.json +++ b/hacs.json @@ -2,4 +2,4 @@ "name": "Music Assistant Queue Actions", "render_readme": true, "zip_release": false -} \ No newline at end of file +} From 9f5b8d991a6bb8db83a6841f75226892257f1414 Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Fri, 15 Aug 2025 15:18:36 -0400 Subject: [PATCH 4/8] Add workflows --- .github/labels.yml | 85 +++++++++++++++++++++++++++ .github/release-drafter.yml | 57 ++++++++++++++++++ .github/workflows/labels.yaml | 29 +++++++++ .github/workflows/pr-labels.yaml | 27 +++++++++ .github/workflows/release-drafter.yml | 24 ++++++++ 5 files changed, 222 insertions(+) create mode 100644 .github/labels.yml create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/labels.yaml create mode 100644 .github/workflows/pr-labels.yaml create mode 100644 .github/workflows/release-drafter.yml diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..d874f47 --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,85 @@ +--- +- name: "breaking-change" + color: ee0701 + description: "A breaking change for existing users." +- name: "bugfix" + color: ee0701 + description: "Inconsistencies or issues which will cause a problem for users or implementors." +- name: "documentation" + color: 0052cc + description: "Solely about the documentation of the project." +- name: "enhancement" + color: 1d76db + description: "Enhancement of the code, not introducing new features." +- name: "refactor" + color: 1d76db + description: "Improvement of existing code, not introducing new features." +- name: "performance" + color: 1d76db + description: "Improving performance, not introducing new features." +- name: "new-feature" + color: 0e8a16 + description: "New features or options." +- name: "maintenance" + color: 2af79e + description: "Generic maintenance tasks." +- name: "ci" + color: 1d76db + description: "Work that improves the continue integration." +- name: "dependencies" + color: 1d76db + description: "Upgrade or downgrade of project dependencies." + +- name: "in-progress" + color: fbca04 + description: "Issue is currently being resolved by a developer." +- name: "stale" + color: fef2c0 + description: "There has not been activity on this issue or PR for quite some time." +- name: "no-stale" + color: fef2c0 + description: "This issue or PR is exempted from the stable bot." + +- name: "security" + color: ee0701 + description: "Marks a security issue that needs to be resolved asap." +- name: "incomplete" + color: fef2c0 + description: "Marks a PR or issue that is missing information." +- name: "invalid" + color: fef2c0 + description: "Marks a PR or issue that is missing information." + +- name: "beginner-friendly" + color: 0e8a16 + description: "Good first issue for people wanting to contribute to the project." +- name: "help-wanted" + color: 0e8a16 + description: "We need some extra helping hands or expertise in order to resolve this." + +- name: "hacktoberfest" + description: "Issues/PRs are participating in the Hacktoberfest." + color: fbca04 +- name: "hacktoberfest-accepted" + description: "Issues/PRs are participating in the Hacktoberfest." + color: fbca04 + +- name: "priority-critical" + color: ee0701 + description: "This should be dealt with ASAP. Not fixing this issue would be a serious error." +- name: "priority-high" + color: b60205 + description: "After critical issues are fixed, these should be dealt with before any further issues." +- name: "priority-medium" + color: 0e8a16 + description: "This issue may be useful, and needs some attention." +- name: "priority-low" + color: e4ea8a + description: "Nice addition, maybe... someday..." + +- name: "major" + color: b60205 + description: "This PR causes a major version bump in the version number." +- name: "minor" + color: 0e8a16 + description: "This PR causes a minor version bump in the version number." \ No newline at end of file diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..7edfd24 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,57 @@ + +name-template: "v$RESOLVED_VERSION" +tag-template: "v$RESOLVED_VERSION" +change-template: "- $TITLE @$AUTHOR (#$NUMBER)" +sort-direction: ascending + +categories: + - title: "๐Ÿšจ Breaking changes" + labels: + - "breaking-change" + - title: "โœจ New features" + labels: + - "new-feature" + - title: "๐Ÿ› Bug fixes" + labels: + - "bugfix" + - title: "๐Ÿš€ Enhancements" + labels: + - "enhancement" + - "refactor" + - "performance" + - title: "๐Ÿงฐ Maintenance" + labels: + - "maintenance" + - "ci" + - title: "๐Ÿ“š Documentation" + labels: + - "documentation" + - title: "โฌ†๏ธ Dependency updates" + labels: + - "dependencies" + +version-resolver: + major: + labels: + - "major" + - "breaking-change" + minor: + labels: + - "minor" + - "new-feature" + patch: + labels: + - "bugfix" + - "chore" + - "ci" + - "dependencies" + - "documentation" + - "enhancement" + - "performance" + - "refactor" + default: patch + +template: | + # Music Assistant Queue Actions v$RESOLVED_VERSION + + $CHANGES \ No newline at end of file diff --git a/.github/workflows/labels.yaml b/.github/workflows/labels.yaml new file mode 100644 index 0000000..4c91d63 --- /dev/null +++ b/.github/workflows/labels.yaml @@ -0,0 +1,29 @@ +--- +name: Sync labels + +# yamllint disable-line rule:truthy +on: + push: + branches: + - main + paths: + - .github/labels.yml + workflow_dispatch: + +permissions: + pull-requests: write + +jobs: + labels: + name: โ™ป๏ธ Sync labels + runs-on: ubuntu-latest + steps: + - name: โคต๏ธ Check out code from GitHub + uses: actions/checkout@v5.0.0 + + - name: ๐Ÿš€ Run Label Syncer + uses: micnncim/action-label-syncer@v1.3.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + manifest: .github/labels.yml \ No newline at end of file diff --git a/.github/workflows/pr-labels.yaml b/.github/workflows/pr-labels.yaml new file mode 100644 index 0000000..ddf21fb --- /dev/null +++ b/.github/workflows/pr-labels.yaml @@ -0,0 +1,27 @@ +--- +name: PR Labels + +# yamllint disable-line rule:truthy +on: + pull_request_target: + types: + - opened + - labeled + - unlabeled + - synchronize + workflow_call: + +jobs: + pr_labels: + name: Verify + runs-on: ubuntu-latest + steps: + - name: ๐Ÿท Verify PR has a valid label + uses: jesusvasquez333/verify-pr-label-action@v1.4.0 + with: + pull-request-number: "${{ github.event.pull_request.number }}" + github-token: "${{ secrets.GITHUB_TOKEN }}" + valid-labels: >- + breaking-change, bugfix, documentation, enhancement, + refactor, performance, new-feature, maintenance, ci, dependencies + disable-reviews: true \ No newline at end of file diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..0499e4a --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,24 @@ +--- +name: Release Drafter + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + update_release_draft: + name: Draft release + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Run Release Drafter + uses: release-drafter/release-drafter@v6.1.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file From e43488498ab76097d42cfa0adf90c8e514b03b0c Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Fri, 15 Aug 2025 15:20:13 -0400 Subject: [PATCH 5/8] Linting --- .github/labels.yml | 2 +- .github/release-drafter.yml | 2 +- .github/workflows/labels.yaml | 2 +- .github/workflows/pr-labels.yaml | 2 +- .github/workflows/release-drafter.yml | 4 ++-- .pre-commit-config.yaml | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/labels.yml b/.github/labels.yml index d874f47..2d0f68a 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -82,4 +82,4 @@ description: "This PR causes a major version bump in the version number." - name: "minor" color: 0e8a16 - description: "This PR causes a minor version bump in the version number." \ No newline at end of file + description: "This PR causes a minor version bump in the version number." diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 7edfd24..e64076c 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -54,4 +54,4 @@ version-resolver: template: | # Music Assistant Queue Actions v$RESOLVED_VERSION - $CHANGES \ No newline at end of file + $CHANGES diff --git a/.github/workflows/labels.yaml b/.github/workflows/labels.yaml index 4c91d63..cc85c03 100644 --- a/.github/workflows/labels.yaml +++ b/.github/workflows/labels.yaml @@ -26,4 +26,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - manifest: .github/labels.yml \ No newline at end of file + manifest: .github/labels.yml diff --git a/.github/workflows/pr-labels.yaml b/.github/workflows/pr-labels.yaml index ddf21fb..7c2b074 100644 --- a/.github/workflows/pr-labels.yaml +++ b/.github/workflows/pr-labels.yaml @@ -24,4 +24,4 @@ jobs: valid-labels: >- breaking-change, bugfix, documentation, enhancement, refactor, performance, new-feature, maintenance, ci, dependencies - disable-reviews: true \ No newline at end of file + disable-reviews: true diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 0499e4a..6d2a19e 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -14,11 +14,11 @@ jobs: update_release_draft: name: Draft release runs-on: ubuntu-latest - permissions: + permissions: contents: write pull-requests: write steps: - name: Run Release Drafter uses: release-drafter/release-drafter@v6.1.0 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 020789a..1321a48 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,4 +15,4 @@ repos: - repo: https://github.com/psf/black rev: 25.1.0 hooks: - - id: black \ No newline at end of file + - id: black From 4b68d3bbe5c4db19b362595f883d52ba37afaa61 Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Fri, 15 Aug 2025 15:25:47 -0400 Subject: [PATCH 6/8] Linting --- custom_components/mass_queue/__init__.py | 5 +---- custom_components/mass_queue/config_flow.py | 5 +++++ custom_components/mass_queue/controller.py | 4 ++-- custom_components/mass_queue/utils.py | 10 ++++------ 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/custom_components/mass_queue/__init__.py b/custom_components/mass_queue/__init__.py index 8ff3867..0fcd9e5 100644 --- a/custom_components/mass_queue/__init__.py +++ b/custom_components/mass_queue/__init__.py @@ -6,11 +6,10 @@ from music_assistant_client import MusicAssistantClient from music_assistant_client.exceptions import CannotConnect, InvalidServerVersion -from music_assistant_models.enums import EventType from music_assistant_models.errors import ActionUnavailable, MusicAssistantError from homeassistant.config_entries import ConfigEntry, ConfigEntryState -from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP, Platform +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 @@ -25,9 +24,7 @@ from .const import DOMAIN, LOGGER if TYPE_CHECKING: - from music_assistant_models.event import MassEvent from homeassistant.helpers.typing import ConfigType - from .actions import MassQueueActions # PLATFORMS = [Platform.MEDIA_PLAYER] PLATFORMS = [] diff --git a/custom_components/mass_queue/config_flow.py b/custom_components/mass_queue/config_flow.py index c90eb75..ada0fc7 100644 --- a/custom_components/mass_queue/config_flow.py +++ b/custom_components/mass_queue/config_flow.py @@ -3,6 +3,11 @@ from typing import TYPE_CHECKING, Any 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 diff --git a/custom_components/mass_queue/controller.py b/custom_components/mass_queue/controller.py index c431bdc..b2750a1 100644 --- a/custom_components/mass_queue/controller.py +++ b/custom_components/mass_queue/controller.py @@ -133,7 +133,7 @@ async def player_queue( if offset == -1: try: offset = await self.get_queue_index(queue_id) - 5 - except: + except Exception: offset = 0 offset = max(offset, 0) result = queue[offset : offset + limit] @@ -154,7 +154,7 @@ async def get_queue( if offset == -1: try: offset = await self.get_queue_index(queue_id) - 5 - except: + except Exception: offset = 0 offset = max(offset, 0) queue_items = await self._client.player_queues.get_player_queue_items( diff --git a/custom_components/mass_queue/utils.py b/custom_components/mass_queue/utils.py index c6e1b9c..146e6e7 100644 --- a/custom_components/mass_queue/utils.py +++ b/custom_components/mass_queue/utils.py @@ -13,8 +13,6 @@ def format_event_data_queue_item(queue_item): def format_queue_updated_event_data(event): event_data = event.copy() - event_current_item = format_event_data_queue_item(event_data.get("current_item")) - event_next_item = format_event_data_queue_item(event_data.get("next_item")) event_data["current_item"] = format_event_data_queue_item( event_data.get("current_item") ) @@ -23,11 +21,11 @@ def format_queue_updated_event_data(event): def get_queue_id_from_player_data(player_data): - try: - """Force as dict if not already""" + """Force as dict if not already""" + if type(player_data) is not dict: data = player_data.to_dict() - except: - pass + else: + data = player_data current_media = data.get("current_media", None) if current_media is None: return None From a3d4fe4192e5b3699e9a856698bf75cda70696b6 Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Fri, 15 Aug 2025 16:08:38 -0400 Subject: [PATCH 7/8] Linting, type checking --- .github/workflows/pre-commit.yaml | 14 +++ .ruff.toml | 58 ++++++++++ custom_components/mass_queue/__init__.py | 42 +++++--- custom_components/mass_queue/actions.py | 111 ++++++++++++-------- custom_components/mass_queue/config_flow.py | 42 +++++--- custom_components/mass_queue/controller.py | 94 +++++++++++------ custom_components/mass_queue/schemas.py | 37 ++++--- custom_components/mass_queue/utils.py | 19 ++-- 8 files changed, 283 insertions(+), 134 deletions(-) create mode 100644 .github/workflows/pre-commit.yaml create mode 100644 .ruff.toml diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml new file mode 100644 index 0000000..860f798 --- /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 \ No newline at end of file diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..6b8c0da --- /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 \ No newline at end of file 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") From 2d13934c5b3b9a04c232d188d6508e840efb03e8 Mon Sep 17 00:00:00 2001 From: Michael Carroll Date: Fri, 15 Aug 2025 16:09:47 -0400 Subject: [PATCH 8/8] More linting --- .github/workflows/pre-commit.yaml | 2 +- .ruff.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml index 860f798..6f48d18 100644 --- a/.github/workflows/pre-commit.yaml +++ b/.github/workflows/pre-commit.yaml @@ -11,4 +11,4 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - - uses: pre-commit/action@v3.0.1 \ No newline at end of file + - uses: pre-commit/action@v3.0.1 diff --git a/.ruff.toml b/.ruff.toml index 6b8c0da..a6ddd64 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -55,4 +55,4 @@ fixture-parentheses = false keep-runtime-typing = true [lint.mccabe] -max-complexity = 25 \ No newline at end of file +max-complexity = 25