Home Assistant Unofficial Reference 2024.12.1
media_source.py
Go to the documentation of this file.
1 """Expose Radio Browser as a media source."""
2 
3 from __future__ import annotations
4 
5 import mimetypes
6 
7 import pycountry
8 from radios import FilterBy, Order, RadioBrowser, Station
9 
10 from homeassistant.components.media_player import MediaClass, MediaType
12  BrowseMediaSource,
13  MediaSource,
14  MediaSourceItem,
15  PlayMedia,
16  Unresolvable,
17 )
18 from homeassistant.core import HomeAssistant, callback
19 
20 from . import RadioBrowserConfigEntry
21 from .const import DOMAIN
22 
23 CODEC_TO_MIMETYPE = {
24  "MP3": "audio/mpeg",
25  "AAC": "audio/aac",
26  "AAC+": "audio/aac",
27  "OGG": "application/ogg",
28 }
29 
30 
31 async def async_get_media_source(hass: HomeAssistant) -> RadioMediaSource:
32  """Set up Radio Browser media source."""
33  # Radio browser supports only a single config entry
34  entry = hass.config_entries.async_entries(DOMAIN)[0]
35 
36  return RadioMediaSource(hass, entry)
37 
38 
40  """Provide Radio stations as media sources."""
41 
42  name = "Radio Browser"
43 
44  def __init__(self, hass: HomeAssistant, entry: RadioBrowserConfigEntry) -> None:
45  """Initialize RadioMediaSource."""
46  super().__init__(DOMAIN)
47  self.hasshass = hass
48  self.entryentry = entry
49 
50  @property
51  def radios(self) -> RadioBrowser:
52  """Return the radio browser."""
53  return self.entryentry.runtime_data
54 
55  async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
56  """Resolve selected Radio station to a streaming URL."""
57  radios = self.radiosradios
58 
59  station = await radios.station(uuid=item.identifier)
60  if not station:
61  raise Unresolvable("Radio station is no longer available")
62 
63  if not (mime_type := self._async_get_station_mime_type_async_get_station_mime_type(station)):
64  raise Unresolvable("Could not determine stream type of radio station")
65 
66  # Register "click" with Radio Browser
67  await radios.station_click(uuid=station.uuid)
68 
69  return PlayMedia(station.url, mime_type)
70 
71  async def async_browse_media(
72  self,
73  item: MediaSourceItem,
74  ) -> BrowseMediaSource:
75  """Return media."""
76  radios = self.radiosradios
77 
78  return BrowseMediaSource(
79  domain=DOMAIN,
80  identifier=None,
81  media_class=MediaClass.CHANNEL,
82  media_content_type=MediaType.MUSIC,
83  title=self.entryentry.title,
84  can_play=False,
85  can_expand=True,
86  children_media_class=MediaClass.DIRECTORY,
87  children=[
88  *await self._async_build_popular_async_build_popular(radios, item),
89  *await self._async_build_by_tag_async_build_by_tag(radios, item),
90  *await self._async_build_by_language_async_build_by_language(radios, item),
91  *await self._async_build_by_country_async_build_by_country(radios, item),
92  ],
93  )
94 
95  @callback
96  @staticmethod
97  def _async_get_station_mime_type(station: Station) -> str | None:
98  """Determine mime type of a radio station."""
99  mime_type = CODEC_TO_MIMETYPE.get(station.codec)
100  if not mime_type:
101  mime_type, _ = mimetypes.guess_type(station.url)
102  return mime_type
103 
104  @callback
106  self, radios: RadioBrowser, stations: list[Station]
107  ) -> list[BrowseMediaSource]:
108  """Build list of media sources from radio stations."""
109  items: list[BrowseMediaSource] = []
110 
111  for station in stations:
112  if station.codec == "UNKNOWN" or not (
113  mime_type := self._async_get_station_mime_type_async_get_station_mime_type(station)
114  ):
115  continue
116 
117  items.append(
119  domain=DOMAIN,
120  identifier=station.uuid,
121  media_class=MediaClass.MUSIC,
122  media_content_type=mime_type,
123  title=station.name,
124  can_play=True,
125  can_expand=False,
126  thumbnail=station.favicon,
127  )
128  )
129 
130  return items
131 
133  self, radios: RadioBrowser, item: MediaSourceItem
134  ) -> list[BrowseMediaSource]:
135  """Handle browsing radio stations by country."""
136  category, _, country_code = (item.identifier or "").partition("/")
137  if country_code:
138  stations = await radios.stations(
139  filter_by=FilterBy.COUNTRY_CODE_EXACT,
140  filter_term=country_code,
141  hide_broken=True,
142  order=Order.NAME,
143  reverse=False,
144  )
145  return self._async_build_stations_async_build_stations(radios, stations)
146 
147  # We show country in the root additionally, when there is no item
148  if not item.identifier or category == "country":
149  # Trigger the lazy loading of the country database to happen inside the executor
150  await self.hasshass.async_add_executor_job(lambda: len(pycountry.countries))
151  countries = await radios.countries(order=Order.NAME)
152  return [
154  domain=DOMAIN,
155  identifier=f"country/{country.code}",
156  media_class=MediaClass.DIRECTORY,
157  media_content_type=MediaType.MUSIC,
158  title=country.name,
159  can_play=False,
160  can_expand=True,
161  thumbnail=country.favicon,
162  )
163  for country in countries
164  ]
165 
166  return []
167 
169  self, radios: RadioBrowser, item: MediaSourceItem
170  ) -> list[BrowseMediaSource]:
171  """Handle browsing radio stations by language."""
172  category, _, language = (item.identifier or "").partition("/")
173  if category == "language" and language:
174  stations = await radios.stations(
175  filter_by=FilterBy.LANGUAGE_EXACT,
176  filter_term=language,
177  hide_broken=True,
178  order=Order.NAME,
179  reverse=False,
180  )
181  return self._async_build_stations_async_build_stations(radios, stations)
182 
183  if category == "language":
184  languages = await radios.languages(order=Order.NAME, hide_broken=True)
185  return [
187  domain=DOMAIN,
188  identifier=f"language/{language.code}",
189  media_class=MediaClass.DIRECTORY,
190  media_content_type=MediaType.MUSIC,
191  title=language.name,
192  can_play=False,
193  can_expand=True,
194  thumbnail=language.favicon,
195  )
196  for language in languages
197  ]
198 
199  if not item.identifier:
200  return [
202  domain=DOMAIN,
203  identifier="language",
204  media_class=MediaClass.DIRECTORY,
205  media_content_type=MediaType.MUSIC,
206  title="By Language",
207  can_play=False,
208  can_expand=True,
209  )
210  ]
211 
212  return []
213 
215  self, radios: RadioBrowser, item: MediaSourceItem
216  ) -> list[BrowseMediaSource]:
217  """Handle browsing popular radio stations."""
218  if item.identifier == "popular":
219  stations = await radios.stations(
220  hide_broken=True,
221  limit=250,
222  order=Order.CLICK_COUNT,
223  reverse=True,
224  )
225  return self._async_build_stations_async_build_stations(radios, stations)
226 
227  if not item.identifier:
228  return [
230  domain=DOMAIN,
231  identifier="popular",
232  media_class=MediaClass.DIRECTORY,
233  media_content_type=MediaType.MUSIC,
234  title="Popular",
235  can_play=False,
236  can_expand=True,
237  )
238  ]
239 
240  return []
241 
243  self, radios: RadioBrowser, item: MediaSourceItem
244  ) -> list[BrowseMediaSource]:
245  """Handle browsing radio stations by tags."""
246  category, _, tag = (item.identifier or "").partition("/")
247  if category == "tag" and tag:
248  stations = await radios.stations(
249  filter_by=FilterBy.TAG_EXACT,
250  filter_term=tag,
251  hide_broken=True,
252  order=Order.NAME,
253  reverse=False,
254  )
255  return self._async_build_stations_async_build_stations(radios, stations)
256 
257  if category == "tag":
258  tags = await radios.tags(
259  hide_broken=True,
260  limit=100,
261  order=Order.STATION_COUNT,
262  reverse=True,
263  )
264 
265  # Now we have the top tags, reorder them by name
266  tags.sort(key=lambda tag: tag.name)
267 
268  return [
270  domain=DOMAIN,
271  identifier=f"tag/{tag.name}",
272  media_class=MediaClass.DIRECTORY,
273  media_content_type=MediaType.MUSIC,
274  title=tag.name.title(),
275  can_play=False,
276  can_expand=True,
277  )
278  for tag in tags
279  ]
280 
281  if not item.identifier:
282  return [
284  domain=DOMAIN,
285  identifier="tag",
286  media_class=MediaClass.DIRECTORY,
287  media_content_type=MediaType.MUSIC,
288  title="By Category",
289  can_play=False,
290  can_expand=True,
291  )
292  ]
293 
294  return []
BrowseMediaSource async_browse_media(self, MediaSourceItem item)
Definition: media_source.py:74
list[BrowseMediaSource] _async_build_by_tag(self, RadioBrowser radios, MediaSourceItem item)
list[BrowseMediaSource] _async_build_stations(self, RadioBrowser radios, list[Station] stations)
list[BrowseMediaSource] _async_build_by_country(self, RadioBrowser radios, MediaSourceItem item)
None __init__(self, HomeAssistant hass, RadioBrowserConfigEntry entry)
Definition: media_source.py:44
list[BrowseMediaSource] _async_build_popular(self, RadioBrowser radios, MediaSourceItem item)
list[BrowseMediaSource] _async_build_by_language(self, RadioBrowser radios, MediaSourceItem item)
RadioMediaSource async_get_media_source(HomeAssistant hass)
Definition: media_source.py:31