Home Assistant Unofficial Reference 2024.12.1
media_browser.py
Go to the documentation of this file.
1 """Support to interface with the Plex API."""
2 
3 from __future__ import annotations
4 
5 from yarl import URL
6 
7 from homeassistant.components.media_player import BrowseError, BrowseMedia, MediaClass
8 
9 from .const import DOMAIN, SERVERS
10 from .errors import MediaNotFound
11 from .helpers import get_plex_data, get_plex_server, pretty_title
12 
13 
15  """Unknown media type."""
16 
17 
18 EXPANDABLES = ["album", "artist", "playlist", "season", "show"]
19 ITEM_TYPE_MEDIA_CLASS = {
20  "album": MediaClass.ALBUM,
21  "artist": MediaClass.ARTIST,
22  "clip": MediaClass.VIDEO,
23  "episode": MediaClass.EPISODE,
24  "mixed": MediaClass.DIRECTORY,
25  "movie": MediaClass.MOVIE,
26  "playlist": MediaClass.PLAYLIST,
27  "season": MediaClass.SEASON,
28  "show": MediaClass.TV_SHOW,
29  "station": MediaClass.ARTIST,
30  "track": MediaClass.TRACK,
31  "video": MediaClass.VIDEO,
32 }
33 
34 
35 def browse_media( # noqa: C901
36  hass, is_internal, media_content_type, media_content_id, *, platform=None
37 ):
38  """Implement the websocket media browsing helper."""
39  server_id = None
40  plex_server = None
41  special_folder = None
42 
43  if media_content_id:
44  url = URL(media_content_id)
45  server_id = url.host
46  plex_server = get_plex_server(hass, server_id)
47  if media_content_type == "hub":
48  _, hub_location, hub_identifier = url.parts
49  elif media_content_type in ["library", "server"] and len(url.parts) > 2:
50  _, media_content_id, special_folder = url.parts
51  else:
52  media_content_id = url.name
53 
54  if media_content_type in ("plex_root", None):
55  return root_payload(hass, is_internal, platform=platform)
56 
57  def item_payload(item, short_name=False, extra_params=None):
58  """Create response payload for a single media item."""
59  try:
60  media_class = ITEM_TYPE_MEDIA_CLASS[item.type]
61  except KeyError as err:
62  raise UnknownMediaType(f"Unknown type received: {item.type}") from err
63  payload = {
64  "title": pretty_title(item, short_name),
65  "media_class": media_class,
66  "media_content_id": generate_plex_uri(
67  server_id, item.ratingKey, params=extra_params
68  ),
69  "media_content_type": item.type,
70  "can_play": True,
71  "can_expand": item.type in EXPANDABLES,
72  }
73  if hasattr(item, "thumbUrl"):
74  plex_server.thumbnail_cache.setdefault(str(item.ratingKey), item.thumbUrl)
75  if is_internal:
76  thumbnail = item.thumbUrl
77  else:
78  thumbnail = get_proxy_image_url(
79  server_id,
80  item.ratingKey,
81  )
82  payload["thumbnail"] = thumbnail
83 
84  return BrowseMedia(**payload)
85 
86  def server_payload():
87  """Create response payload to describe libraries of the Plex server."""
88  server_info = BrowseMedia(
89  title=plex_server.friendly_name,
90  media_class=MediaClass.DIRECTORY,
91  media_content_id=generate_plex_uri(server_id, "server"),
92  media_content_type="server",
93  can_play=False,
94  can_expand=True,
95  children=[],
96  children_media_class=MediaClass.DIRECTORY,
97  thumbnail="https://brands.home-assistant.io/_/plex/logo.png",
98  )
99  if platform != "sonos":
100  server_info.children.append(
101  special_library_payload(server_info, "Recommended")
102  )
103  for library in plex_server.library.sections():
104  if library.type == "photo":
105  continue
106  if library.type != "artist" and platform == "sonos":
107  continue
108  server_info.children.append(library_section_payload(library))
109  server_info.children.append(playlists_payload())
110  return server_info
111 
112  def library_contents(library):
113  """Create response payload to describe contents of a specific library."""
114  library_info = library_section_payload(library)
115  library_info.children = [special_library_payload(library_info, "Recommended")]
116  for item in library.all():
117  try:
118  library_info.children.append(item_payload(item))
119  except UnknownMediaType:
120  continue
121  return library_info
122 
123  def playlists_payload():
124  """Create response payload for all available playlists."""
125  playlists_info = {
126  "title": "Playlists",
127  "media_class": MediaClass.DIRECTORY,
128  "media_content_id": generate_plex_uri(server_id, "all"),
129  "media_content_type": "playlists",
130  "can_play": False,
131  "can_expand": True,
132  "children": [],
133  }
134  for playlist in plex_server.playlists():
135  if (
136  playlist.type != "directory"
137  and playlist.playlistType != "audio"
138  and platform == "sonos"
139  ):
140  continue
141  try:
142  playlists_info["children"].append(item_payload(playlist))
143  except UnknownMediaType:
144  continue
145  response = BrowseMedia(**playlists_info)
146  response.children_media_class = MediaClass.PLAYLIST
147  return response
148 
149  def build_item_response(payload):
150  """Create response payload for the provided media query."""
151  try:
152  media = plex_server.lookup_media(**payload)
153  except MediaNotFound:
154  return None
155 
156  try:
157  media_info = item_payload(media)
158  except UnknownMediaType:
159  return None
160  if media_info.can_expand:
161  media_info.children = []
162  if media.TYPE == "artist" and platform != "sonos":
163  if (station := media.station()) is not None:
164  media_info.children.append(station_payload(station))
165  for item in media:
166  try:
167  media_info.children.append(item_payload(item, short_name=True))
168  except UnknownMediaType:
169  continue
170  return media_info
171 
172  if media_content_type == "hub":
173  if hub_location == "server":
174  hub = next(
175  x
176  for x in plex_server.library.hubs()
177  if x.hubIdentifier == hub_identifier
178  )
179  media_content_id = f"server/{hub.hubIdentifier}"
180  else:
181  library_section = plex_server.library.sectionByID(int(hub_location))
182  hub = next(
183  x for x in library_section.hubs() if x.hubIdentifier == hub_identifier
184  )
185  media_content_id = f"{hub.librarySectionID}/{hub.hubIdentifier}"
186  try:
187  children_media_class = ITEM_TYPE_MEDIA_CLASS[hub.type]
188  except KeyError as err:
189  raise UnknownMediaType(f"Unknown type received: {hub.type}") from err
190  payload = {
191  "title": hub.title,
192  "media_class": MediaClass.DIRECTORY,
193  "media_content_id": generate_plex_uri(server_id, media_content_id),
194  "media_content_type": "hub",
195  "can_play": False,
196  "can_expand": True,
197  "children": [],
198  "children_media_class": children_media_class,
199  }
200  for item in hub.items:
201  if hub.type == "station":
202  if platform == "sonos":
203  continue
204  payload["children"].append(station_payload(item))
205  else:
206  extra_params = None
207  hub_context = hub.context.split(".")[-1]
208  if hub_context in ("continue", "inprogress", "ondeck"):
209  extra_params = {"resume": 1}
210  payload["children"].append(
211  item_payload(item, extra_params=extra_params)
212  )
213  return BrowseMedia(**payload)
214 
215  if special_folder:
216  if media_content_type == "server":
217  library_or_section = plex_server.library
218  children_media_class = MediaClass.DIRECTORY
219  title = plex_server.friendly_name
220  elif media_content_type == "library":
221  library_or_section = plex_server.library.sectionByID(int(media_content_id))
222  title = library_or_section.title
223  try:
224  children_media_class = ITEM_TYPE_MEDIA_CLASS[library_or_section.TYPE]
225  except KeyError as err:
226  raise UnknownMediaType(
227  f"Unknown type received: {library_or_section.TYPE}"
228  ) from err
229  else:
230  raise BrowseError(
231  f"Media not found: {media_content_type} / {media_content_id}"
232  )
233 
234  payload = {
235  "title": title,
236  "media_class": MediaClass.DIRECTORY,
237  "media_content_id": generate_plex_uri(
238  server_id, f"{media_content_id}/{special_folder}"
239  ),
240  "media_content_type": media_content_type,
241  "can_play": False,
242  "can_expand": True,
243  "children": [],
244  "children_media_class": children_media_class,
245  }
246 
247  if special_folder == "Recommended":
248  for item in library_or_section.hubs():
249  if item.type == "photo":
250  continue
251  payload["children"].append(hub_payload(item))
252 
253  return BrowseMedia(**payload)
254 
255  try:
256  if media_content_type == "server":
257  return server_payload()
258 
259  if media_content_type == "library":
260  library_id = int(media_content_id)
261  library = plex_server.library.sectionByID(library_id)
262  return library_contents(library)
263 
264  except UnknownMediaType as err:
265  raise BrowseError(
266  f"Media not found: {media_content_type} / {media_content_id}"
267  ) from err
268 
269  if media_content_type == "playlists":
270  return playlists_payload()
271 
272  payload = {
273  "media_type": DOMAIN,
274  "plex_key": int(media_content_id),
275  }
276  response = build_item_response(payload)
277  if response is None:
278  raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}")
279  return response
280 
281 
282 def generate_plex_uri(server_id, media_id, params=None):
283  """Create a media_content_id URL for playable Plex media."""
284  if isinstance(media_id, int):
285  media_id = str(media_id)
286  if isinstance(media_id, str) and not media_id.startswith("/"):
287  media_id = f"/{media_id}"
288  return str(
289  URL.build(
290  scheme=DOMAIN,
291  host=server_id,
292  path=media_id,
293  query=params,
294  )
295  )
296 
297 
298 def root_payload(hass, is_internal, platform=None):
299  """Return root payload for Plex."""
300  children = [
301  browse_media(
302  hass,
303  is_internal,
304  "server",
305  generate_plex_uri(server_id, ""),
306  platform=platform,
307  )
308  for server_id in get_plex_data(hass)[SERVERS]
309  ]
310 
311  if len(children) == 1:
312  return children[0]
313 
314  return BrowseMedia(
315  title="Plex",
316  media_class=MediaClass.DIRECTORY,
317  media_content_id="",
318  media_content_type="plex_root",
319  can_play=False,
320  can_expand=True,
321  children=children,
322  )
323 
324 
326  """Create response payload for a single library section."""
327  try:
328  children_media_class = ITEM_TYPE_MEDIA_CLASS[section.TYPE]
329  except KeyError as err:
330  raise UnknownMediaType(f"Unknown type received: {section.TYPE}") from err
331  server_id = section._server.machineIdentifier # noqa: SLF001
332  return BrowseMedia(
333  title=section.title,
334  media_class=MediaClass.DIRECTORY,
335  media_content_id=generate_plex_uri(server_id, section.key),
336  media_content_type="library",
337  can_play=False,
338  can_expand=True,
339  children_media_class=children_media_class,
340  )
341 
342 
343 def special_library_payload(parent_payload, special_type):
344  """Create response payload for special library folders."""
345  title = f"{special_type} ({parent_payload.title})"
346  special_library_id = f"{parent_payload.media_content_id}/{special_type}"
347  return BrowseMedia(
348  title=title,
349  media_class=parent_payload.media_class,
350  media_content_id=special_library_id,
351  media_content_type=parent_payload.media_content_type,
352  can_play=False,
353  can_expand=True,
354  children_media_class=parent_payload.children_media_class,
355  )
356 
357 
358 def hub_payload(hub):
359  """Create response payload for a hub."""
360  if hasattr(hub, "librarySectionID"):
361  media_content_id = f"{hub.librarySectionID}/{hub.hubIdentifier}"
362  else:
363  media_content_id = f"server/{hub.hubIdentifier}"
364  server_id = hub._server.machineIdentifier # noqa: SLF001
365  payload = {
366  "title": hub.title,
367  "media_class": MediaClass.DIRECTORY,
368  "media_content_id": generate_plex_uri(server_id, media_content_id),
369  "media_content_type": "hub",
370  "can_play": False,
371  "can_expand": True,
372  }
373  return BrowseMedia(**payload)
374 
375 
376 def station_payload(station):
377  """Create response payload for a music station."""
378  server_id = station._server.machineIdentifier # noqa: SLF001
379  return BrowseMedia(
380  title=station.title,
381  media_class=ITEM_TYPE_MEDIA_CLASS[station.type],
382  media_content_id=generate_plex_uri(server_id, station.key),
383  media_content_type="station",
384  can_play=True,
385  can_expand=False,
386  )
387 
388 
390  server_id: str,
391  media_content_id: str,
392 ) -> str:
393  """Generate an url for a Plex media browser image."""
394  return f"/api/plex_image_proxy/{server_id}/{media_content_id}"
PlexData get_plex_data(HomeAssistant hass)
Definition: helpers.py:29
def pretty_title(media, short_name=False)
Definition: helpers.py:39
PlexServer get_plex_server(HomeAssistant hass, str server_id)
Definition: helpers.py:34
def special_library_payload(parent_payload, special_type)
def browse_media(hass, is_internal, media_content_type, media_content_id, *platform=None)
def root_payload(hass, is_internal, platform=None)
def generate_plex_uri(server_id, media_id, params=None)
str get_proxy_image_url(str server_id, str media_content_id)
BrowseMedia|None build_item_response(MusicLibrary media_library, dict[str, str] payload, get_thumbnail_url=None)
BrowseMedia item_payload(DidlObject item, get_thumbnail_url=None)