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 from collections.abc import Callable
6 from functools import partial
7 
8 from homeassistant.components import media_source
10  BrowseError,
11  BrowseMedia,
12  MediaClass,
13  MediaType,
14 )
15 from homeassistant.core import HomeAssistant
16 from homeassistant.helpers.network import is_internal_request
17 
18 from .coordinator import RokuDataUpdateCoordinator
19 from .helpers import format_channel_name
20 
21 CONTENT_TYPE_MEDIA_CLASS = {
22  MediaType.APP: MediaClass.APP,
23  MediaType.APPS: MediaClass.APP,
24  MediaType.CHANNEL: MediaClass.CHANNEL,
25  MediaType.CHANNELS: MediaClass.CHANNEL,
26 }
27 
28 CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS = {
29  MediaType.APPS: MediaClass.DIRECTORY,
30  MediaType.CHANNELS: MediaClass.DIRECTORY,
31 }
32 
33 PLAYABLE_MEDIA_TYPES = [
34  MediaType.APP,
35  MediaType.CHANNEL,
36 ]
37 
38 EXPANDABLE_MEDIA_TYPES = [
39  MediaType.APPS,
40  MediaType.CHANNELS,
41 ]
42 
43 type GetBrowseImageUrlType = Callable[[str, str, str | None], str | None]
44 
45 
47  coordinator: RokuDataUpdateCoordinator,
48  is_internal: bool,
49  get_browse_image_url: GetBrowseImageUrlType,
50  media_content_type: str,
51  media_content_id: str,
52  media_image_id: str | None = None,
53 ) -> str | None:
54  """Get thumbnail URL."""
55  if is_internal:
56  if media_content_type == MediaType.APP and media_content_id:
57  return coordinator.roku.app_icon_url(media_content_id)
58  return None
59 
60  return get_browse_image_url(
61  media_content_type,
62  media_content_id,
63  media_image_id,
64  )
65 
66 
68  hass: HomeAssistant,
69  coordinator: RokuDataUpdateCoordinator,
70  get_browse_image_url: GetBrowseImageUrlType,
71  media_content_id: str | None,
72  media_content_type: str | None,
73 ) -> BrowseMedia:
74  """Browse media."""
75  if media_content_id is None:
76  return await root_payload(
77  hass,
78  coordinator,
79  get_browse_image_url,
80  )
81 
82  if media_source.is_media_source_id(media_content_id):
83  return await media_source.async_browse_media(hass, media_content_id)
84 
85  payload = {
86  "search_type": media_content_type,
87  "search_id": media_content_id,
88  }
89 
90  response = await hass.async_add_executor_job(
91  build_item_response,
92  coordinator,
93  payload,
94  partial(
95  get_thumbnail_url_full,
96  coordinator,
97  is_internal_request(hass),
98  get_browse_image_url,
99  ),
100  )
101 
102  if response is None:
103  raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}")
104 
105  return response
106 
107 
108 async def root_payload(
109  hass: HomeAssistant,
110  coordinator: RokuDataUpdateCoordinator,
111  get_browse_image_url: GetBrowseImageUrlType,
112 ) -> BrowseMedia:
113  """Return root payload for Roku."""
114  device = coordinator.data
115 
116  children = [
117  item_payload(
118  {"title": "Apps", "type": MediaType.APPS},
119  coordinator,
120  get_browse_image_url,
121  )
122  ]
123 
124  if device.info.device_type == "tv" and len(device.channels) > 0:
125  children.append(
126  item_payload(
127  {"title": "TV Channels", "type": MediaType.CHANNELS},
128  coordinator,
129  get_browse_image_url,
130  )
131  )
132 
133  for child in children:
134  child.thumbnail = "https://brands.home-assistant.io/_/roku/logo.png"
135 
136  try:
137  browse_item = await media_source.async_browse_media(hass, None)
138 
139  # If domain is None, it's overview of available sources
140  if browse_item.domain is None:
141  if browse_item.children is not None:
142  children.extend(browse_item.children)
143  else:
144  children.append(browse_item)
146  pass
147 
148  if len(children) == 1:
149  return await async_browse_media(
150  hass,
151  coordinator,
152  get_browse_image_url,
153  children[0].media_content_id,
154  children[0].media_content_type,
155  )
156 
157  return BrowseMedia(
158  title="Roku",
159  media_class=MediaClass.DIRECTORY,
160  media_content_id="",
161  media_content_type="root",
162  can_play=False,
163  can_expand=True,
164  children=children,
165  )
166 
167 
169  coordinator: RokuDataUpdateCoordinator,
170  payload: dict,
171  get_browse_image_url: GetBrowseImageUrlType,
172 ) -> BrowseMedia | None:
173  """Create response payload for the provided media query."""
174  search_id = payload["search_id"]
175  search_type = payload["search_type"]
176 
177  thumbnail = None
178  title = None
179  media = None
180  children_media_class = None
181 
182  if search_type == MediaType.APPS:
183  title = "Apps"
184  media = [
185  {"app_id": item.app_id, "title": item.name, "type": MediaType.APP}
186  for item in coordinator.data.apps
187  ]
188  children_media_class = MediaClass.APP
189  elif search_type == MediaType.CHANNELS:
190  title = "TV Channels"
191  media = [
192  {
193  "channel_number": channel.number,
194  "title": format_channel_name(channel.number, channel.name),
195  "type": MediaType.CHANNEL,
196  }
197  for channel in coordinator.data.channels
198  ]
199  children_media_class = MediaClass.CHANNEL
200 
201  if title is None or media is None:
202  return None
203 
204  return BrowseMedia(
205  media_class=CONTAINER_TYPES_SPECIFIC_MEDIA_CLASS.get(
206  search_type, MediaClass.DIRECTORY
207  ),
208  media_content_id=search_id,
209  media_content_type=search_type,
210  title=title,
211  can_play=search_type in PLAYABLE_MEDIA_TYPES and search_id,
212  can_expand=True,
213  children=[
214  item_payload(item, coordinator, get_browse_image_url) for item in media
215  ],
216  children_media_class=children_media_class,
217  thumbnail=thumbnail,
218  )
219 
220 
222  item: dict,
223  coordinator: RokuDataUpdateCoordinator,
224  get_browse_image_url: GetBrowseImageUrlType,
225 ) -> BrowseMedia:
226  """Create response payload for a single media item.
227 
228  Used by async_browse_media.
229  """
230  thumbnail = None
231 
232  if "app_id" in item:
233  media_content_type = MediaType.APP
234  media_content_id = item["app_id"]
235  thumbnail = get_browse_image_url(media_content_type, media_content_id, None)
236  elif "channel_number" in item:
237  media_content_type = MediaType.CHANNEL
238  media_content_id = item["channel_number"]
239  else:
240  media_content_type = item["type"]
241  media_content_id = ""
242 
243  title = item["title"]
244  can_play = media_content_type in PLAYABLE_MEDIA_TYPES and media_content_id
245  can_expand = media_content_type in EXPANDABLE_MEDIA_TYPES
246 
247  return BrowseMedia(
248  title=title,
249  media_class=CONTENT_TYPE_MEDIA_CLASS[media_content_type],
250  media_content_type=media_content_type,
251  media_content_id=media_content_id,
252  can_play=can_play,
253  can_expand=can_expand,
254  thumbnail=thumbnail,
255  )
str|None get_thumbnail_url_full(RokuDataUpdateCoordinator coordinator, bool is_internal, GetBrowseImageUrlType get_browse_image_url, str media_content_type, str media_content_id, str|None media_image_id=None)
Definition: browse_media.py:53
BrowseMedia item_payload(dict item, RokuDataUpdateCoordinator coordinator, GetBrowseImageUrlType get_browse_image_url)
BrowseMedia async_browse_media(HomeAssistant hass, RokuDataUpdateCoordinator coordinator, GetBrowseImageUrlType get_browse_image_url, str|None media_content_id, str|None media_content_type)
Definition: browse_media.py:73
BrowseMedia|None build_item_response(RokuDataUpdateCoordinator coordinator, dict payload, GetBrowseImageUrlType get_browse_image_url)
BrowseMedia root_payload(HomeAssistant hass, RokuDataUpdateCoordinator coordinator, GetBrowseImageUrlType get_browse_image_url)
str format_channel_name(str channel_number, str|None channel_name=None)
Definition: helpers.py:21
bool is_internal_request(HomeAssistant hass)
Definition: network.py:31