Home Assistant Unofficial Reference 2024.12.1
media_source.py
Go to the documentation of this file.
1 """Expose Synology DSM as a media source."""
2 
3 from __future__ import annotations
4 
5 import mimetypes
6 
7 from aiohttp import web
8 from synology_dsm.api.photos import SynoPhotosAlbum, SynoPhotosItem
9 from synology_dsm.exceptions import SynologyDSMException
10 
11 from homeassistant.components import http
12 from homeassistant.components.media_player import MediaClass
14  BrowseError,
15  BrowseMediaSource,
16  MediaSource,
17  MediaSourceItem,
18  PlayMedia,
19  Unresolvable,
20 )
21 from homeassistant.config_entries import ConfigEntry
22 from homeassistant.core import HomeAssistant
23 
24 from .const import DOMAIN, SHARED_SUFFIX
25 from .models import SynologyDSMData
26 
27 
28 async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
29  """Set up Synology media source."""
30  entries = hass.config_entries.async_entries(
31  DOMAIN, include_disabled=False, include_ignore=False
32  )
33  hass.http.register_view(SynologyDsmMediaView(hass))
34  return SynologyPhotosMediaSource(hass, entries)
35 
36 
38  """Synology Photos item identifier."""
39 
40  def __init__(self, identifier: str) -> None:
41  """Split identifier into parts."""
42  parts = identifier.split("/")
43 
44  self.unique_idunique_id = None
45  self.album_idalbum_id = None
46  self.cache_keycache_key = None
47  self.file_namefile_name = None
48  self.is_sharedis_shared = False
49  self.passphrasepassphrase = ""
50 
51  self.unique_idunique_id = parts[0]
52 
53  if len(parts) > 1:
54  album_parts = parts[1].split("_")
55  self.album_idalbum_id = album_parts[0]
56  if len(album_parts) > 1:
57  self.passphrasepassphrase = parts[1].replace(f"{self.album_id}_", "")
58 
59  if len(parts) > 2:
60  self.cache_keycache_key = parts[2]
61 
62  if len(parts) > 3:
63  self.file_namefile_name = parts[3]
64  if self.file_namefile_name.endswith(SHARED_SUFFIX):
65  self.is_sharedis_shared = True
66  self.file_namefile_name = self.file_namefile_name.removesuffix(SHARED_SUFFIX)
67 
68 
70  """Provide Synology Photos as media sources."""
71 
72  name = "Synology Photos"
73 
74  def __init__(self, hass: HomeAssistant, entries: list[ConfigEntry]) -> None:
75  """Initialize Synology source."""
76  super().__init__(DOMAIN)
77  self.hasshass = hass
78  self.entriesentries = entries
79 
80  async def async_browse_media(
81  self,
82  item: MediaSourceItem,
83  ) -> BrowseMediaSource:
84  """Return media."""
85  if not self.hasshass.data.get(DOMAIN):
86  raise BrowseError("Diskstation not initialized")
87  return BrowseMediaSource(
88  domain=DOMAIN,
89  identifier=None,
90  media_class=MediaClass.DIRECTORY,
91  media_content_type=MediaClass.IMAGE,
92  title="Synology Photos",
93  can_play=False,
94  can_expand=True,
95  children_media_class=MediaClass.DIRECTORY,
96  children=[
97  *await self._async_build_diskstations_async_build_diskstations(item),
98  ],
99  )
100 
102  self, item: MediaSourceItem
103  ) -> list[BrowseMediaSource]:
104  """Handle browsing different diskstations."""
105  if not item.identifier:
106  return [
108  domain=DOMAIN,
109  identifier=entry.unique_id,
110  media_class=MediaClass.DIRECTORY,
111  media_content_type=MediaClass.IMAGE,
112  title=f"{entry.title} - {entry.unique_id}",
113  can_play=False,
114  can_expand=True,
115  )
116  for entry in self.entriesentries
117  ]
118  identifier = SynologyPhotosMediaSourceIdentifier(item.identifier)
119  diskstation: SynologyDSMData = self.hasshass.data[DOMAIN][identifier.unique_id]
120  assert diskstation.api.photos is not None
121 
122  if identifier.album_id is None:
123  # Get Albums
124  try:
125  albums = await diskstation.api.photos.get_albums()
126  except SynologyDSMException:
127  return []
128  assert albums is not None
129 
130  ret = [
132  domain=DOMAIN,
133  identifier=f"{item.identifier}/0",
134  media_class=MediaClass.DIRECTORY,
135  media_content_type=MediaClass.IMAGE,
136  title="All images",
137  can_play=False,
138  can_expand=True,
139  )
140  ]
141  ret.extend(
143  domain=DOMAIN,
144  identifier=f"{item.identifier}/{album.album_id}_{album.passphrase}",
145  media_class=MediaClass.DIRECTORY,
146  media_content_type=MediaClass.IMAGE,
147  title=album.name,
148  can_play=False,
149  can_expand=True,
150  )
151  for album in albums
152  )
153 
154  return ret
155 
156  # Request items of album
157  # Get Items
158  album = SynoPhotosAlbum(int(identifier.album_id), "", 0, identifier.passphrase)
159  try:
160  album_items = await diskstation.api.photos.get_items_from_album(
161  album, 0, 1000
162  )
163  except SynologyDSMException:
164  return []
165  assert album_items is not None
166 
167  ret = []
168  for album_item in album_items:
169  mime_type, _ = mimetypes.guess_type(album_item.file_name)
170  if isinstance(mime_type, str) and mime_type.startswith("image/"):
171  # Force small small thumbnails
172  album_item.thumbnail_size = "sm"
173  suffix = ""
174  if album_item.is_shared:
175  suffix = SHARED_SUFFIX
176  ret.append(
178  domain=DOMAIN,
179  identifier=(
180  f"{identifier.unique_id}/"
181  f"{identifier.album_id}_{identifier.passphrase}/"
182  f"{album_item.thumbnail_cache_key}/"
183  f"{album_item.file_name}{suffix}"
184  ),
185  media_class=MediaClass.IMAGE,
186  media_content_type=mime_type,
187  title=album_item.file_name,
188  can_play=True,
189  can_expand=False,
190  thumbnail=await self.async_get_thumbnailasync_get_thumbnail(
191  album_item, diskstation
192  ),
193  )
194  )
195  return ret
196 
197  async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
198  """Resolve media to a url."""
199  identifier = SynologyPhotosMediaSourceIdentifier(item.identifier)
200  if identifier.album_id is None:
201  raise Unresolvable("No album id")
202  if identifier.file_name is None:
203  raise Unresolvable("No file name")
204  mime_type, _ = mimetypes.guess_type(identifier.file_name)
205  if not isinstance(mime_type, str):
206  raise Unresolvable("No file extension")
207  suffix = ""
208  if identifier.is_shared:
209  suffix = SHARED_SUFFIX
210  return PlayMedia(
211  (
212  f"/synology_dsm/{identifier.unique_id}/"
213  f"{identifier.cache_key}/"
214  f"{identifier.file_name}{suffix}/"
215  f"{identifier.passphrase}"
216  ),
217  mime_type,
218  )
219 
221  self, item: SynoPhotosItem, diskstation: SynologyDSMData
222  ) -> str | None:
223  """Get thumbnail."""
224  assert diskstation.api.photos is not None
225 
226  try:
227  thumbnail = await diskstation.api.photos.get_item_thumbnail_url(item)
228  except SynologyDSMException:
229  return None
230  return str(thumbnail)
231 
232 
233 class SynologyDsmMediaView(http.HomeAssistantView):
234  """Synology Media Finder View."""
235 
236  url = "/synology_dsm/{source_dir_id}/{location:.*}"
237  name = "synology_dsm"
238 
239  def __init__(self, hass: HomeAssistant) -> None:
240  """Initialize the media view."""
241  self.hasshass = hass
242 
243  async def get(
244  self, request: web.Request, source_dir_id: str, location: str
245  ) -> web.Response:
246  """Start a GET request."""
247  if not self.hasshass.data.get(DOMAIN):
248  raise web.HTTPNotFound
249  # location: {cache_key}/{filename}
250  cache_key, file_name, passphrase = location.split("/")
251  image_id = int(cache_key.split("_")[0])
252 
253  if shared := file_name.endswith(SHARED_SUFFIX):
254  file_name = file_name.removesuffix(SHARED_SUFFIX)
255 
256  mime_type, _ = mimetypes.guess_type(file_name)
257  if not isinstance(mime_type, str):
258  raise web.HTTPNotFound
259 
260  diskstation: SynologyDSMData = self.hasshass.data[DOMAIN][source_dir_id]
261  assert diskstation.api.photos is not None
262  item = SynoPhotosItem(image_id, "", "", "", cache_key, "xl", shared, passphrase)
263  try:
264  if passphrase:
265  image = await diskstation.api.photos.download_item_thumbnail(item)
266  else:
267  image = await diskstation.api.photos.download_item(item)
268  except SynologyDSMException as exc:
269  raise web.HTTPNotFound from exc
270  return web.Response(body=image, content_type=mime_type)
web.Response get(self, web.Request request, str source_dir_id, str location)
list[BrowseMediaSource] _async_build_diskstations(self, MediaSourceItem item)
str|None async_get_thumbnail(self, SynoPhotosItem item, SynologyDSMData diskstation)
None __init__(self, HomeAssistant hass, list[ConfigEntry] entries)
Definition: media_source.py:74
BrowseMediaSource async_browse_media(self, MediaSourceItem item)
Definition: media_source.py:83
MediaSource async_get_media_source(HomeAssistant hass)
Definition: media_source.py:28