Skip to content

Commit 57ba7b8

Browse files
authored
Merge pull request #51 from droans/add-recommendations
Add WS command to download images
2 parents 77d1f55 + d248f5c commit 57ba7b8

File tree

3 files changed

+131
-65
lines changed

3 files changed

+131
-65
lines changed

custom_components/mass_queue/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from dataclasses import dataclass
77
from typing import TYPE_CHECKING
88

9+
from homeassistant.components import websocket_api
910
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
1011
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP
1112
from homeassistant.exceptions import ConfigEntryNotReady
@@ -28,6 +29,7 @@
2829
)
2930
from .const import DOMAIN, LOGGER
3031
from .services import register_actions
32+
from .utils import download_images
3133

3234
if TYPE_CHECKING:
3335
from homeassistant.core import HomeAssistant
@@ -119,6 +121,7 @@ async def on_hass_stop(event: Event) -> None: # noqa: ARG001
119121
actions = await setup_controller_and_actions(hass, mass)
120122
register_actions(hass)
121123
entry.runtime_data = MusicAssistantQueueEntryData(mass, actions, listen_task)
124+
websocket_api.async_register_command(hass, download_images)
122125

123126
# If the listen task is already failed, we need to raise ConfigEntryNotReady
124127
if listen_task.done() and (listen_error := listen_task.exception()) is not None:

custom_components/mass_queue/services.py

Lines changed: 1 addition & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,11 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING
6-
7-
from homeassistant.config_entries import ConfigEntryState
85
from homeassistant.core import (
9-
HomeAssistant,
106
ServiceCall,
117
SupportsResponse,
128
callback,
139
)
14-
from homeassistant.exceptions import ServiceValidationError
15-
from homeassistant.helpers import entity_registry as er
1610

1711
from .const import (
1812
ATTR_CONFIG_ENTRY_ID,
@@ -39,12 +33,7 @@
3933
SEND_COMMAND_SERVICE_SCHEMA,
4034
UNFAVORITE_CURRENT_ITEM_SERVICE_SCHEMA,
4135
)
42-
from .utils import process_recommendations
43-
44-
if TYPE_CHECKING:
45-
from music_assistant_client import MusicAssistantClient
46-
47-
from . import MassQueueEntryData
36+
from .utils import get_entity_actions_controller, process_recommendations
4837

4938

5039
@callback
@@ -115,59 +104,6 @@ def register_actions(hass) -> None:
115104
)
116105

117106

118-
def _get_mass_entity_config_entry_id(hass, entity_id):
119-
"""Helper to grab config entry ID from entity ID."""
120-
registry = er.async_get(hass)
121-
return registry.async_get(entity_id).config_entry_id
122-
123-
124-
@callback
125-
def _get_config_entry(
126-
hass: HomeAssistant,
127-
config_entry_id: str,
128-
) -> MusicAssistantClient:
129-
"""Get Music Assistant Client from config_entry_id."""
130-
entry: MassQueueEntryData | None
131-
if not (entry := hass.config_entries.async_get_entry(config_entry_id)):
132-
exc = "Entry not found."
133-
raise ServiceValidationError(exc)
134-
if entry.state is not ConfigEntryState.LOADED:
135-
exc = "Entry not loaded"
136-
raise ServiceValidationError(exc)
137-
return entry
138-
139-
140-
def get_mass_entry(hass, entity_id):
141-
"""Helper function to pull MA Config Entry."""
142-
config_id = _get_mass_entity_config_entry_id(hass, entity_id)
143-
return _get_config_entry(hass, config_id)
144-
145-
146-
def _get_mass_queue_entries(hass):
147-
"""Gets all entries for mass_queue domain."""
148-
entries = hass.config_entries.async_entries()
149-
return [entry for entry in entries if entry.domain == "mass_queue"]
150-
151-
152-
def find_mass_queue_entry(hass, mass_url):
153-
"""Finds the mass_queue entry for the given MA URL."""
154-
entries = _get_mass_queue_entries(hass)
155-
for entry in entries:
156-
entry_url = entry.runtime_data.mass.connection.ws_server_url
157-
if entry_url == mass_url:
158-
return entry
159-
msg = f"Cannot find entry for Music Assistant at {mass_url}"
160-
raise ServiceValidationError(msg)
161-
162-
163-
def get_entity_actions_controller(hass, entity_id):
164-
"""Gets the actions for the selected entity."""
165-
mass_entry = get_mass_entry(hass, entity_id)
166-
mass = mass_entry.runtime_data.mass.connection.ws_server_url
167-
mass_queue_entry = find_mass_queue_entry(hass, mass)
168-
return mass_queue_entry.runtime_data.actions
169-
170-
171107
async def get_queue_items(call: ServiceCall):
172108
"""Service wrapper to get queue items."""
173109
entity_id = call.data[ATTR_PLAYER_ENTITY]

custom_components/mass_queue/utils.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,81 @@
11
"""Utilities."""
22

3+
from __future__ import annotations
4+
5+
import base64
6+
import urllib.parse
7+
from typing import TYPE_CHECKING
8+
9+
import voluptuous as vol
10+
from homeassistant.components import websocket_api
11+
from homeassistant.config_entries import ConfigEntryState
12+
from homeassistant.core import callback
13+
from homeassistant.exceptions import ServiceValidationError
14+
from homeassistant.helpers import aiohttp_client
15+
from homeassistant.helpers import entity_registry as er
16+
17+
if TYPE_CHECKING:
18+
from homeassistant.core import HomeAssistant
19+
from music_assistant_client import MusicAssistantClient
20+
21+
from . import MassQueueEntryData
22+
323
from .const import LOGGER
424

525

26+
@callback
27+
def _get_config_entry(
28+
hass: HomeAssistant,
29+
config_entry_id: str,
30+
) -> MusicAssistantClient:
31+
"""Get Music Assistant Client from config_entry_id."""
32+
entry: MassQueueEntryData | None
33+
if not (entry := hass.config_entries.async_get_entry(config_entry_id)):
34+
exc = "Entry not found."
35+
raise ServiceValidationError(exc)
36+
if entry.state is not ConfigEntryState.LOADED:
37+
exc = "Entry not loaded"
38+
raise ServiceValidationError(exc)
39+
return entry
40+
41+
42+
def get_entity_actions_controller(hass, entity_id):
43+
"""Gets the actions for the selected entity."""
44+
mass_entry = get_mass_entry(hass, entity_id)
45+
mass = mass_entry.runtime_data.mass.connection.ws_server_url
46+
mass_queue_entry = find_mass_queue_entry(hass, mass)
47+
return mass_queue_entry.runtime_data.actions
48+
49+
50+
def get_mass_entry(hass, entity_id):
51+
"""Helper function to pull MA Config Entry."""
52+
config_id = _get_mass_entity_config_entry_id(hass, entity_id)
53+
return _get_config_entry(hass, config_id)
54+
55+
56+
def _get_mass_entity_config_entry_id(hass, entity_id):
57+
"""Helper to grab config entry ID from entity ID."""
58+
registry = er.async_get(hass)
59+
return registry.async_get(entity_id).config_entry_id
60+
61+
62+
def find_mass_queue_entry(hass, mass_url):
63+
"""Finds the mass_queue entry for the given MA URL."""
64+
entries = _get_mass_queue_entries(hass)
65+
for entry in entries:
66+
entry_url = entry.runtime_data.mass.connection.ws_server_url
67+
if entry_url == mass_url:
68+
return entry
69+
msg = f"Cannot find entry for Music Assistant at {mass_url}"
70+
raise ServiceValidationError(msg)
71+
72+
73+
def _get_mass_queue_entries(hass):
74+
"""Gets all entries for mass_queue domain."""
75+
entries = hass.config_entries.async_entries()
76+
return [entry for entry in entries if entry.domain == "mass_queue"]
77+
78+
679
def format_event_data_queue_item(queue_item):
780
"""Format event data results for usage by controller."""
881
if queue_item is None:
@@ -173,3 +246,57 @@ def process_recommendations(recs: list):
173246
if len(processed["items"]):
174247
result.append(processed)
175248
return result
249+
250+
251+
def _generate_image_url(image_data: dict, client):
252+
img_path = image_data["path"]
253+
provider = image_data["provider"]
254+
base_url = client.server_url
255+
img = urllib.parse.quote_plus(urllib.parse.quote_plus(img_path))
256+
return f"{base_url}/imageproxy?provider={provider}&size=256&format=png&path={img}"
257+
258+
259+
async def _download_single_image(image_data: dict, entity_id, hass, session):
260+
"""Downloads a single image from Music Assistant and returns the base64 encoded string."""
261+
entry = get_mass_entry(hass, entity_id)
262+
client = entry.runtime_data.mass
263+
url = _generate_image_url(image_data, client)
264+
try:
265+
req = await session.get(url)
266+
read = await req.content.read()
267+
return f"data:image;base64,{base64.b64encode(read).decode('utf-8')}"
268+
except: # noqa: E722
269+
LOGGER.error(f"Unable to get image with data {image_data}")
270+
return None
271+
272+
273+
@websocket_api.websocket_command(
274+
{
275+
vol.Required("type"): "mass_queue/encode_images",
276+
vol.Required("entity_id"): str,
277+
vol.Required("images"): list,
278+
},
279+
)
280+
@websocket_api.async_response
281+
async def download_images(
282+
hass: HomeAssistant,
283+
connection: websocket_api.ActiveConnection,
284+
msg: dict,
285+
) -> None:
286+
"""Download images and return them as b64 encoded."""
287+
LOGGER.error(f"Received message: {msg}")
288+
session = aiohttp_client.async_get_clientsession(hass)
289+
LOGGER.error("Got session")
290+
images = msg["images"]
291+
LOGGER.error("Pulled images from message")
292+
LOGGER.error(images)
293+
result = []
294+
entity_id = msg["entity_id"]
295+
for image in images:
296+
img = await _download_single_image(image, entity_id, hass, session)
297+
LOGGER.error(f"Downloaded image: {img}")
298+
image["encoded"] = img
299+
result.append(image)
300+
LOGGER.error("Final:")
301+
LOGGER.error(result)
302+
connection.send_result(msg["id"], result)

0 commit comments

Comments
 (0)