core/homeassistant/components/synology_dsm/media_source.py

232 lines
8.0 KiB
Python

"""Expose Synology DSM as a media source."""
from __future__ import annotations
import mimetypes
from aiohttp import web
from synology_dsm.api.photos import SynoPhotosAlbum, SynoPhotosItem
from synology_dsm.exceptions import SynologyDSMException
from homeassistant.components import http
from homeassistant.components.media_player import MediaClass
from homeassistant.components.media_source import (
BrowseError,
BrowseMediaSource,
MediaSource,
MediaSourceItem,
PlayMedia,
Unresolvable,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .models import SynologyDSMData
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
"""Set up Synology media source."""
entries = hass.config_entries.async_entries(DOMAIN)
hass.http.register_view(SynologyDsmMediaView(hass))
return SynologyPhotosMediaSource(hass, entries)
class SynologyPhotosMediaSourceIdentifier:
"""Synology Photos item identifier."""
def __init__(self, identifier: str) -> None:
"""Split identifier into parts."""
parts = identifier.split("/")
self.unique_id = None
self.album_id = None
self.cache_key = None
self.file_name = None
if parts:
self.unique_id = parts[0]
if len(parts) > 1:
self.album_id = parts[1]
if len(parts) > 2:
self.cache_key = parts[2]
if len(parts) > 3:
self.file_name = parts[3]
class SynologyPhotosMediaSource(MediaSource):
"""Provide Synology Photos as media sources."""
name = "Synology Photos"
def __init__(self, hass: HomeAssistant, entries: list[ConfigEntry]) -> None:
"""Initialize Synology source."""
super().__init__(DOMAIN)
self.hass = hass
self.entries = entries
async def async_browse_media(
self,
item: MediaSourceItem,
) -> BrowseMediaSource:
"""Return media."""
if not self.hass.data.get(DOMAIN):
raise BrowseError("Diskstation not initialized")
return BrowseMediaSource(
domain=DOMAIN,
identifier=None,
media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE,
title="Synology Photos",
can_play=False,
can_expand=True,
children_media_class=MediaClass.DIRECTORY,
children=[
*await self._async_build_diskstations(item),
],
)
async def _async_build_diskstations(
self, item: MediaSourceItem
) -> list[BrowseMediaSource]:
"""Handle browsing different diskstations."""
if not item.identifier:
ret = []
for entry in self.entries:
ret.append(
BrowseMediaSource(
domain=DOMAIN,
identifier=entry.unique_id,
media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE,
title=f"{entry.title} - {entry.unique_id}",
can_play=False,
can_expand=True,
)
)
return ret
identifier = SynologyPhotosMediaSourceIdentifier(item.identifier)
diskstation: SynologyDSMData = self.hass.data[DOMAIN][identifier.unique_id]
if identifier.album_id is None:
# Get Albums
try:
albums = await diskstation.api.photos.get_albums()
except SynologyDSMException:
return []
ret = [
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{item.identifier}/0",
media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE,
title="All images",
can_play=False,
can_expand=True,
)
]
for album in albums:
ret.append(
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{item.identifier}/{album.album_id}",
media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE,
title=album.name,
can_play=False,
can_expand=True,
)
)
return ret
# Request items of album
# Get Items
album = SynoPhotosAlbum(int(identifier.album_id), "", 0)
try:
album_items = await diskstation.api.photos.get_items_from_album(
album, 0, 1000
)
except SynologyDSMException:
return []
ret = []
for album_item in album_items:
mime_type, _ = mimetypes.guess_type(album_item.file_name)
assert isinstance(mime_type, str)
if mime_type.startswith("image/"):
# Force small small thumbnails
album_item.thumbnail_size = "sm"
ret.append(
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{identifier.unique_id}/{identifier.album_id}/{album_item.thumbnail_cache_key}/{album_item.file_name}",
media_class=MediaClass.IMAGE,
media_content_type=mime_type,
title=album_item.file_name,
can_play=True,
can_expand=False,
thumbnail=await self.async_get_thumbnail(
album_item, diskstation
),
)
)
return ret
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve media to a url."""
identifier = SynologyPhotosMediaSourceIdentifier(item.identifier)
if identifier.album_id is None:
raise Unresolvable("No album id")
if identifier.file_name is None:
raise Unresolvable("No file name")
mime_type, _ = mimetypes.guess_type(identifier.file_name)
if not isinstance(mime_type, str):
raise Unresolvable("No file extension")
return PlayMedia(
f"/synology_dsm/{identifier.unique_id}/{identifier.cache_key}/{identifier.file_name}",
mime_type,
)
async def async_get_thumbnail(
self, item: SynoPhotosItem, diskstation: SynologyDSMData
) -> str | None:
"""Get thumbnail."""
try:
thumbnail = await diskstation.api.photos.get_item_thumbnail_url(item)
except SynologyDSMException:
return None
return str(thumbnail)
class SynologyDsmMediaView(http.HomeAssistantView):
"""Synology Media Finder View."""
url = "/synology_dsm/{source_dir_id}/{location:.*}"
name = "synology_dsm"
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the media view."""
self.hass = hass
async def get(
self, request: web.Request, source_dir_id: str, location: str
) -> web.Response:
"""Start a GET request."""
if not self.hass.data.get(DOMAIN):
raise web.HTTPNotFound()
# location: {cache_key}/{filename}
cache_key, file_name = location.split("/")
image_id = cache_key.split("_")[0]
mime_type, _ = mimetypes.guess_type(file_name)
if not isinstance(mime_type, str):
raise web.HTTPNotFound()
diskstation: SynologyDSMData = self.hass.data[DOMAIN][source_dir_id]
item = SynoPhotosItem(image_id, "", "", "", cache_key, "")
try:
image = await diskstation.api.photos.download_item(item)
except SynologyDSMException as exc:
raise web.HTTPNotFound() from exc
return web.Response(body=image, content_type=mime_type)