Skip to content

Commit 1b49e5f

Browse files
committed
Make image links update only when media changes, fix login errors, and reuse HA sessions for better performance
1 parent 1bd8acf commit 1b49e5f

File tree

6 files changed

+68
-17
lines changed

6 files changed

+68
-17
lines changed

custom_components/plex_recently_added/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
5050
raise ConfigEntryNotReady("Failed to Log-in") from err
5151
coordinator = PlexDataCoordinator(hass, client)
5252

53-
hass.http.register_view(ImagesRedirect(config_entry))
53+
hass.http.register_view(ImagesRedirect(hass, config_entry))
5454
await coordinator.async_config_entry_first_refresh()
5555
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = coordinator
5656

custom_components/plex_recently_added/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
DOMAIN: Final = "plex_recently_added"
44
TIMEOUT_MINUTES: Final = 10
5+
POLL_INTERVAL_MINUTES: Final = 10
6+
SIGN_URL_TTL_MINUTES: Final = 10080 # 7 days
57

68

79
DEFAULT_NAME: Final = 'Plex Recently Added'

custom_components/plex_recently_added/coordinator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from homeassistant.core import HomeAssistant
77
from homeassistant.exceptions import ConfigEntryError
88

9-
from .const import DOMAIN, TIMEOUT_MINUTES
9+
from .const import DOMAIN, POLL_INTERVAL_MINUTES
1010
from .plex_api import (
1111
PlexApi,
1212
FailedToLogin,
@@ -23,11 +23,11 @@ def __init__(self, hass: HomeAssistant, client: PlexApi):
2323
_LOGGER,
2424
name=DOMAIN,
2525
update_method=self._async_update_data,
26-
update_interval=timedelta(minutes=TIMEOUT_MINUTES),
26+
update_interval=timedelta(minutes=POLL_INTERVAL_MINUTES),
2727
)
2828

2929
async def _async_update_data(self) -> Dict[str, Any]:
3030
try:
3131
return await self._client.update()
32-
except FailedToLogin:
32+
except FailedToLogin as err:
3333
raise ConfigEntryError("Failed to Log-in") from err

custom_components/plex_recently_added/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@
99
"iot_class": "local_polling",
1010
"issue_tracker": "https://github.com/custom-components/sensor.plex_recently_added/issues",
1111
"requirements": [],
12-
"version": "0.4.8"
12+
"version": "0.5.4"
1313
}

custom_components/plex_recently_added/parser.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,26 @@
99
from datetime import datetime, timedelta
1010
from homeassistant.core import HomeAssistant
1111
from homeassistant.components.http.auth import async_sign_path
12-
13-
from .const import TIMEOUT_MINUTES
12+
from time import time
13+
14+
# Stable signed-path cache: prevents new URLs on each 10-min refresh
15+
# Re-signs only when near expiry, so browsers don't re-download unchanged images.
16+
_SIGNED_URL_CACHE = {} # key: raw_path (without authSig), value: (signed_url, expires_epoch)
17+
_STALE_MARGIN_SEC = 24 * 3600 # renew 1 day before expiry to be safe
18+
19+
def _stable_signed_path(hass: HomeAssistant, raw_path: str, ttl_minutes: int) -> str:
20+
now = time()
21+
entry = _SIGNED_URL_CACHE.get(raw_path)
22+
if entry:
23+
signed_url, exp = entry
24+
if exp - now > _STALE_MARGIN_SEC:
25+
return signed_url
26+
# Sign a fresh URL and remember its approximate expiry
27+
signed = async_sign_path(hass, raw_path, timedelta(minutes=ttl_minutes))
28+
_SIGNED_URL_CACHE[raw_path] = (signed, now + ttl_minutes * 60)
29+
return signed
30+
31+
from .const import SIGN_URL_TTL_MINUTES
1432

1533
import logging
1634
_LOGGER = logging.getLogger(__name__)
@@ -94,11 +112,19 @@ def parse_data(hass: HomeAssistant, data, max, base_url, token, identifier, sect
94112

95113

96114
thumb_IDs = extract_metadata_and_type(thumb)
97-
data_output["poster"] = async_sign_path(hass, f'{images_base_url}?metadata={thumb_IDs[0]}&thumb={thumb_IDs[2]}', timedelta(minutes=TIMEOUT_MINUTES)) if thumb_IDs else ""
115+
data_output["poster"] = _stable_signed_path(
116+
hass,
117+
f'{images_base_url}?metadata={thumb_IDs[0]}&thumb={thumb_IDs[2]}&v={int(item.get("updatedAt", item.get("addedAt", 0)))}',
118+
SIGN_URL_TTL_MINUTES
119+
) if thumb_IDs else ""
98120

99121

100122
art_IDs = extract_metadata_and_type(art)
101-
data_output["fanart"] = async_sign_path(hass, f'{images_base_url}?metadata={art_IDs[0]}&art={art_IDs[2]}', timedelta(minutes=TIMEOUT_MINUTES)) if art_IDs else ""
123+
data_output["fanart"] = _stable_signed_path(
124+
hass,
125+
f'{images_base_url}?metadata={art_IDs[0]}&art={art_IDs[2]}&v={int(item.get("updatedAt", item.get("addedAt", 0)))}',
126+
SIGN_URL_TTL_MINUTES
127+
) if art_IDs else ""
102128

103129

104130
data_output["deep_link"] = deep_link if identifier else None

custom_components/plex_recently_added/redirect.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from homeassistant.components.http import HomeAssistantView
22
from homeassistant.config_entries import ConfigEntry
3-
from aiohttp import web, ClientSession
3+
from aiohttp import web
4+
from homeassistant.helpers.aiohttp_client import async_get_clientsession
45
import requests
56
import os
67

@@ -15,12 +16,13 @@
1516
from .const import DOMAIN
1617

1718
class ImagesRedirect(HomeAssistantView):
18-
def __init__(self, config_entry: ConfigEntry):
19+
def __init__(self, hass, config_entry: ConfigEntry):
1920
super().__init__()
2021
self._token = config_entry.data[CONF_API_KEY]
2122
self._base_url = f'http{'s' if config_entry.data[CONF_SSL] else ''}://{config_entry.data[CONF_HOST]}:{config_entry.data[CONF_PORT]}'
2223
self.name = f'{self._token}_Plex_Recently_Added'
2324
self.url = f'/{config_entry.data[CONF_NAME].lower() + "_" if len(config_entry.data[CONF_NAME]) > 0 else ""}plex_recently_added'
25+
self._session = async_get_clientsession(hass)
2426

2527
async def get(self, request):
2628
metadataId = int(request.query.get("metadata", 0))
@@ -34,12 +36,33 @@ async def get(self, request):
3436

3537
url = f'{self._base_url}/library/metadata/{metadataId}/{image_type}/{image_id}?X-Plex-Token={self._token}'
3638

37-
async with ClientSession() as session:
38-
async with session.get(url) as res:
39-
if res.ok:
40-
content = await res.read()
41-
return web.Response(body=content, content_type=res.content_type)
39+
fwd_headers = {}
40+
if_modified = request.headers.get("If-Modified-Since")
41+
if_none = request.headers.get("If-None-Match")
42+
if if_modified:
43+
fwd_headers["If-Modified-Since"] = if_modified
44+
if if_none:
45+
fwd_headers["If-None-Match"] = if_none
4246

43-
return web.HTTPNotFound()
47+
async with self._session.get(url, headers=fwd_headers, timeout=10) as res:
48+
if res.status == 304:
49+
return web.Response(status=304)
50+
51+
if res.status == 200:
52+
body = await res.read()
53+
headers = {
54+
"Content-Type": res.headers.get("Content-Type", "image/jpeg"),
55+
# Strong client caching: immutable for a year cuts repeat requests
56+
"Cache-Control": "public, max-age=31536000, immutable",
57+
}
58+
etag = res.headers.get("ETag")
59+
last_mod = res.headers.get("Last-Modified")
60+
if etag:
61+
headers["ETag"] = etag
62+
if last_mod:
63+
headers["Last-Modified"] = last_mod
64+
return web.Response(body=body, headers=headers)
65+
66+
return web.HTTPNotFound()
4467

4568

0 commit comments

Comments
 (0)