Skip to content

Commit

Permalink
upload preview image to cloud and delete after
Browse files Browse the repository at this point in the history
  • Loading branch information
sanghviharshit committed May 6, 2022
1 parent 8724077 commit 04a8954
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 21 deletions.
46 changes: 41 additions & 5 deletions custom_components/meural/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"async_preview_image",
)

platform.async_register_entity_service(
"preview_image_cloud",
{
vol.Required("content_url"): str,
vol.Required("content_type"): str,
vol.Optional("name"): str,
vol.Optional("author"): str,
vol.Optional("description"): str,
vol.Optional("medium"): str,
vol.Optional("year"): int,
},
"async_preview_image_cloud",
)


platform.async_register_entity_service(
"reset_brightness",
{},
Expand Down Expand Up @@ -488,6 +503,14 @@ async def async_set_shuffle(self, shuffle):

async def async_play_media(self, media_type, media_id, **kwargs):
"""Play media from media_source."""
use_cloud = kwargs.get("use_cloud", False)
if use_cloud:
name = kwargs.get("name")
author = kwargs.get("author")
description = kwargs.get("description")
medium = kwargs.get("medium")
year = kwargs.get("year")

if media_source.is_media_source_id(media_id):
sourced_media = await media_source.async_resolve_media(self.hass, media_id)
media_type = sourced_media.mime_type
Expand All @@ -510,19 +533,24 @@ async def async_play_media(self, media_type, media_id, **kwargs):
# Prepend external URL.
hass_url = get_url(self.hass, allow_internal=True)
media_id = f"{hass_url}{media_id}"

_LOGGER.info("Meural device %s: Playing media. Media type is %s, previewing image from %s", self.name, media_type, media_id)
await self.local_meural.send_postcard(media_id, media_type)
if use_cloud:
await self.meural.send_postcard_cloud(self._meural_device, media_id, media_type, name, author, description, medium, year)
else:
await self.local_meural.send_postcard(media_id, media_type)

# Play gallery (playlist or album) by ID.
elif media_type in ['playlist']:
_LOGGER.info("Meural device %s: Playing media. Media type is %s, playing gallery %s", self.name, media_type, media_id)
await self.local_meural.send_change_gallery(media_id)

# "Preview image from URL.
elif media_type in [ 'image/jpg', 'image/png', 'image/jpeg' ]:
if media_type in [ 'image/jpg', 'image/png', 'image/jpeg', 'image/gif' ]:
_LOGGER.info("Meural device %s: Playing media. Media type is %s, previewing image from %s", self.name, media_type, media_id)
await self.local_meural.send_postcard(media_id, media_type)
if use_cloud:
await self.meural.send_postcard_cloud(self._meural_device, media_id, media_type, name, author, description, medium, year)
else:
await self.local_meural.send_postcard(media_id, media_type)

# Play item (artwork) by ID. Play locally if item is in currently displayed gallery. If not, play using Meural server."""
elif media_type in ['item']:
Expand Down Expand Up @@ -550,7 +578,15 @@ async def async_preview_image(self, content_url, content_type):
"""Preview image from URL."""
if content_type in [ 'image/jpg', 'image/png', 'image/jpeg' ]:
_LOGGER.info("Meural device %s: Previewing image. Media type is %s, previewing image from %s", self.name, content_type, content_url)
await self.local_meural.send_postcard(content_url, content_type)
await self.async_play_media(media_type=content_type, media_id=content_url)
else:
_LOGGER.error("Meural device %s: Previewing image. Does not support media type %s", self.name, content_type)

async def async_preview_image_cloud(self, content_url, content_type, name=None, author=None, description=None, medium=None, year=None):
"""Preview image from URL."""
if content_type in [ 'image/jpg', 'image/png', 'image/jpeg', 'image/gif' ]:
_LOGGER.info("Meural device %s: Previewing image via meural cloud. Media type is %s, previewing image from %s", self.name, content_type, content_url)
await self.async_play_media(media_type=content_type, media_id=content_url, use_cloud=True, name=name, author=author, description=description, medium=medium, year=year)
else:
_LOGGER.error("Meural device %s: Previewing image. Does not support media type %s", self.name, content_type)

Expand Down
116 changes: 104 additions & 12 deletions custom_components/meural/pymeural.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import asyncio
import logging
import json
from datetime import datetime

from typing import Dict
import aiohttp
import async_timeout

from .util import snake_case

from aiohttp.client_exceptions import ClientResponseError

from homeassistant.exceptions import HomeAssistantError

_LOGGER = logging.getLogger(__name__)

BASE_URL = "https://api.meural.com/v0/"
BASE_URL = "https://api.meural.com/v1/"


async def authenticate(
Expand Down Expand Up @@ -50,26 +53,30 @@ def __init__(self, username, password, token, token_update_callback, session: ai
self.token = token
self.token_update_callback = token_update_callback

async def request(self, method, path, data=None) -> Dict:
async def request(self, method, path, data=None, data_key=None) -> Dict:
fetched_new_token = self.token is None
if self.token == None:
await self.get_new_token()
url = f"{BASE_URL}{path}"
kwargs = {}
if data:
if method == "get":
if data_key == "data":
kwargs["data"] = data
elif method == "get":
kwargs["query"] = data
else:
kwargs["json"] = data
headers = {
"Authorization": f"Token {self.token}",
"x-meural-api-version": "3",
}

with async_timeout.timeout(10):
try:
resp = await self.session.request(
method,
url,
headers={
"Authorization": f"Token {self.token}",
"x-meural-api-version": "3",
},
headers=headers,
raise_for_status=True,
**kwargs,
)
Expand All @@ -79,14 +86,16 @@ async def request(self, method, path, data=None) -> Dict:
# If a new token was just fetched and it fails again, just raise
if fetched_new_token:
raise
_LOGGER.info('Meural: Sending Request failed. Re-Authenticating')
_LOGGER.info(
'Meural: Sending Request failed. Re-Authenticating')
self.token = None
return await self.request(method, path, data)
except Exception as err:
_LOGGER.error('Meural: Sending Request failed. Raising: %s' %err)
_LOGGER.error(
'Meural: Sending Request failed. Raising: %s' % err)
raise
response = await resp.json()
return response["data"]
return response["data"] if response != None else None

async def get_new_token(self):
self.token = await authenticate(self.session, self.username, self.password)
Expand Down Expand Up @@ -128,6 +137,87 @@ async def sync_device(self, device_id):
async def get_item(self, item_id):
return await self.request("get", f"items/{item_id}")

async def update_content(self, id, name=None, author=None, description=None, medium=None, year=None):
_LOGGER.info(f"Meural: Updating postcard. Id is {id}")

now = datetime.now()
dt_string = now.strftime("%d/%m/%Y %H:%M:%S")

name = "Homeassistant Preview Image" if name == None else name
author = "Homeassistant" if author == None else author
description = "Preview Image from Home Assistant Meural component" if description == None else description
medium = "Photo" if medium == None else medium
year = dt_string if year == None else str(year)

data = aiohttp.FormData()
data.add_field("name", name)
data.add_field("author", author)
data.add_field("description", description)
data.add_field("medium", medium)
data.add_field("year", year)

response = await self.request("put", f"items/{id}",
data=data, data_key="data")

_LOGGER.info(f'Meural: Updating postcard. {id} updated')
return response

async def upload_content(self, url, content_type, name):
# photo uploads are done doing a multipart/form-data form
# with key 'image' or 'video' and value being the image/video data

_LOGGER.info('Meural: Sending postcard. URL is %s' % (url))
name = "homeassistant-preview-image.jpg" if name == None else snake_case(
name) + '.jpg'
with async_timeout.timeout(10):
response = await self.session.get(url)
content = await response.read()
_LOGGER.info(
'Meural: Sending postcard. Downloaded %d bytes of image' % (len(content)))

data = aiohttp.FormData()

field_name = 'image'
if not (content_type == 'image/jpg' or content_type == 'image/jpeg'):
field_name = 'video'
data.add_field(field_name, content, filename=name,
content_type=content_type)

response = await self.request("post", f"items",
data=data, data_key="data")

_LOGGER.info('Meural: Sending postcard. %s uploaded' % (
field_name))
return response

async def preview_item(self, device_id, item_id):
return await self.request("post", f"devices/{device_id}/preview/{item_id}")

async def delete_item(self, item_id):
return await self.request("delete", f"items/{item_id}")

async def send_postcard_cloud(self, device, url, content_type, name, author, description, medium, year):
_LOGGER.info('Meural device %s: Uploading content. URL is %s' % (
device['alias'], url))
response = await self.upload_content(url, content_type=content_type, name=name)

item_id = response["id"]
response = await self.update_content(id=item_id, name=name, author=author, description=description, medium=medium, year=year)

_LOGGER.info('Meural device %s: Sending postcard. Item Id: %s' % (
device['alias'], item_id))
response = await self.preview_item(device_id=device["id"], item_id=item_id)
_LOGGER.info('Meural device %s: Sending postcard. Sent for preview: %s' % (
device['alias'], item_id))

await asyncio.sleep(120)

response = await self.delete_item(item_id=item_id)
_LOGGER.info('Meural device %s: Sending postcard. Deleted the item: %s' % (
device['alias'], item_id))
return response


class LocalMeural:
def __init__(self, device, session: aiohttp.ClientSession):
self.ip = device["localIp"]
Expand Down Expand Up @@ -234,26 +324,28 @@ async def send_postcard(self, url, content_type):
data = aiohttp.FormData()
data.add_field('photo', image, content_type=content_type)
response = await self.session.post(f"http://{self.ip}/remote/postcard",
data=data)
data=data)
_LOGGER.info('Meural device %s: Sending postcard. Response: %s' % (
self.device['alias'], response))
text = await response.text()

r = json.loads(text)
_LOGGER.info('Meural device %s: Sending postcard. Image uploaded, status: %s, response: %s' % (
self.device['alias'], r['status'], r['response']))
self.device['alias'], r['status'], r['response']))
if r['status'] != 'pass':
_LOGGER.error('Meural device %s: Sending postcard. Could not upload, response: %s' % (
self.device['alias'], r['response']))

return response


class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""


class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""


class DeviceTurnedOff(HomeAssistantError):
"""Error to indicate device turned off or not connected to the network."""
37 changes: 33 additions & 4 deletions custom_components/meural/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ reset_brightness:
description: Entity ID of the Meural Canvas to update.
example: "media_player.meural-123"
toggle_informationcard:
description: Toggle display of the information card, a museum-style placard, on your Canvas.
description: Toggle display of the information card, a museum-style placard, on your Canvas.
fields:
entity_id:
description: Entity ID of the Meural Canvas to toggle display on.
Expand All @@ -39,6 +39,35 @@ preview_image:
content_type:
description: The type of image to preview. Can be image/jpg or image/png.
example: "image/png"

preview_image_cloud:
description: Preview an image from an URL on Meural Canvas (Uses Meural cloud storage to work around the sticky image issue when displaying an image using local API).
fields:
entity_id:
description: Entity ID of the Meural Canvas to update.
example: "media_player.meural-123"
content_url:
description: URL of the image to preview.
example: "https://home-assistant.io/images/cast/splash.png"
content_type:
description: The type of image to preview. Can be image/jpg or image/png.
example: "image/png"
name:
description: Name of the media item
example: "Game of Thrones"
author:
description: Author or Artist name
example: George Martin
description:
description: Description for the media item
example: Game of Thrones Poster image
medium:
description: Medium for the media item
example: Photography
year:
description: Year
example: 2022/05/01

set_device_option:
description: Set the configuration options of a Meural Canvas.
fields:
Expand All @@ -47,9 +76,9 @@ set_device_option:
example: "media_player.meural-123"
orientation:
description: Override the orientation of images on your Canvas. Can be horizontal, vertical.
example: "horizontal"
example: "horizontal"
orientationMatch:
description: Your Canvas will only show images that match its current orientation, i.e. if your Canvas is in vertical, only vertical images will display.
description: Your Canvas will only show images that match its current orientation, i.e. if your Canvas is in vertical, only vertical images will display.
example: "true"
alsEnabled:
description: Your Canvas will automatically adjusts its brightness to match its surroundings using the Ambient Light Sensor.
Expand Down Expand Up @@ -84,7 +113,7 @@ set_device_option:
backgroundColor:
description: Color displayed behind images that don't fill the frame. Can be grey, white, black.
example: "black"
fillMode:
fillMode:
description: How images will fill the screen if they don't match the Canvas' aspect ratio. Can be contain, auto crop, as is, stretch.
example: "auto crop"
galleryRotation:
Expand Down
7 changes: 7 additions & 0 deletions custom_components/meural/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from re import sub

def snake_case(s):
return '_'.join(
sub('([A-Z][a-z]+)', r' \1',
sub('([A-Z]+)', r' \1',
s.replace('-', ' '))).split()).lower()

0 comments on commit 04a8954

Please sign in to comment.