mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Add support for videos in Immich media source (#145254)
add support for videos
This commit is contained in:
parent
abcf925b79
commit
b0415588d7
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from logging import getLogger
|
||||
import mimetypes
|
||||
|
||||
from aiohttp.web import HTTPNotFound, Request, Response
|
||||
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse
|
||||
from aioimmich.exceptions import ImmichError
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
@ -20,6 +20,7 @@ from homeassistant.components.media_source import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import ChunkAsyncStreamIterator
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ImmichConfigEntry
|
||||
@ -136,7 +137,7 @@ class ImmichMediaSource(MediaSource):
|
||||
except ImmichError:
|
||||
return []
|
||||
|
||||
return [
|
||||
ret = [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=(
|
||||
@ -156,6 +157,28 @@ class ImmichMediaSource(MediaSource):
|
||||
if asset.mime_type.startswith("image/")
|
||||
]
|
||||
|
||||
ret.extend(
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=(
|
||||
f"{identifier.unique_id}/"
|
||||
f"{identifier.album_id}/"
|
||||
f"{asset.asset_id}/"
|
||||
f"{asset.file_name}"
|
||||
),
|
||||
media_class=MediaClass.VIDEO,
|
||||
media_content_type=asset.mime_type,
|
||||
title=asset.file_name,
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail.jpg/thumbnail",
|
||||
)
|
||||
for asset in album_info.assets
|
||||
if asset.mime_type.startswith("video/")
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
"""Resolve media to a url."""
|
||||
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
||||
@ -184,12 +207,12 @@ class ImmichMediaView(HomeAssistantView):
|
||||
|
||||
async def get(
|
||||
self, request: Request, source_dir_id: str, location: str
|
||||
) -> Response:
|
||||
) -> Response | StreamResponse:
|
||||
"""Start a GET request."""
|
||||
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
raise HTTPNotFound
|
||||
asset_id, file_name, size = location.split("/")
|
||||
|
||||
asset_id, file_name, size = location.split("/")
|
||||
mime_type, _ = mimetypes.guess_type(file_name)
|
||||
if not isinstance(mime_type, str):
|
||||
raise HTTPNotFound
|
||||
@ -202,6 +225,20 @@ class ImmichMediaView(HomeAssistantView):
|
||||
assert entry
|
||||
immich_api = entry.runtime_data.api
|
||||
|
||||
# stream response for videos
|
||||
if mime_type.startswith("video/"):
|
||||
try:
|
||||
resp = await immich_api.assets.async_play_video_stream(asset_id)
|
||||
except ImmichError as exc:
|
||||
raise HTTPNotFound from exc
|
||||
stream = ChunkAsyncStreamIterator(resp)
|
||||
response = StreamResponse()
|
||||
await response.prepare(request)
|
||||
async for chunk in stream:
|
||||
await response.write(chunk)
|
||||
return response
|
||||
|
||||
# web response for images
|
||||
try:
|
||||
image = await immich_api.assets.async_view_asset(asset_id, size)
|
||||
except ImmichError as exc:
|
||||
|
@ -37,6 +37,9 @@ class MockPayloadWriter:
|
||||
async def write_headers(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Write headers."""
|
||||
|
||||
async def write(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Write payload."""
|
||||
|
||||
|
||||
_MOCK_PAYLOAD_WRITER = MockPayloadWriter()
|
||||
|
||||
|
@ -1,10 +1,19 @@
|
||||
"""Tests for the Immich integration."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util.aiohttp import MockStreamReader
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
class MockStreamReaderChunked(MockStreamReader):
|
||||
"""Mock a stream reader with simulated chunked data."""
|
||||
|
||||
async def readchunk(self) -> tuple[bytes, bool]:
|
||||
"""Read bytes."""
|
||||
return (self._content.read(), False)
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Fixture for setting up the component."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
@ -24,6 +24,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import MockStreamReaderChunked
|
||||
from .const import MOCK_ALBUM_WITH_ASSETS, MOCK_ALBUM_WITHOUT_ASSETS
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@ -69,6 +70,7 @@ def mock_immich_assets() -> AsyncMock:
|
||||
"""Mock the Immich server."""
|
||||
mock = AsyncMock(spec=ImmichAssests)
|
||||
mock.async_view_asset.return_value = b"xxxx"
|
||||
mock.async_play_video_stream.return_value = MockStreamReaderChunked(b"xxxx")
|
||||
return mock
|
||||
|
||||
|
||||
|
@ -41,5 +41,12 @@ MOCK_ALBUM_WITH_ASSETS = ImmichAlbum(
|
||||
"This is my first great album",
|
||||
"0d03a7ad-ddc7-45a6-adee-68d322a6d2f5",
|
||||
1,
|
||||
[ImmichAsset("2e94c203-50aa-4ad2-8e29-56dd74e0eff4", "filename.jpg", "image/jpeg")],
|
||||
[
|
||||
ImmichAsset(
|
||||
"2e94c203-50aa-4ad2-8e29-56dd74e0eff4", "filename.jpg", "image/jpeg"
|
||||
),
|
||||
ImmichAsset(
|
||||
"2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b", "filename.mp4", "video/mp4"
|
||||
),
|
||||
],
|
||||
)
|
||||
|
@ -25,7 +25,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.aiohttp import MockRequest
|
||||
|
||||
from . import setup_integration
|
||||
from . import MockStreamReaderChunked, setup_integration
|
||||
from .const import MOCK_ALBUM_WITHOUT_ASSETS
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@ -255,7 +255,7 @@ async def test_browse_media_get_items(
|
||||
result = await source.async_browse_media(item)
|
||||
|
||||
assert result
|
||||
assert len(result.children) == 1
|
||||
assert len(result.children) == 2
|
||||
media_file = result.children[0]
|
||||
assert isinstance(media_file, BrowseMedia)
|
||||
assert media_file.identifier == (
|
||||
@ -273,6 +273,23 @@ async def test_browse_media_get_items(
|
||||
"2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail"
|
||||
)
|
||||
|
||||
media_file = result.children[1]
|
||||
assert isinstance(media_file, BrowseMedia)
|
||||
assert media_file.identifier == (
|
||||
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/"
|
||||
"721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/"
|
||||
"2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4"
|
||||
)
|
||||
assert media_file.title == "filename.mp4"
|
||||
assert media_file.media_class == MediaClass.VIDEO
|
||||
assert media_file.media_content_type == "video/mp4"
|
||||
assert media_file.can_play is True
|
||||
assert not media_file.can_expand
|
||||
assert media_file.thumbnail == (
|
||||
"/immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/"
|
||||
"2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/thumbnail.jpg/thumbnail"
|
||||
)
|
||||
|
||||
|
||||
async def test_media_view(
|
||||
hass: HomeAssistant,
|
||||
@ -317,6 +334,22 @@ async def test_media_view(
|
||||
"2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail",
|
||||
)
|
||||
|
||||
# exception in async_play_video_stream()
|
||||
mock_immich.assets.async_play_video_stream.side_effect = ImmichError(
|
||||
{
|
||||
"message": "Not found or no asset.read access",
|
||||
"error": "Bad Request",
|
||||
"statusCode": 400,
|
||||
"correlationId": "e0hlizyl",
|
||||
}
|
||||
)
|
||||
with pytest.raises(web.HTTPNotFound):
|
||||
await view.get(
|
||||
request,
|
||||
"e7ef5713-9dab-4bd4-b899-715b0ca4379e",
|
||||
"2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4/fullsize",
|
||||
)
|
||||
|
||||
# success
|
||||
mock_immich.assets.async_view_asset.side_effect = None
|
||||
mock_immich.assets.async_view_asset.return_value = b"xxxx"
|
||||
@ -334,3 +367,15 @@ async def test_media_view(
|
||||
"2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/fullsize",
|
||||
)
|
||||
assert isinstance(result, web.Response)
|
||||
|
||||
mock_immich.assets.async_play_video_stream.side_effect = None
|
||||
mock_immich.assets.async_play_video_stream.return_value = MockStreamReaderChunked(
|
||||
b"xxxx"
|
||||
)
|
||||
with patch.object(tempfile, "tempdir", tmp_path):
|
||||
result = await view.get(
|
||||
request,
|
||||
"e7ef5713-9dab-4bd4-b899-715b0ca4379e",
|
||||
"2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4/fullsize",
|
||||
)
|
||||
assert isinstance(result, web.StreamResponse)
|
||||
|
Loading…
x
Reference in New Issue
Block a user