diff --git a/.coveragerc b/.coveragerc index 90437cac34e..da4bdaede00 100644 --- a/.coveragerc +++ b/.coveragerc @@ -919,6 +919,7 @@ omit = homeassistant/components/plaato/entity.py homeassistant/components/plaato/sensor.py homeassistant/components/plex/media_player.py + homeassistant/components/plex/view.py homeassistant/components/plum_lightpad/light.py homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/__init__.py diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 2de42c05dde..99d493a75c4 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -999,25 +999,7 @@ class MediaPlayerEntity(Entity): async def _async_fetch_image(self, url: str) -> tuple[bytes | None, str | None]: """Retrieve an image.""" - content, content_type = (None, None) - websession = async_get_clientsession(self.hass) - with suppress(asyncio.TimeoutError), async_timeout.timeout(10): - response = await websession.get(url) - if response.status == HTTPStatus.OK: - content = await response.read() - if content_type := response.headers.get(CONTENT_TYPE): - content_type = content_type.split(";")[0] - - if content is None: - url_parts = URL(url) - if url_parts.user is not None: - url_parts = url_parts.with_user("xxxx") - if url_parts.password is not None: - url_parts = url_parts.with_password("xxxxxxxx") - url = str(url_parts) - _LOGGER.warning("Error retrieving proxied image from %s", url) - - return content, content_type + return await async_fetch_image(_LOGGER, self.hass, url) def get_browse_image_url( self, @@ -1205,3 +1187,28 @@ async def websocket_browse_media(hass, connection, msg): _LOGGER.warning("Browse Media should use new BrowseMedia class") connection.send_result(msg["id"], payload) + + +async def async_fetch_image( + logger: logging.Logger, hass: HomeAssistant, url: str +) -> tuple[bytes | None, str | None]: + """Retrieve an image.""" + content, content_type = (None, None) + websession = async_get_clientsession(hass) + with suppress(asyncio.TimeoutError), async_timeout.timeout(10): + response = await websession.get(url) + if response.status == HTTPStatus.OK: + content = await response.read() + if content_type := response.headers.get(CONTENT_TYPE): + content_type = content_type.split(";")[0] + + if content is None: + url_parts = URL(url) + if url_parts.user is not None: + url_parts = url_parts.with_user("xxxx") + if url_parts.password is not None: + url_parts = url_parts.with_password("xxxxxxxx") + url = str(url_parts) + logger.warning("Error retrieving proxied image from %s", url) + + return content, content_type diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 26a158d240f..44bca818333 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -48,6 +48,7 @@ from .errors import ShouldUpdateConfigEntry from .media_browser import browse_media from .server import PlexServer from .services import async_setup_services +from .view import PlexImageView _LOGGER = logging.getLogger(__package__) @@ -84,6 +85,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: await async_setup_services(hass) + hass.http.register_view(PlexImageView()) + gdm = hass.data[PLEX_DOMAIN][GDM_SCANNER] = GDM() def gdm_scan(): diff --git a/homeassistant/components/plex/media_browser.py b/homeassistant/components/plex/media_browser.py index 0bff5cfb5cd..11599086179 100644 --- a/homeassistant/components/plex/media_browser.py +++ b/homeassistant/components/plex/media_browser.py @@ -1,4 +1,6 @@ """Support to interface with the Plex API.""" +from __future__ import annotations + import logging from homeassistant.components.media_player import BrowseMedia @@ -73,7 +75,15 @@ def browse_media( # noqa: C901 "can_expand": item.type in EXPANDABLES, } if hasattr(item, "thumbUrl"): - payload["thumbnail"] = item.thumbUrl + plex_server.thumbnail_cache.setdefault(str(item.ratingKey), item.thumbUrl) + if is_internal: + thumbnail = item.thumbUrl + else: + thumbnail = get_proxy_image_url( + plex_server.machine_identifier, + item.ratingKey, + ) + payload["thumbnail"] = thumbnail return BrowseMedia(**payload) @@ -321,3 +331,11 @@ def station_payload(station): can_play=True, can_expand=False, ) + + +def get_proxy_image_url( + server_id: str, + media_content_id: str, +) -> str: + """Generate an url for a Plex media browser image.""" + return f"/api/plex_image_proxy/{server_id}/{media_content_id}" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 1ff58ed468d..9b8b0df14c7 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -585,17 +585,3 @@ class PlexMediaPlayer(MediaPlayerEntity): media_content_type, media_content_id, ) - - async def async_get_browse_image( - self, - media_content_type: str, - media_content_id: str, - media_image_id: str | None = None, - ) -> tuple[bytes | None, str | None]: - """Get media image from Plex server.""" - image_url = self.plex_server.thumbnail_cache.get(media_content_id) - if image_url: - result = await self._async_fetch_image(image_url) - return result - - return (None, None) diff --git a/homeassistant/components/plex/view.py b/homeassistant/components/plex/view.py new file mode 100644 index 00000000000..3cb7d40b2de --- /dev/null +++ b/homeassistant/components/plex/view.py @@ -0,0 +1,48 @@ +"""Implement a view to provide proxied Plex thumbnails to the media browser.""" +from __future__ import annotations + +from http import HTTPStatus +import logging + +from aiohttp import web +from aiohttp.hdrs import CACHE_CONTROL +from aiohttp.typedefs import LooseHeaders + +from homeassistant.components.http import KEY_AUTHENTICATED, HomeAssistantView +from homeassistant.components.media_player import async_fetch_image + +from .const import DOMAIN as PLEX_DOMAIN, SERVERS + +_LOGGER = logging.getLogger(__name__) + + +class PlexImageView(HomeAssistantView): + """Media player view to serve a Plex image.""" + + name = "api:plex:image" + url = "/api/plex_image_proxy/{server_id}/{media_content_id}" + + async def get( # pylint: disable=no-self-use + self, + request: web.Request, + server_id: str, + media_content_id: str, + ) -> web.Response: + """Start a get request.""" + if not request[KEY_AUTHENTICATED]: + return web.Response(status=HTTPStatus.UNAUTHORIZED) + + hass = request.app["hass"] + if (server := hass.data[PLEX_DOMAIN][SERVERS].get(server_id)) is None: + return web.Response(status=HTTPStatus.NOT_FOUND) + + if (image_url := server.thumbnail_cache.get(media_content_id)) is None: + return web.Response(status=HTTPStatus.NOT_FOUND) + + data, content_type = await async_fetch_image(_LOGGER, hass, image_url) + + if data is None: + return web.Response(status=HTTPStatus.SERVICE_UNAVAILABLE) + + headers: LooseHeaders = {CACHE_CONTROL: "max-age=3600"} + return web.Response(body=data, content_type=content_type, headers=headers)