Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
41 changes: 36 additions & 5 deletions src/external_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import json
import logging
import os
import re
from io import BytesIO
from pathlib import Path
from typing import Dict
Expand All @@ -27,6 +28,7 @@
CACHE_ROOT = "external_cache"
ICON_SUBDIR = "icons"
ICON_SIZE = (240, 240)
IMAGE_SIZE_MAX = 900


# Paths
Expand Down Expand Up @@ -119,14 +121,14 @@ 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.

Returns a base64-encoded PNG data URI.
"""
if isinstance(icon_name, MediaImage):
icon_name = icon_name.url
icon_name: str = icon_name.url

if isinstance(icon_name, str) and icon_name.startswith("data:image"):
_LOG.debug("Icon is already a data URI")
Expand All @@ -135,13 +137,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 +224,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,8 +233,30 @@ 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)
return {"name": package_id, "icon": ""}


def reformat_media_image_url(url: str) -> str:
"""
Reformat media image URL for well known services to correspond to remote capabilities.

:param url: The media image URL.
:type url: str
:return: Reformatted image URL.
:rtype: str
"""
# Plex URL format : https://xxx/photo/:/transcode?...height=xxx...width=yyy
if re.search(r"/photo/:/transcode\?", url, re.IGNORECASE):
for argument in ["width", "height"]:
if value := re.search(rf"{argument}=(\d+)", url, re.IGNORECASE):
try:
if value and int(value.group(1)) > IMAGE_SIZE_MAX:
new_size = IMAGE_SIZE_MAX
url = re.sub(rf"{argument}=(\d+)", f"{argument}={new_size}", url, re.IGNORECASE)
except ValueError:
pass
Comment on lines +253 to +261

@zehnm zehnm Dec 19, 2025

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.

Please do not use regex for modifying URLs. This is not recommended and dangerous. Too many things can go wrong.
Either use the built-in urllib.parse module or a 3rd party module like furl.

Quick AI assist:

from urllib.parse import urlparse, parse_qs, urlencode, urlunparse

# Your original URL
url = "https://192.168.1.10:32400/photo/:/transcode?height=1800&machineIdentifier=xxx&quality=90&url=http%3A%2F%2F127.0.0.1%3A32400%2Flibrary%2Fmetadata%2F20%2Fthumb%2F1764553003&width=1200&X-Plex-Token=abc"

# 1. Parse the URL into components
parsed_url = urlparse(url)

# 2. Parse the query string into a dictionary
query_dict = parse_qs(parsed_url.query, keep_blank_values=True)

# 3. Define your modification logic
IMAGE_SIZE_MAX = 900

# Convert values from list (from parse_qs) to simple string for logic
if 'height' in query_dict:
    height_val = int(query_dict['height'][0])
    if height_val > IMAGE_SIZE_MAX:
        query_dict['height'] = [str(IMAGE_SIZE_MAX)] # Set back as a list

if 'width' in query_dict:
    width_val = int(query_dict['width'][0])
    if width_val > IMAGE_SIZE_MAX:
        query_dict['width'] = [str(IMAGE_SIZE_MAX)] # Set back as a list

# 4. Re-encode the modified query parameters
new_query_string = urlencode(query_dict, doseq=True)

# 5. Reconstruct the full URL with the new query string
modified_url = urlunparse((
    parsed_url.scheme,
    parsed_url.netloc,
    parsed_url.path,
    parsed_url.params,
    new_query_string,
    parsed_url.fragment
))

print(modified_url)

Warning: untested, never trust AI!

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.

I've added Plex URL rewrite in PR #123

return url
13 changes: 9 additions & 4 deletions src/tv.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@
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,
reformat_media_image_url,
)
from profiles import KeyPress, Profile
from util import filter_data_img_properties

Expand Down Expand Up @@ -611,7 +615,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 @@ -919,14 +923,15 @@ async def _handle_new_media_status(self, status: MediaStatus):
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
update[MediaAttr.MEDIA_IMAGE_URL] = self._media_image_url
# Reformat the media image URL if necessary (image size parameters)
update[MediaAttr.MEDIA_IMAGE_URL] = reformat_media_image_url(self._media_image_url)
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