Home Assistant Unofficial Reference 2024.12.1
browse_media.py
Go to the documentation of this file.
1 """Support for media browsing."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from typing import Any
7 
8 from jellyfin_apiclient_python import JellyfinClient
9 
11  BrowseError,
12  BrowseMedia,
13  MediaClass,
14  MediaType,
15 )
16 from homeassistant.core import HomeAssistant
17 
18 from .client_wrapper import get_artwork_url
19 from .const import (
20  CONTENT_TYPE_MAP,
21  MEDIA_CLASS_MAP,
22  MEDIA_TYPE_NONE,
23  SUPPORTED_COLLECTION_TYPES,
24 )
25 
26 CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS: dict[str, str] = {
27  MediaType.MUSIC: MediaClass.MUSIC,
28  MediaType.SEASON: MediaClass.SEASON,
29  MediaType.TVSHOW: MediaClass.TV_SHOW,
30  "boxset": MediaClass.DIRECTORY,
31  "collection": MediaClass.DIRECTORY,
32  "library": MediaClass.DIRECTORY,
33 }
34 
35 PLAYABLE_MEDIA_TYPES = [
36  MediaType.EPISODE,
37  MediaType.MOVIE,
38  MediaType.MUSIC,
39 ]
40 
41 
42 async def item_payload(
43  hass: HomeAssistant,
44  client: JellyfinClient,
45  user_id: str,
46  item: dict[str, Any],
47 ) -> BrowseMedia:
48  """Create response payload for a single media item."""
49  title = item["Name"]
50  thumbnail = get_artwork_url(client, item, 600)
51 
52  media_content_id = item["Id"]
53  media_content_type = CONTENT_TYPE_MAP.get(item["Type"], MEDIA_TYPE_NONE)
54 
55  return BrowseMedia(
56  title=title,
57  media_content_id=media_content_id,
58  media_content_type=media_content_type,
59  media_class=MEDIA_CLASS_MAP.get(item["Type"], MediaClass.DIRECTORY),
60  can_play=bool(media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id),
61  can_expand=bool(item.get("IsFolder")),
62  children_media_class=None,
63  thumbnail=thumbnail,
64  )
65 
66 
68  hass: HomeAssistant, client: JellyfinClient, user_id: str
69 ) -> BrowseMedia:
70  """Create response payload for root folder."""
71  folders = await hass.async_add_executor_job(client.jellyfin.get_media_folders)
72 
73  children = [
74  await item_payload(hass, client, user_id, folder)
75  for folder in folders["Items"]
76  if folder["CollectionType"] in SUPPORTED_COLLECTION_TYPES
77  ]
78 
79  return BrowseMedia(
80  media_content_id="",
81  media_content_type="root",
82  media_class=MediaClass.DIRECTORY,
83  children_media_class=MediaClass.DIRECTORY,
84  title="Jellyfin",
85  can_play=False,
86  can_expand=True,
87  children=children,
88  )
89 
90 
92  hass: HomeAssistant,
93  client: JellyfinClient,
94  user_id: str,
95  media_content_type: str | None,
96  media_content_id: str,
97 ) -> BrowseMedia:
98  """Create response payload for the provided media query."""
99  title, media, thumbnail = await get_media_info(
100  hass, client, user_id, media_content_type, media_content_id
101  )
102 
103  if title is None or media is None:
104  raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}")
105 
106  children = await asyncio.gather(
107  *(item_payload(hass, client, user_id, media_item) for media_item in media)
108  )
109 
110  response = BrowseMedia(
111  media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get(
112  str(media_content_type), MediaClass.DIRECTORY
113  ),
114  media_content_id=media_content_id,
115  media_content_type=str(media_content_type),
116  title=title,
117  can_play=bool(media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id),
118  can_expand=True,
119  children=children,
120  thumbnail=thumbnail,
121  )
122 
123  response.calculate_children_class()
124 
125  return response
126 
127 
128 def fetch_item(client: JellyfinClient, item_id: str) -> dict[str, Any] | None:
129  """Fetch item from Jellyfin server."""
130  result = client.jellyfin.get_item(item_id)
131 
132  if not result:
133  return None
134 
135  item: dict[str, Any] = result
136  return item
137 
138 
140  client: JellyfinClient,
141  params: dict[str, Any],
142 ) -> list[dict[str, Any]] | None:
143  """Fetch items from Jellyfin server."""
144  result = client.jellyfin.user_items(params=params)
145 
146  if not result or "Items" not in result or len(result["Items"]) < 1:
147  return None
148 
149  items: list[dict[str, Any]] = result["Items"]
150 
151  return [
152  item
153  for item in items
154  if not item.get("IsFolder")
155  or (item.get("IsFolder") and item.get("ChildCount", 1) > 0)
156  ]
157 
158 
159 async def get_media_info(
160  hass: HomeAssistant,
161  client: JellyfinClient,
162  user_id: str,
163  media_content_type: str | None,
164  media_content_id: str,
165 ) -> tuple[str | None, list[dict[str, Any]] | None, str | None]:
166  """Fetch media info."""
167  thumbnail: str | None = None
168  title: str | None = None
169  media: list[dict[str, Any]] | None = None
170 
171  item = await hass.async_add_executor_job(fetch_item, client, media_content_id)
172 
173  if item is None:
174  return None, None, None
175 
176  title = item["Name"]
177  thumbnail = get_artwork_url(client, item)
178 
179  if item.get("IsFolder"):
180  media = await hass.async_add_executor_job(
181  fetch_items, client, {"parentId": media_content_id, "fields": "childCount"}
182  )
183 
184  if not media or len(media) == 0:
185  media = None
186 
187  return title, media, thumbnail
BrowseMedia build_root_response(HomeAssistant hass, JellyfinClient client, str user_id)
Definition: browse_media.py:69
dict[str, Any]|None fetch_item(JellyfinClient client, str item_id)
list[dict[str, Any]]|None fetch_items(JellyfinClient client, dict[str, Any] params)
tuple[str|None, list[dict[str, Any]]|None, str|None] get_media_info(HomeAssistant hass, JellyfinClient client, str user_id, str|None media_content_type, str media_content_id)
BrowseMedia build_item_response(HomeAssistant hass, JellyfinClient client, str user_id, str|None media_content_type, str media_content_id)
Definition: browse_media.py:97
BrowseMedia item_payload(HomeAssistant hass, JellyfinClient client, str user_id, dict[str, Any] item)
Definition: browse_media.py:47
str|None get_artwork_url(JellyfinClient client, dict[str, Any] item, int max_width=600)