Use mime type provided by Immich (#145830)

use mime type from immich instead of guessing it
This commit is contained in:
Michael 2025-05-29 10:28:02 +02:00 committed by GitHub
parent cad6c72cfa
commit 80189495c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 78 additions and 59 deletions

View File

@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
from logging import getLogger from logging import getLogger
import mimetypes
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse
from aioimmich.exceptions import ImmichError from aioimmich.exceptions import ImmichError
@ -39,13 +38,14 @@ class ImmichMediaSourceIdentifier:
def __init__(self, identifier: str) -> None: def __init__(self, identifier: str) -> None:
"""Split identifier into parts.""" """Split identifier into parts."""
parts = identifier.split("/") parts = identifier.split("|")
# config_entry.unique_id/collection/collection_id/asset_id/file_name # config_entry.unique_id|collection|collection_id|asset_id|file_name|mime_type
self.unique_id = parts[0] self.unique_id = parts[0]
self.collection = parts[1] if len(parts) > 1 else None self.collection = parts[1] if len(parts) > 1 else None
self.collection_id = parts[2] if len(parts) > 2 else None self.collection_id = parts[2] if len(parts) > 2 else None
self.asset_id = parts[3] if len(parts) > 3 else None self.asset_id = parts[3] if len(parts) > 3 else None
self.file_name = parts[4] if len(parts) > 3 else None self.file_name = parts[4] if len(parts) > 3 else None
self.mime_type = parts[5] if len(parts) > 3 else None
class ImmichMediaSource(MediaSource): class ImmichMediaSource(MediaSource):
@ -111,7 +111,7 @@ class ImmichMediaSource(MediaSource):
return [ return [
BrowseMediaSource( BrowseMediaSource(
domain=DOMAIN, domain=DOMAIN,
identifier=f"{identifier.unique_id}/albums", identifier=f"{identifier.unique_id}|albums",
media_class=MediaClass.DIRECTORY, media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE, media_content_type=MediaClass.IMAGE,
title="albums", title="albums",
@ -130,13 +130,13 @@ class ImmichMediaSource(MediaSource):
return [ return [
BrowseMediaSource( BrowseMediaSource(
domain=DOMAIN, domain=DOMAIN,
identifier=f"{identifier.unique_id}/albums/{album.album_id}", identifier=f"{identifier.unique_id}|albums|{album.album_id}",
media_class=MediaClass.DIRECTORY, media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE, media_content_type=MediaClass.IMAGE,
title=album.name, title=album.name,
can_play=False, can_play=False,
can_expand=True, can_expand=True,
thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumb.jpg/thumbnail", thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumbnail/image/jpg",
) )
for album in albums for album in albums
] ]
@ -157,17 +157,18 @@ class ImmichMediaSource(MediaSource):
BrowseMediaSource( BrowseMediaSource(
domain=DOMAIN, domain=DOMAIN,
identifier=( identifier=(
f"{identifier.unique_id}/albums/" f"{identifier.unique_id}|albums|"
f"{identifier.collection_id}/" f"{identifier.collection_id}|"
f"{asset.asset_id}/" f"{asset.asset_id}|"
f"{asset.file_name}" f"{asset.file_name}|"
f"{asset.mime_type}"
), ),
media_class=MediaClass.IMAGE, media_class=MediaClass.IMAGE,
media_content_type=asset.mime_type, media_content_type=asset.mime_type,
title=asset.file_name, title=asset.file_name,
can_play=False, can_play=False,
can_expand=False, can_expand=False,
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/{asset.file_name}/thumbnail", thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{asset.mime_type}",
) )
for asset in album_info.assets for asset in album_info.assets
if asset.mime_type.startswith("image/") if asset.mime_type.startswith("image/")
@ -177,17 +178,18 @@ class ImmichMediaSource(MediaSource):
BrowseMediaSource( BrowseMediaSource(
domain=DOMAIN, domain=DOMAIN,
identifier=( identifier=(
f"{identifier.unique_id}/albums/" f"{identifier.unique_id}|albums|"
f"{identifier.collection_id}/" f"{identifier.collection_id}|"
f"{asset.asset_id}/" f"{asset.asset_id}|"
f"{asset.file_name}" f"{asset.file_name}|"
f"{asset.mime_type}"
), ),
media_class=MediaClass.VIDEO, media_class=MediaClass.VIDEO,
media_content_type=asset.mime_type, media_content_type=asset.mime_type,
title=asset.file_name, title=asset.file_name,
can_play=True, can_play=True,
can_expand=False, can_expand=False,
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail.jpg/thumbnail", thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/image/jpeg",
) )
for asset in album_info.assets for asset in album_info.assets
if asset.mime_type.startswith("video/") if asset.mime_type.startswith("video/")
@ -197,17 +199,23 @@ class ImmichMediaSource(MediaSource):
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve media to a url.""" """Resolve media to a url."""
identifier = ImmichMediaSourceIdentifier(item.identifier) try:
if identifier.file_name is None: identifier = ImmichMediaSourceIdentifier(item.identifier)
raise Unresolvable("No file name") except IndexError as err:
mime_type, _ = mimetypes.guess_type(identifier.file_name) raise Unresolvable(
if not isinstance(mime_type, str): f"Could not parse identifier: {item.identifier}"
raise Unresolvable("No file extension") ) from err
if identifier.mime_type is None:
raise Unresolvable(
f"Could not resolve identifier that has no mime-type: {item.identifier}"
)
return PlayMedia( return PlayMedia(
( (
f"/immich/{identifier.unique_id}/{identifier.asset_id}/{identifier.file_name}/fullsize" f"/immich/{identifier.unique_id}/{identifier.asset_id}/fullsize/{identifier.mime_type}"
), ),
mime_type, identifier.mime_type,
) )
@ -228,10 +236,10 @@ class ImmichMediaView(HomeAssistantView):
if not self.hass.config_entries.async_loaded_entries(DOMAIN): if not self.hass.config_entries.async_loaded_entries(DOMAIN):
raise HTTPNotFound raise HTTPNotFound
asset_id, file_name, size = location.split("/") try:
mime_type, _ = mimetypes.guess_type(file_name) asset_id, size, mime_type_base, mime_type_format = location.split("/")
if not isinstance(mime_type, str): except ValueError as err:
raise HTTPNotFound raise HTTPNotFound from err
entry: ImmichConfigEntry | None = ( entry: ImmichConfigEntry | None = (
self.hass.config_entries.async_entry_for_domain_unique_id( self.hass.config_entries.async_entry_for_domain_unique_id(
@ -242,7 +250,7 @@ class ImmichMediaView(HomeAssistantView):
immich_api = entry.runtime_data.api immich_api = entry.runtime_data.api
# stream response for videos # stream response for videos
if mime_type.startswith("video/"): if mime_type_base == "video":
try: try:
resp = await immich_api.assets.async_play_video_stream(asset_id) resp = await immich_api.assets.async_play_video_stream(asset_id)
except ImmichError as exc: except ImmichError as exc:
@ -259,4 +267,4 @@ class ImmichMediaView(HomeAssistantView):
image = await immich_api.assets.async_view_asset(asset_id, size) image = await immich_api.assets.async_view_asset(asset_id, size)
except ImmichError as exc: except ImmichError as exc:
raise HTTPNotFound from exc raise HTTPNotFound from exc
return Response(body=image, content_type=mime_type) return Response(body=image, content_type=f"{mime_type_base}/{mime_type_format}")

View File

@ -43,9 +43,15 @@ async def test_get_media_source(hass: HomeAssistant) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
("identifier", "exception_msg"), ("identifier", "exception_msg"),
[ [
("unique_id", "No file name"), ("unique_id", "Could not resolve identifier that has no mime-type"),
("unique_id/albums/album_id", "No file name"), (
("unique_id/albums/album_id/asset_id/filename", "No file extension"), "unique_id|albums|album_id",
"Could not resolve identifier that has no mime-type",
),
(
"unique_id|albums|album_id|asset_id|filename",
"Could not parse identifier",
),
], ],
) )
async def test_resolve_media_bad_identifier( async def test_resolve_media_bad_identifier(
@ -64,15 +70,20 @@ async def test_resolve_media_bad_identifier(
("identifier", "url", "mime_type"), ("identifier", "url", "mime_type"),
[ [
( (
"unique_id/albums/album_id/asset_id/filename.jpg", "unique_id|albums|album_id|asset_id|filename.jpg|image/jpeg",
"/immich/unique_id/asset_id/filename.jpg/fullsize", "/immich/unique_id/asset_id/fullsize/image/jpeg",
"image/jpeg", "image/jpeg",
), ),
( (
"unique_id/albums/album_id/asset_id/filename.png", "unique_id|albums|album_id|asset_id|filename.png|image/png",
"/immich/unique_id/asset_id/filename.png/fullsize", "/immich/unique_id/asset_id/fullsize/image/png",
"image/png", "image/png",
), ),
(
"unique_id|albums|album_id|asset_id|filename.mp4|video/mp4",
"/immich/unique_id/asset_id/fullsize/video/mp4",
"video/mp4",
),
], ],
) )
async def test_resolve_media_success( async def test_resolve_media_success(
@ -137,7 +148,7 @@ async def test_browse_media_get_root(
assert isinstance(media_file, BrowseMedia) assert isinstance(media_file, BrowseMedia)
assert media_file.title == "albums" assert media_file.title == "albums"
assert media_file.media_content_id == ( assert media_file.media_content_id == (
"media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums" "media-source://immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums"
) )
@ -154,7 +165,7 @@ async def test_browse_media_get_albums(
source = await async_get_media_source(hass) source = await async_get_media_source(hass)
item = MediaSourceItem( item = MediaSourceItem(
hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums", None hass, DOMAIN, "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums", None
) )
result = await source.async_browse_media(item) result = await source.async_browse_media(item)
@ -165,7 +176,7 @@ async def test_browse_media_get_albums(
assert media_file.title == "My Album" assert media_file.title == "My Album"
assert media_file.media_content_id == ( assert media_file.media_content_id == (
"media-source://immich/" "media-source://immich/"
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/" "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|"
"721e1a4b-aa12-441e-8d3b-5ac7ab283bb6" "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6"
) )
@ -193,7 +204,7 @@ async def test_browse_media_get_albums_error(
source = await async_get_media_source(hass) source = await async_get_media_source(hass)
item = MediaSourceItem(hass, DOMAIN, f"{mock_config_entry.unique_id}/albums", None) item = MediaSourceItem(hass, DOMAIN, f"{mock_config_entry.unique_id}|albums", None)
result = await source.async_browse_media(item) result = await source.async_browse_media(item)
assert result assert result
@ -219,7 +230,7 @@ async def test_browse_media_get_album_items_error(
item = MediaSourceItem( item = MediaSourceItem(
hass, hass,
DOMAIN, DOMAIN,
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
None, None,
) )
result = await source.async_browse_media(item) result = await source.async_browse_media(item)
@ -240,7 +251,7 @@ async def test_browse_media_get_album_items_error(
item = MediaSourceItem( item = MediaSourceItem(
hass, hass,
DOMAIN, DOMAIN,
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
None, None,
) )
result = await source.async_browse_media(item) result = await source.async_browse_media(item)
@ -266,7 +277,7 @@ async def test_browse_media_get_album_items(
item = MediaSourceItem( item = MediaSourceItem(
hass, hass,
DOMAIN, DOMAIN,
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/721e1a4b-aa12-441e-8d3b-5ac7ab283bb6", "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
None, None,
) )
result = await source.async_browse_media(item) result = await source.async_browse_media(item)
@ -276,9 +287,9 @@ async def test_browse_media_get_album_items(
media_file = result.children[0] media_file = result.children[0]
assert isinstance(media_file, BrowseMedia) assert isinstance(media_file, BrowseMedia)
assert media_file.identifier == ( assert media_file.identifier == (
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/" "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|"
"721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/" "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6|"
"2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg" "2e94c203-50aa-4ad2-8e29-56dd74e0eff4|filename.jpg|image/jpeg"
) )
assert media_file.title == "filename.jpg" assert media_file.title == "filename.jpg"
assert media_file.media_class == MediaClass.IMAGE assert media_file.media_class == MediaClass.IMAGE
@ -287,15 +298,15 @@ async def test_browse_media_get_album_items(
assert not media_file.can_expand assert not media_file.can_expand
assert media_file.thumbnail == ( assert media_file.thumbnail == (
"/immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/" "/immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/"
"2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail" "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/thumbnail/image/jpeg"
) )
media_file = result.children[1] media_file = result.children[1]
assert isinstance(media_file, BrowseMedia) assert isinstance(media_file, BrowseMedia)
assert media_file.identifier == ( assert media_file.identifier == (
"e7ef5713-9dab-4bd4-b899-715b0ca4379e/albums/" "e7ef5713-9dab-4bd4-b899-715b0ca4379e|albums|"
"721e1a4b-aa12-441e-8d3b-5ac7ab283bb6/" "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6|"
"2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4" "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b|filename.mp4|video/mp4"
) )
assert media_file.title == "filename.mp4" assert media_file.title == "filename.mp4"
assert media_file.media_class == MediaClass.VIDEO assert media_file.media_class == MediaClass.VIDEO
@ -304,7 +315,7 @@ async def test_browse_media_get_album_items(
assert not media_file.can_expand assert not media_file.can_expand
assert media_file.thumbnail == ( assert media_file.thumbnail == (
"/immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/" "/immich/e7ef5713-9dab-4bd4-b899-715b0ca4379e/"
"2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/thumbnail.jpg/thumbnail" "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/thumbnail/image/jpeg"
) )
@ -327,12 +338,12 @@ async def test_media_view(
with patch("homeassistant.components.immich.PLATFORMS", []): with patch("homeassistant.components.immich.PLATFORMS", []):
await setup_integration(hass, mock_config_entry) await setup_integration(hass, mock_config_entry)
# wrong url (without file extension) # wrong url (without mime type)
with pytest.raises(web.HTTPNotFound): with pytest.raises(web.HTTPNotFound):
await view.get( await view.get(
request, request,
"e7ef5713-9dab-4bd4-b899-715b0ca4379e", "e7ef5713-9dab-4bd4-b899-715b0ca4379e",
"2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename/thumbnail", "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/thumbnail",
) )
# exception in async_view_asset() # exception in async_view_asset()
@ -348,7 +359,7 @@ async def test_media_view(
await view.get( await view.get(
request, request,
"e7ef5713-9dab-4bd4-b899-715b0ca4379e", "e7ef5713-9dab-4bd4-b899-715b0ca4379e",
"2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail", "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/thumbnail/image/jpeg",
) )
# exception in async_play_video_stream() # exception in async_play_video_stream()
@ -364,7 +375,7 @@ async def test_media_view(
await view.get( await view.get(
request, request,
"e7ef5713-9dab-4bd4-b899-715b0ca4379e", "e7ef5713-9dab-4bd4-b899-715b0ca4379e",
"2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4/fullsize", "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/fullsize/video/mp4",
) )
# success # success
@ -374,14 +385,14 @@ async def test_media_view(
result = await view.get( result = await view.get(
request, request,
"e7ef5713-9dab-4bd4-b899-715b0ca4379e", "e7ef5713-9dab-4bd4-b899-715b0ca4379e",
"2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/thumbnail", "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/thumbnail/image/jpeg",
) )
assert isinstance(result, web.Response) assert isinstance(result, web.Response)
with patch.object(tempfile, "tempdir", tmp_path): with patch.object(tempfile, "tempdir", tmp_path):
result = await view.get( result = await view.get(
request, request,
"e7ef5713-9dab-4bd4-b899-715b0ca4379e", "e7ef5713-9dab-4bd4-b899-715b0ca4379e",
"2e94c203-50aa-4ad2-8e29-56dd74e0eff4/filename.jpg/fullsize", "2e94c203-50aa-4ad2-8e29-56dd74e0eff4/fullsize/image/jpeg",
) )
assert isinstance(result, web.Response) assert isinstance(result, web.Response)
@ -393,6 +404,6 @@ async def test_media_view(
result = await view.get( result = await view.get(
request, request,
"e7ef5713-9dab-4bd4-b899-715b0ca4379e", "e7ef5713-9dab-4bd4-b899-715b0ca4379e",
"2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/filename.mp4/fullsize", "2e65a5f2-db83-44c4-81ab-f5ff20c9bd7b/fullsize/video/mp4",
) )
assert isinstance(result, web.StreamResponse) assert isinstance(result, web.StreamResponse)