232 lines
8.0 KiB
Python
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)
|