Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions src/external_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
CACHE_ROOT = "external_cache"
ICON_SUBDIR = "icons"
ICON_SIZE = (240, 240)
IMAGE_SIZE_MAX = 900


# Paths
Expand Down Expand Up @@ -119,7 +120,7 @@ def resize_image() -> str:
return None


async def encode_icon_to_data_uri(icon_name: str) -> str:
async def encode_image_to_data_uri(icon_name: str) -> str:
"""
Encode an image from a local file path or remote URL.

Expand All @@ -135,13 +136,20 @@ async def encode_icon_to_data_uri(icon_name: str) -> str:
_LOG.debug("Encoding icon to data URI: %s", icon_name)
try:
if _is_url(icon_name):
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient(verify=False) as client:
response = await client.get(icon_name, timeout=10)
response.raise_for_status()
img_bytes = BytesIO(response.content)

def encode_image() -> str:
img = Image.open(img_bytes)
size = max(img.size[0], img.size[1])
if size > IMAGE_SIZE_MAX:
percent = IMAGE_SIZE_MAX / float(size)
img = img.resize(
(int((float(img.size[0]) * float(percent))), int((float(img.size[1]) * float(percent)))),
Image.Resampling.LANCZOS,
)
img = img.convert("RGBA")
buffer = BytesIO()
img.save(buffer, format="PNG")
Expand Down Expand Up @@ -215,7 +223,7 @@ async def get_app_metadata(package_id: str) -> Dict[str, str]:
if package_id in cache:
_LOG.debug("Cache hit for %s", package_id)
icon_name = cache[package_id].get("icon")
icon_data_uri = await encode_icon_to_data_uri(icon_name) if icon_name else ""
icon_data_uri = await encode_image_to_data_uri(icon_name) if icon_name else ""
return {"name": cache[package_id]["name"], "icon": icon_data_uri}

_LOG.debug("Cache miss for %s", package_id)
Expand All @@ -224,7 +232,7 @@ async def get_app_metadata(package_id: str) -> Dict[str, str]:
if metadata:
cache[package_id] = metadata
_save_cache(cache)
icon_data_uri = await encode_icon_to_data_uri(metadata["icon"]) if metadata["icon"] else ""
icon_data_uri = await encode_image_to_data_uri(metadata["icon"]) if metadata["icon"] else ""
return {"name": metadata["name"], "icon": icon_data_uri}

_LOG.debug("Falling back to default metadata for %s", package_id)
Expand Down
17 changes: 11 additions & 6 deletions src/tv.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
import discover
import inputs
from config import AtvDevice
from external_metadata import encode_icon_to_data_uri, get_app_metadata
from external_metadata import encode_image_to_data_uri, get_app_metadata
from profiles import KeyPress, Profile
from util import filter_data_img_properties

Expand Down Expand Up @@ -611,7 +611,7 @@ async def _apply_current_app_metadata(self, current_app: str) -> dict:
# one-time initialization
if HOMESCREEN_IMAGE is None:
HOMESCREEN_IMAGE = ""
HOMESCREEN_IMAGE = await encode_icon_to_data_uri("config://androidtv.png")
HOMESCREEN_IMAGE = await encode_image_to_data_uri("config://androidtv.png")

# Special handling for homescreen & Android TV system apps: show pre-defined icon
homescreen_app = apps.is_homescreen_app(current_app)
Expand Down Expand Up @@ -916,16 +916,21 @@ async def _handle_new_media_status(self, status: MediaStatus):
self._media_type = GOOGLE_CAST_MEDIA_TYPES_MAP.get(status.metadata_type, MediaType.VIDEO)
update[MediaAttr.MEDIA_TYPE] = self._media_type

if status.images and len(status.images) > 0 and status.images[0].url != self._media_image_url:
self._media_image_url = status.images[0].url
update[MediaAttr.MEDIA_IMAGE_URL] = self._media_image_url
if status.images and len(status.images) > 0:
if status.images[0].url != self._media_image_url:
self._media_image_url = status.images[0].url
# The remote does not support https URL images, encode them
if self._media_image_url.lower().startswith("https"):
update[MediaAttr.MEDIA_IMAGE_URL] = await encode_image_to_data_uri(self._media_image_url)
else:
update[MediaAttr.MEDIA_IMAGE_URL] = self._media_image_url

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https image urls are supported in the UI! I just verified it again and it works.

What's not supported are invalid and self-signed certificates.
Does httpx.AsyncClient(verify=False) in src/external_metadata.py disable certificate validation?

Base64 embedding is quite an overhead and should be avoided when possible. The image now also gets resized twice, in the integration and again in the UI.
Large base64 data can create WebSocket issues if the image is too large. The entity change events are distributed to every connected client using the Core-API, for example the web-configurator.

If there are problematic URLs that can't be processed in the UI then it should be fixed there, solving it for every integration. I'll check about certificate checks in the UI if they could be disabled with a user option, even though I have quite a strong opinion that one should only use valid certificates :-)

@albaintor albaintor Dec 2, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At start I thought that https wasn't supported but Marton said it was and this narrowed this down to the certificate (self signed). But this is not possible to determine if the https is or not self signed

Yes httpx.AsyncClient(verify=False) disables SSL verification and this change is necessary for some apps such as Plex hosting media files locally (on a NAS...) and exposing https image urls.

But if you disable it at the UI/core level it will indeed work for all. I just assumed that the risk was lower to make it only for AndroidTV

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To test if https works is easy: hardcode a https image url from a public website in tv.py, for example:

update[MediaAttr.MEDIA_IMAGE_URL] = "https://brands.home-assistant.io/_/conversation/logo@2x.png"

There might still be some occasional hickups in the UI showing the loading dot. We'll check that again too.

httpx.AsyncClient makes it too easy disabling certificate checks. No wonder we are still using self signed certificates :-( Let's Encrypt has been around for a while... /rant off

self._use_app_url = False
else:
self._media_image_url = None
if self._device_config.use_external_metadata:
self._use_app_url = True
if self._app_image_url:
update[MediaAttr.MEDIA_IMAGE_URL] = await encode_icon_to_data_uri(self._app_image_url)
update[MediaAttr.MEDIA_IMAGE_URL] = await encode_image_to_data_uri(self._app_image_url)

if update:
if _LOG.isEnabledFor(logging.DEBUG):
Expand Down