From 2c1a754e5de421ca3d1708e1574112ac07010051 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 29 Nov 2024 21:25:59 -0800 Subject: [PATCH] Make uploaded images browsable in media (#131468) * Make uploaded images browsable in media * tests * Update homeassistant/components/image_upload/media_source.py Co-authored-by: Martin Hjelmare * use executor * more executor * use thumbnail --------- Co-authored-by: Martin Hjelmare --- .../components/image_upload/media_source.py | 76 ++++++++++++++++ .../image_upload/test_media_source.py | 90 +++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 homeassistant/components/image_upload/media_source.py create mode 100644 tests/components/image_upload/test_media_source.py diff --git a/homeassistant/components/image_upload/media_source.py b/homeassistant/components/image_upload/media_source.py new file mode 100644 index 00000000000..ee9511e2c36 --- /dev/null +++ b/homeassistant/components/image_upload/media_source.py @@ -0,0 +1,76 @@ +"""Expose image_upload as media sources.""" + +from __future__ import annotations + +from homeassistant.components.media_player import BrowseError, MediaClass +from homeassistant.components.media_source import ( + BrowseMediaSource, + MediaSource, + MediaSourceItem, + PlayMedia, + Unresolvable, +) +from homeassistant.core import HomeAssistant + +from .const import DOMAIN + + +async def async_get_media_source(hass: HomeAssistant) -> ImageUploadMediaSource: + """Set up image media source.""" + return ImageUploadMediaSource(hass) + + +class ImageUploadMediaSource(MediaSource): + """Provide images as media sources.""" + + name: str = "Image Upload" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize ImageMediaSource.""" + super().__init__(DOMAIN) + self.hass = hass + + async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: + """Resolve media to a url.""" + image = self.hass.data[DOMAIN].data.get(item.identifier) + + if not image: + raise Unresolvable(f"Could not resolve media item: {item.identifier}") + + return PlayMedia( + f"/api/image/serve/{image['id']}/original", image["content_type"] + ) + + async def async_browse_media( + self, + item: MediaSourceItem, + ) -> BrowseMediaSource: + """Return media.""" + if item.identifier: + raise BrowseError("Unknown item") + + children = [ + BrowseMediaSource( + domain=DOMAIN, + identifier=image["id"], + media_class=MediaClass.IMAGE, + media_content_type=image["content_type"], + title=image["name"], + thumbnail=f"/api/image/serve/{image['id']}/256x256", + can_play=True, + can_expand=False, + ) + for image in self.hass.data[DOMAIN].data.values() + ] + + return BrowseMediaSource( + domain=DOMAIN, + identifier=None, + media_class=MediaClass.APP, + media_content_type="", + title="Image Upload", + can_play=False, + can_expand=True, + children_media_class=MediaClass.IMAGE, + children=children, + ) diff --git a/tests/components/image_upload/test_media_source.py b/tests/components/image_upload/test_media_source.py new file mode 100644 index 00000000000..d66e099bdc9 --- /dev/null +++ b/tests/components/image_upload/test_media_source.py @@ -0,0 +1,90 @@ +"""Test image_upload media source.""" + +import tempfile +from unittest.mock import patch + +from aiohttp import ClientSession +import pytest + +from homeassistant.components import media_source +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import TEST_IMAGE + +from tests.typing import ClientSessionGenerator + + +@pytest.fixture(autouse=True) +async def setup_media_source(hass: HomeAssistant) -> None: + """Set up media source.""" + assert await async_setup_component(hass, "media_source", {}) + + +async def __upload_test_image( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, +) -> str: + with ( + tempfile.TemporaryDirectory() as tempdir, + patch.object(hass.config, "path", return_value=tempdir), + ): + assert await async_setup_component(hass, "image_upload", {}) + client: ClientSession = await hass_client() + + file = await hass.async_add_executor_job(TEST_IMAGE.open, "rb") + res = await client.post("/api/image/upload", data={"file": file}) + hass.async_add_executor_job(file.close) + + assert res.status == 200 + item = await res.json() + assert item["content_type"] == "image/png" + assert item["filesize"] == 38847 + return item["id"] + + +async def test_browsing( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, +) -> None: + """Test browsing image media source.""" + image_id = await __upload_test_image(hass, hass_client) + + item = await media_source.async_browse_media(hass, "media-source://image_upload") + + assert item is not None + assert item.title == "Image Upload" + assert len(item.children) == 1 + assert item.children[0].media_content_type == "image/png" + assert item.children[0].identifier == image_id + assert item.children[0].thumbnail == f"/api/image/serve/{image_id}/256x256" + + with pytest.raises( + media_source.BrowseError, + match="Unknown item", + ): + await media_source.async_browse_media( + hass, "media-source://image_upload/invalid_path" + ) + + +async def test_resolving( + hass: HomeAssistant, hass_client: ClientSessionGenerator +) -> None: + """Test resolving.""" + image_id = await __upload_test_image(hass, hass_client) + item = await media_source.async_resolve_media( + hass, f"media-source://image_upload/{image_id}", None + ) + assert item is not None + assert item.url == f"/api/image/serve/{image_id}/original" + assert item.mime_type == "image/png" + + invalid_id = "aabbccddeeff" + with pytest.raises( + media_source.Unresolvable, + match=f"Could not resolve media item: {invalid_id}", + ): + await media_source.async_resolve_media( + hass, f"media-source://image_upload/{invalid_id}", None + )