Home Assistant Unofficial Reference 2024.12.1
media_source.py
Go to the documentation of this file.
1 """Text-to-speech media source."""
2 
3 from __future__ import annotations
4 
5 import json
6 import mimetypes
7 from typing import TypedDict
8 
9 from yarl import URL
10 
11 from homeassistant.components.media_player import BrowseError, MediaClass
13  BrowseMediaSource,
14  MediaSource,
15  MediaSourceItem,
16  PlayMedia,
17  Unresolvable,
18  generate_media_source_id as ms_generate_media_source_id,
19 )
20 from homeassistant.core import HomeAssistant, callback
21 from homeassistant.exceptions import HomeAssistantError
22 
23 from .const import DATA_COMPONENT, DATA_TTS_MANAGER, DOMAIN
24 from .helper import get_engine_instance
25 
26 URL_QUERY_TTS_OPTIONS = "tts_options"
27 
28 
29 async def async_get_media_source(hass: HomeAssistant) -> TTSMediaSource:
30  """Set up tts media source."""
31  return TTSMediaSource(hass)
32 
33 
34 @callback
36  hass: HomeAssistant,
37  message: str,
38  engine: str | None = None,
39  language: str | None = None,
40  options: dict | None = None,
41  cache: bool | None = None,
42 ) -> str:
43  """Generate a media source ID for text-to-speech."""
44  from . import async_resolve_engine # pylint: disable=import-outside-toplevel
45 
46  if (engine := async_resolve_engine(hass, engine)) is None:
47  raise HomeAssistantError("Invalid TTS provider selected")
48 
49  engine_instance = get_engine_instance(hass, engine)
50  # We raise above if the engine is not resolved, so engine_instance can't be None
51  assert engine_instance is not None
52 
53  hass.data[DATA_TTS_MANAGER].process_options(engine_instance, language, options)
54  params = {
55  "message": message,
56  }
57  if cache is not None:
58  params["cache"] = "true" if cache else "false"
59  if language is not None:
60  params["language"] = language
61  params[URL_QUERY_TTS_OPTIONS] = json.dumps(options, separators=(",", ":"))
62 
63  return ms_generate_media_source_id(
64  DOMAIN,
65  str(URL.build(path=engine, query=params)),
66  )
67 
68 
69 class MediaSourceOptions(TypedDict):
70  """Media source options."""
71 
72  engine: str
73  message: str
74  language: str | None
75  options: dict | None
76  cache: bool | None
77 
78 
79 @callback
80 def media_source_id_to_kwargs(media_source_id: str) -> MediaSourceOptions:
81  """Turn a media source ID into options."""
82  parsed = URL(media_source_id)
83  if URL_QUERY_TTS_OPTIONS in parsed.query:
84  try:
85  options = json.loads(parsed.query[URL_QUERY_TTS_OPTIONS])
86  except json.JSONDecodeError as err:
87  raise Unresolvable(f"Invalid TTS options: {err.msg}") from err
88  else:
89  options = {
90  k: v
91  for k, v in parsed.query.items()
92  if k not in ("message", "language", "cache")
93  }
94  if "message" not in parsed.query:
95  raise Unresolvable("No message specified.")
96  kwargs: MediaSourceOptions = {
97  "engine": parsed.name,
98  "message": parsed.query["message"],
99  "language": parsed.query.get("language"),
100  "options": options,
101  "cache": None,
102  }
103  if "cache" in parsed.query:
104  kwargs["cache"] = parsed.query["cache"] == "true"
105 
106  return kwargs
107 
108 
110  """Provide text-to-speech providers as media sources."""
111 
112  name: str = "Text-to-speech"
113 
114  def __init__(self, hass: HomeAssistant) -> None:
115  """Initialize TTSMediaSource."""
116  super().__init__(DOMAIN)
117  self.hasshass = hass
118 
119  async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
120  """Resolve media to a url."""
121  try:
122  url = await self.hasshass.data[DATA_TTS_MANAGER].async_get_url_path(
123  **media_source_id_to_kwargs(item.identifier)
124  )
125  except Unresolvable:
126  raise
127  except HomeAssistantError as err:
128  raise Unresolvable(str(err)) from err
129 
130  mime_type = mimetypes.guess_type(url)[0] or "audio/mpeg"
131 
132  return PlayMedia(url, mime_type)
133 
135  self,
136  item: MediaSourceItem,
137  ) -> BrowseMediaSource:
138  """Return media."""
139  if item.identifier:
140  engine, _, params = item.identifier.partition("?")
141  return self._engine_item_engine_item(engine, params)
142 
143  # Root. List providers.
144  children = [
145  self._engine_item_engine_item(engine)
146  for engine in self.hasshass.data[DATA_TTS_MANAGER].providers
147  ] + [
148  self._engine_item_engine_item(entity.entity_id)
149  for entity in self.hasshass.data[DATA_COMPONENT].entities
150  ]
151  return BrowseMediaSource(
152  domain=DOMAIN,
153  identifier=None,
154  media_class=MediaClass.APP,
155  media_content_type="",
156  title=self.namename,
157  can_play=False,
158  can_expand=True,
159  children_media_class=MediaClass.APP,
160  children=children,
161  )
162 
163  @callback
164  def _engine_item(self, engine: str, params: str | None = None) -> BrowseMediaSource:
165  """Return provider item."""
166  from . import TextToSpeechEntity # pylint: disable=import-outside-toplevel
167 
168  if (engine_instance := get_engine_instance(self.hasshass, engine)) is None:
169  raise BrowseError("Unknown provider")
170 
171  if isinstance(engine_instance, TextToSpeechEntity):
172  engine_domain = engine_instance.platform.domain
173  else:
174  engine_domain = engine
175 
176  if params:
177  params = f"?{params}"
178  else:
179  params = ""
180 
181  return BrowseMediaSource(
182  domain=DOMAIN,
183  identifier=f"{engine}{params}",
184  media_class=MediaClass.APP,
185  media_content_type="provider",
186  title=engine_instance.name,
187  thumbnail=f"https://brands.home-assistant.io/_/{engine_domain}/logo.png",
188  can_play=False,
189  can_expand=True,
190  )
BrowseMediaSource async_browse_media(self, MediaSourceItem item)
PlayMedia async_resolve_media(self, MediaSourceItem item)
BrowseMediaSource _engine_item(self, str engine, str|None params=None)
TextToSpeechEntity|Provider|None get_engine_instance(HomeAssistant hass, str engine)
Definition: helper.py:18
MediaSourceOptions media_source_id_to_kwargs(str media_source_id)
Definition: media_source.py:80
TTSMediaSource async_get_media_source(HomeAssistant hass)
Definition: media_source.py:29
str generate_media_source_id(HomeAssistant hass, str message, str|None engine=None, str|None language=None, dict|None options=None, bool|None cache=None)
Definition: media_source.py:42
str|None async_resolve_engine(HomeAssistant hass, str|None engine)
Definition: __init__.py:156