Skip to content

Commit da14206

Browse files
authored
feat: Optimize Plex artwork image loading (#123)
Change Plex image sizes to a maximum width or height of 480px. Closes #80 Fixes #113
1 parent f44014b commit da14206

3 files changed

Lines changed: 139 additions & 5 deletions

File tree

src/external_metadata.py

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
import os
1313
from io import BytesIO
1414
from pathlib import Path
15-
from typing import Dict
16-
from urllib.parse import urlparse
15+
from typing import Dict, Literal
16+
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
1717

1818
import google_play_scraper
1919
import httpx
@@ -27,6 +27,7 @@
2727
CACHE_ROOT = "external_cache"
2828
ICON_SUBDIR = "icons"
2929
ICON_SIZE = (240, 240)
30+
IMAGE_SIZE_MAX = 480
3031

3132

3233
# Paths
@@ -171,8 +172,12 @@ def load_and_encode() -> str:
171172

172173

173174
def _is_url(path: str) -> bool:
174-
parsed = urlparse(path)
175-
return parsed.scheme in ("http", "https")
175+
"""Check if the provided string is a valid http/https URL."""
176+
try:
177+
parsed = urlparse(path)
178+
return parsed.scheme in ("http", "https") and bool(parsed.netloc)
179+
except (ValueError, AttributeError):
180+
return False
176181

177182

178183
async def _fetch_google_play_metadata(package_id: str) -> Dict[str, str] | None:
@@ -229,3 +234,72 @@ async def get_app_metadata(package_id: str) -> Dict[str, str]:
229234

230235
_LOG.debug("Falling back to default metadata for %s", package_id)
231236
return {"name": package_id, "icon": ""}
237+
238+
239+
def get_resized_image_url(url: str, max_size: int = IMAGE_SIZE_MAX) -> str | Literal[b""]:
240+
"""
241+
Adjust width and height query parameters while maintaining aspect ratio.
242+
243+
Only performs the rewrite if the URL path contains '/photo/:/transcode' (Plex transcode URL).
244+
- If parameters are missing: returns original URL.
245+
- If parameters are invalid (non-numeric/<=0): returns original URL.
246+
- If one parameter exists: sets it to max_size.
247+
- If both exist: resizes proportionally to fit within max_size.
248+
"""
249+
if not url or not _is_url(url):
250+
_LOG.warning("Invalid URL provided for resizing: %s", url)
251+
return url
252+
253+
parsed_url = urlparse(url)
254+
255+
# Only rewrite Plex transcode URLs
256+
# Other services can be added later
257+
if "/photo/:/transcode" not in parsed_url.path:
258+
return url
259+
260+
query_dict = parse_qs(parsed_url.query, keep_blank_values=True)
261+
262+
width_str = query_dict.get("width", [None])[0]
263+
height_str = query_dict.get("height", [None])[0]
264+
265+
# Helper to validate and convert numeric strings
266+
def _safe_int(val):
267+
try:
268+
res = int(val)
269+
return res if res > 0 else None
270+
except (TypeError, ValueError):
271+
return None
272+
273+
w = _safe_int(width_str)
274+
h = _safe_int(height_str)
275+
276+
# both parameters present
277+
if w is not None and h is not None:
278+
if w > max_size or h > max_size:
279+
if w > h:
280+
new_w = max_size
281+
new_h = max(1, int(h * (max_size / w)))
282+
else:
283+
new_h = max_size
284+
new_w = max(1, int(w * (max_size / h)))
285+
286+
query_dict["width"] = [str(new_w)]
287+
query_dict["height"] = [str(new_h)]
288+
else:
289+
return url # No resizing needed
290+
291+
# only width present
292+
elif w is not None:
293+
query_dict["width"] = [str(max_size)]
294+
295+
# only height present
296+
elif h is not None:
297+
query_dict["height"] = [str(max_size)]
298+
299+
# If neither are valid/present, return original
300+
else:
301+
return url
302+
303+
# Reconstruct the URL safely
304+
new_query = urlencode(query_dict, doseq=True)
305+
return urlunparse(parsed_url._replace(query=new_query))

src/tv.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@
5151
import discover
5252
import inputs
5353
from config import AtvDevice
54-
from external_metadata import encode_icon_to_data_uri, get_app_metadata
54+
from external_metadata import (
55+
encode_icon_to_data_uri,
56+
get_app_metadata,
57+
get_resized_image_url,
58+
)
5559
from profiles import KeyPress, Profile
5660
from util import filter_data_img_properties
5761

@@ -974,6 +978,8 @@ async def _handle_new_media_status(self, status: MediaStatus):
974978
if status.images[0].url != self._media_image_url:
975979
self._media_image_url = status.images[0].url
976980
update[MediaAttr.MEDIA_IMAGE_URL] = self._media_image_url
981+
# Reformat the media image URL if necessary (image size parameters)
982+
update[MediaAttr.MEDIA_IMAGE_URL] = get_resized_image_url(self._media_image_url)
977983
self._use_app_url = False
978984
else:
979985
self._media_image_url = None

tests/test_external_metadata.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import unittest
2+
3+
from src.external_metadata import get_resized_image_url
4+
5+
6+
class TestGetResizedImageUrl(unittest.TestCase):
7+
8+
def test_valid_url_with_both_dimensions_exceeding_max_size(self):
9+
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"
10+
result = get_resized_image_url(url, max_size=480)
11+
expected_url = "https://192.168.1.10:32400/photo/:/transcode?height=480&machineIdentifier=xxx&quality=90&url=http%3A%2F%2F127.0.0.1%3A32400%2Flibrary%2Fmetadata%2F20%2Fthumb%2F1764553003&width=320&X-Plex-Token=abc"
12+
self.assertEqual(expected_url, result, "Expected resized dimensions within max_size")
13+
14+
def test_valid_url_with_only_width_parameter(self):
15+
url = "https://192.168.1.10:32400/photo/:/transcode?machineIdentifier=xxx&quality=90&url=http%3A%2F%2F127.0.0.1%3A32400%2Flibrary%2Fmetadata%2F20%2Fthumb%2F1764553003&width=1200&X-Plex-Token=abc"
16+
result = get_resized_image_url(url, max_size=480)
17+
expected_url = "https://192.168.1.10:32400/photo/:/transcode?machineIdentifier=xxx&quality=90&url=http%3A%2F%2F127.0.0.1%3A32400%2Flibrary%2Fmetadata%2F20%2Fthumb%2F1764553003&width=480&X-Plex-Token=abc"
18+
self.assertEqual(expected_url, result, "Expected max_size applied only to width")
19+
20+
def test_valid_url_with_only_height_parameter(self):
21+
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&X-Plex-Token=abc"
22+
result = get_resized_image_url(url, max_size=480)
23+
expected_url = "https://192.168.1.10:32400/photo/:/transcode?height=480&machineIdentifier=xxx&quality=90&url=http%3A%2F%2F127.0.0.1%3A32400%2Flibrary%2Fmetadata%2F20%2Fthumb%2F1764553003&X-Plex-Token=abc"
24+
self.assertEqual(expected_url, result, "Expected max_size applied only to height")
25+
26+
def test_valid_url_with_both_dimensions_within_max_size(self):
27+
url = "http://example.com/photo/:/transcode?width=50&height=40"
28+
result = get_resized_image_url(url, max_size=100)
29+
self.assertEqual(url, result, "Expected original URL as dimensions fit within max_size")
30+
31+
def test_url_with_invalid_width_and_height(self):
32+
url = "http://example.com/photo/:/transcode?width=-50&height=abc"
33+
result = get_resized_image_url(url, max_size=100)
34+
self.assertEqual(url, result, "Expected original URL for invalid width and height")
35+
36+
def test_invalid_url(self):
37+
url = "not_a_valid_url"
38+
result = get_resized_image_url(url, max_size=100)
39+
self.assertEqual(url, result, "Expected original URL for an invalid URL input")
40+
41+
def test_empty_url(self):
42+
url = ""
43+
result = get_resized_image_url(url, max_size=100)
44+
self.assertEqual(url, result, "Expected original empty URL")
45+
46+
def test_url_without_query_parameters(self):
47+
url = "http://example.com/photo/:/transcode"
48+
result = get_resized_image_url(url, max_size=100)
49+
self.assertEqual(url, result, "Expected original URL when no query parameters are present")
50+
51+
def test_valid_non_plex_url_with_both_dimensions_exceeding_max_size(self):
52+
url = "https://192.168.1.10:32400/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"
53+
result = get_resized_image_url(url, max_size=480)
54+
self.assertEqual(url, result, "Non-Plex URLs should not be modified")

0 commit comments

Comments
 (0)