Home Assistant Unofficial Reference 2024.12.1
browse_media.py
Go to the documentation of this file.
1 """Browse media features for media player."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Sequence
6 from datetime import timedelta
7 import logging
8 from typing import Any
9 from urllib.parse import quote
10 
11 import yarl
12 
13 from homeassistant.components.http.auth import async_sign_path
14 from homeassistant.core import HomeAssistant, callback
15 from homeassistant.exceptions import HomeAssistantError
17  NoURLAvailableError,
18  get_supervisor_network_url,
19  get_url,
20  is_hass_url,
21 )
22 
23 from .const import CONTENT_AUTH_EXPIRY_TIME, MediaClass, MediaType
24 
25 # Paths that we don't need to sign
26 PATHS_WITHOUT_AUTH = ("/api/tts_proxy/", "/api/esphome/ffmpeg_proxy/")
27 
28 
29 @callback
31  hass: HomeAssistant,
32  media_content_id: str,
33  *,
34  allow_relative_url: bool = False,
35  for_supervisor_network: bool = False,
36 ) -> str:
37  """Update a media URL with authentication if it points at Home Assistant."""
38  parsed = yarl.URL(media_content_id)
39 
40  if parsed.scheme and parsed.scheme not in ("http", "https"):
41  return media_content_id
42 
43  if parsed.is_absolute():
44  if not is_hass_url(hass, media_content_id):
45  return media_content_id
46  elif media_content_id[0] != "/":
47  return media_content_id
48 
49  # https://github.com/pylint-dev/pylint/issues/3484
50  # pylint: disable-next=using-constant-test
51  if parsed.query:
52  logging.getLogger(__name__).debug(
53  "Not signing path for content with query param"
54  )
55  elif parsed.path.startswith(PATHS_WITHOUT_AUTH):
56  # We don't sign this path if it doesn't need auth. Although signing itself can't
57  # hurt, some devices are unable to handle long URLs and the auth signature might
58  # push it over.
59  pass
60  else:
61  signed_path = async_sign_path(
62  hass,
63  quote(parsed.path),
64  timedelta(seconds=CONTENT_AUTH_EXPIRY_TIME),
65  )
66  media_content_id = str(parsed.join(yarl.URL(signed_path)))
67 
68  # convert relative URL to absolute URL
69  if not parsed.is_absolute() and not allow_relative_url:
70  base_url = None
71  if for_supervisor_network:
72  base_url = get_supervisor_network_url(hass)
73 
74  if not base_url:
75  try:
76  base_url = get_url(hass)
77  except NoURLAvailableError as err:
78  msg = "Unable to determine Home Assistant URL to send to device"
79  if (
80  hass.config.api
81  and hass.config.api.use_ssl
82  and (not hass.config.external_url or not hass.config.internal_url)
83  ):
84  msg += ". Configure internal and external URL in general settings."
85  raise HomeAssistantError(msg) from err
86 
87  media_content_id = f"{base_url}{media_content_id}"
88 
89  return media_content_id
90 
91 
93  """Represent a browsable media file."""
94 
95  def __init__(
96  self,
97  *,
98  media_class: MediaClass | str,
99  media_content_id: str,
100  media_content_type: MediaType | str,
101  title: str,
102  can_play: bool,
103  can_expand: bool,
104  children: Sequence[BrowseMedia] | None = None,
105  children_media_class: MediaClass | str | None = None,
106  thumbnail: str | None = None,
107  not_shown: int = 0,
108  ) -> None:
109  """Initialize browse media item."""
110  self.media_classmedia_class = media_class
111  self.media_content_idmedia_content_id = media_content_id
112  self.media_content_typemedia_content_type = media_content_type
113  self.titletitle = title
114  self.can_playcan_play = can_play
115  self.can_expandcan_expand = can_expand
116  self.childrenchildren = children
117  self.children_media_classchildren_media_class = children_media_class
118  self.thumbnailthumbnail = thumbnail
119  self.not_shownnot_shown = not_shown
120 
121  def as_dict(self, *, parent: bool = True) -> dict[str, Any]:
122  """Convert Media class to browse media dictionary."""
123  if self.children_media_classchildren_media_class is None and self.childrenchildren:
124  self.calculate_children_classcalculate_children_class()
125 
126  response: dict[str, Any] = {
127  "title": self.titletitle,
128  "media_class": self.media_classmedia_class,
129  "media_content_type": self.media_content_typemedia_content_type,
130  "media_content_id": self.media_content_idmedia_content_id,
131  "children_media_class": self.children_media_classchildren_media_class,
132  "can_play": self.can_playcan_play,
133  "can_expand": self.can_expandcan_expand,
134  "thumbnail": self.thumbnailthumbnail,
135  }
136 
137  if not parent:
138  return response
139 
140  response["not_shown"] = self.not_shownnot_shown
141 
142  if self.childrenchildren:
143  response["children"] = [
144  child.as_dict(parent=False) for child in self.childrenchildren
145  ]
146  else:
147  response["children"] = []
148 
149  return response
150 
151  def calculate_children_class(self) -> None:
152  """Count the children media classes and calculate the correct class."""
153  self.children_media_classchildren_media_class = MediaClass.DIRECTORY
154  assert self.childrenchildren is not None
155  proposed_class = self.childrenchildren[0].media_class
156  if all(child.media_class == proposed_class for child in self.childrenchildren):
157  self.children_media_classchildren_media_class = proposed_class
158 
159  def __repr__(self) -> str:
160  """Return representation of browse media."""
161  return f"<BrowseMedia {self.title} ({self.media_class})>"
None __init__(self, *MediaClass|str media_class, str media_content_id, MediaType|str media_content_type, str title, bool can_play, bool can_expand, Sequence[BrowseMedia]|None children=None, MediaClass|str|None children_media_class=None, str|None thumbnail=None, int not_shown=0)
str async_sign_path(HomeAssistant hass, str path, timedelta expiration, *str|None refresh_token_id=None, bool use_content_user=False)
Definition: auth.py:52
str async_process_play_media_url(HomeAssistant hass, str media_content_id, *bool allow_relative_url=False, bool for_supervisor_network=False)
Definition: browse_media.py:36
str|None get_supervisor_network_url(HomeAssistant hass, *bool allow_ssl=False)
Definition: network.py:45
str get_url(HomeAssistant hass, *bool require_current_request=False, bool require_ssl=False, bool require_standard_port=False, bool require_cloud=False, bool allow_internal=True, bool allow_external=True, bool allow_cloud=True, bool|None allow_ip=None, bool|None prefer_external=None, bool prefer_cloud=False)
Definition: network.py:131
bool is_hass_url(HomeAssistant hass, str url)
Definition: network.py:67