Home Assistant Unofficial Reference 2024.12.1
media_browser.py
Go to the documentation of this file.
1 """Media Source Implementation."""
2 
3 from __future__ import annotations
4 
5 from typing import TYPE_CHECKING, Any
6 
7 from music_assistant_models.media_items import MediaItemType
8 
9 from homeassistant.components import media_source
11  BrowseError,
12  BrowseMedia,
13  MediaClass,
14  MediaType,
15 )
16 from homeassistant.core import HomeAssistant
17 
18 from .const import DEFAULT_NAME, DOMAIN
19 
20 if TYPE_CHECKING:
21  from music_assistant_client import MusicAssistantClient
22 
23 MEDIA_TYPE_RADIO = "radio"
24 
25 PLAYABLE_MEDIA_TYPES = [
26  MediaType.PLAYLIST,
27  MediaType.ALBUM,
28  MediaType.ARTIST,
29  MEDIA_TYPE_RADIO,
30  MediaType.TRACK,
31 ]
32 
33 LIBRARY_ARTISTS = "artists"
34 LIBRARY_ALBUMS = "albums"
35 LIBRARY_TRACKS = "tracks"
36 LIBRARY_PLAYLISTS = "playlists"
37 LIBRARY_RADIO = "radio"
38 
39 
40 LIBRARY_TITLE_MAP = {
41  LIBRARY_ARTISTS: "Artists",
42  LIBRARY_ALBUMS: "Albums",
43  LIBRARY_TRACKS: "Tracks",
44  LIBRARY_PLAYLISTS: "Playlists",
45  LIBRARY_RADIO: "Radio stations",
46 }
47 
48 LIBRARY_MEDIA_CLASS_MAP = {
49  LIBRARY_ARTISTS: MediaClass.ARTIST,
50  LIBRARY_ALBUMS: MediaClass.ALBUM,
51  LIBRARY_TRACKS: MediaClass.TRACK,
52  LIBRARY_PLAYLISTS: MediaClass.PLAYLIST,
53  LIBRARY_RADIO: MediaClass.MUSIC, # radio is not accepted by HA
54 }
55 
56 MEDIA_CONTENT_TYPE_FLAC = "audio/flac"
57 THUMB_SIZE = 200
58 
59 
60 def media_source_filter(item: BrowseMedia) -> bool:
61  """Filter media sources."""
62  return item.media_content_type.startswith("audio/")
63 
64 
66  hass: HomeAssistant,
67  mass: MusicAssistantClient,
68  media_content_id: str | None,
69  media_content_type: str | None,
70 ) -> BrowseMedia:
71  """Browse media."""
72  if media_content_id is None:
73  return await build_main_listing(hass)
74 
75  assert media_content_type is not None
76 
77  if media_source.is_media_source_id(media_content_id):
78  return await media_source.async_browse_media(
79  hass, media_content_id, content_filter=media_source_filter
80  )
81 
82  if media_content_id == LIBRARY_ARTISTS:
83  return await build_artists_listing(mass)
84  if media_content_id == LIBRARY_ALBUMS:
85  return await build_albums_listing(mass)
86  if media_content_id == LIBRARY_TRACKS:
87  return await build_tracks_listing(mass)
88  if media_content_id == LIBRARY_PLAYLISTS:
89  return await build_playlists_listing(mass)
90  if media_content_id == LIBRARY_RADIO:
91  return await build_radio_listing(mass)
92  if "artist" in media_content_id:
93  return await build_artist_items_listing(mass, media_content_id)
94  if "album" in media_content_id:
95  return await build_album_items_listing(mass, media_content_id)
96  if "playlist" in media_content_id:
97  return await build_playlist_items_listing(mass, media_content_id)
98 
99  raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}")
100 
101 
102 async def build_main_listing(hass: HomeAssistant) -> BrowseMedia:
103  """Build main browse listing."""
104  children: list[BrowseMedia] = []
105  for library, media_class in LIBRARY_MEDIA_CLASS_MAP.items():
106  child_source = BrowseMedia(
107  media_class=MediaClass.DIRECTORY,
108  media_content_id=library,
109  media_content_type=DOMAIN,
110  title=LIBRARY_TITLE_MAP[library],
111  children_media_class=media_class,
112  can_play=False,
113  can_expand=True,
114  )
115  children.append(child_source)
116 
117  try:
118  item = await media_source.async_browse_media(
119  hass, None, content_filter=media_source_filter
120  )
121  # If domain is None, it's overview of available sources
122  if item.domain is None and item.children is not None:
123  children.extend(item.children)
124  else:
125  children.append(item)
127  pass
128 
129  return BrowseMedia(
130  media_class=MediaClass.DIRECTORY,
131  media_content_id="",
132  media_content_type=DOMAIN,
133  title=DEFAULT_NAME,
134  can_play=False,
135  can_expand=True,
136  children=children,
137  )
138 
139 
140 async def build_playlists_listing(mass: MusicAssistantClient) -> BrowseMedia:
141  """Build Playlists browse listing."""
142  media_class = LIBRARY_MEDIA_CLASS_MAP[LIBRARY_PLAYLISTS]
143  return BrowseMedia(
144  media_class=MediaClass.DIRECTORY,
145  media_content_id=LIBRARY_PLAYLISTS,
146  media_content_type=MediaType.PLAYLIST,
147  title=LIBRARY_TITLE_MAP[LIBRARY_PLAYLISTS],
148  can_play=False,
149  can_expand=True,
150  children_media_class=media_class,
151  children=sorted(
152  [
153  build_item(mass, item, can_expand=True)
154  # we only grab the first page here because the
155  # HA media browser does not support paging
156  for item in await mass.music.get_library_playlists(limit=500)
157  if item.available
158  ],
159  key=lambda x: x.title,
160  ),
161  )
162 
163 
165  mass: MusicAssistantClient, identifier: str
166 ) -> BrowseMedia:
167  """Build Playlist items browse listing."""
168  playlist = await mass.music.get_item_by_uri(identifier)
169 
170  return BrowseMedia(
171  media_class=MediaClass.PLAYLIST,
172  media_content_id=playlist.uri,
173  media_content_type=MediaType.PLAYLIST,
174  title=playlist.name,
175  can_play=True,
176  can_expand=True,
177  children_media_class=MediaClass.TRACK,
178  children=[
179  build_item(mass, item, can_expand=False)
180  # we only grab the first page here because the
181  # HA media browser does not support paging
182  for item in await mass.music.get_playlist_tracks(
183  playlist.item_id, playlist.provider
184  )
185  if item.available
186  ],
187  )
188 
189 
190 async def build_artists_listing(mass: MusicAssistantClient) -> BrowseMedia:
191  """Build Albums browse listing."""
192  media_class = LIBRARY_MEDIA_CLASS_MAP[LIBRARY_ARTISTS]
193 
194  return BrowseMedia(
195  media_class=MediaClass.DIRECTORY,
196  media_content_id=LIBRARY_ARTISTS,
197  media_content_type=MediaType.ARTIST,
198  title=LIBRARY_TITLE_MAP[LIBRARY_ARTISTS],
199  can_play=False,
200  can_expand=True,
201  children_media_class=media_class,
202  children=sorted(
203  [
204  build_item(mass, artist, can_expand=True)
205  # we only grab the first page here because the
206  # HA media browser does not support paging
207  for artist in await mass.music.get_library_artists(limit=500)
208  if artist.available
209  ],
210  key=lambda x: x.title,
211  ),
212  )
213 
214 
216  mass: MusicAssistantClient, identifier: str
217 ) -> BrowseMedia:
218  """Build Artist items browse listing."""
219  artist = await mass.music.get_item_by_uri(identifier)
220  albums = await mass.music.get_artist_albums(artist.item_id, artist.provider)
221 
222  return BrowseMedia(
223  media_class=MediaType.ARTIST,
224  media_content_id=artist.uri,
225  media_content_type=MediaType.ARTIST,
226  title=artist.name,
227  can_play=True,
228  can_expand=True,
229  children_media_class=MediaClass.ALBUM,
230  children=[
231  build_item(mass, album, can_expand=True)
232  for album in albums
233  if album.available
234  ],
235  )
236 
237 
238 async def build_albums_listing(mass: MusicAssistantClient) -> BrowseMedia:
239  """Build Albums browse listing."""
240  media_class = LIBRARY_MEDIA_CLASS_MAP[LIBRARY_ALBUMS]
241 
242  return BrowseMedia(
243  media_class=MediaClass.DIRECTORY,
244  media_content_id=LIBRARY_ALBUMS,
245  media_content_type=MediaType.ALBUM,
246  title=LIBRARY_TITLE_MAP[LIBRARY_ALBUMS],
247  can_play=False,
248  can_expand=True,
249  children_media_class=media_class,
250  children=sorted(
251  [
252  build_item(mass, album, can_expand=True)
253  # we only grab the first page here because the
254  # HA media browser does not support paging
255  for album in await mass.music.get_library_albums(limit=500)
256  if album.available
257  ],
258  key=lambda x: x.title,
259  ),
260  )
261 
262 
264  mass: MusicAssistantClient, identifier: str
265 ) -> BrowseMedia:
266  """Build Album items browse listing."""
267  album = await mass.music.get_item_by_uri(identifier)
268  tracks = await mass.music.get_album_tracks(album.item_id, album.provider)
269 
270  return BrowseMedia(
271  media_class=MediaType.ALBUM,
272  media_content_id=album.uri,
273  media_content_type=MediaType.ALBUM,
274  title=album.name,
275  can_play=True,
276  can_expand=True,
277  children_media_class=MediaClass.TRACK,
278  children=[
279  build_item(mass, track, False) for track in tracks if track.available
280  ],
281  )
282 
283 
284 async def build_tracks_listing(mass: MusicAssistantClient) -> BrowseMedia:
285  """Build Tracks browse listing."""
286  media_class = LIBRARY_MEDIA_CLASS_MAP[LIBRARY_TRACKS]
287 
288  return BrowseMedia(
289  media_class=MediaClass.DIRECTORY,
290  media_content_id=LIBRARY_TRACKS,
291  media_content_type=MediaType.TRACK,
292  title=LIBRARY_TITLE_MAP[LIBRARY_TRACKS],
293  can_play=False,
294  can_expand=True,
295  children_media_class=media_class,
296  children=sorted(
297  [
298  build_item(mass, track, can_expand=False)
299  # we only grab the first page here because the
300  # HA media browser does not support paging
301  for track in await mass.music.get_library_tracks(limit=500)
302  if track.available
303  ],
304  key=lambda x: x.title,
305  ),
306  )
307 
308 
309 async def build_radio_listing(mass: MusicAssistantClient) -> BrowseMedia:
310  """Build Radio browse listing."""
311  media_class = LIBRARY_MEDIA_CLASS_MAP[LIBRARY_RADIO]
312  return BrowseMedia(
313  media_class=MediaClass.DIRECTORY,
314  media_content_id=LIBRARY_RADIO,
315  media_content_type=DOMAIN,
316  title=LIBRARY_TITLE_MAP[LIBRARY_RADIO],
317  can_play=False,
318  can_expand=True,
319  children_media_class=media_class,
320  children=[
321  build_item(mass, track, can_expand=False, media_class=media_class)
322  # we only grab the first page here because the
323  # HA media browser does not support paging
324  for track in await mass.music.get_library_radios(limit=500)
325  if track.available
326  ],
327  )
328 
329 
331  mass: MusicAssistantClient,
332  item: MediaItemType,
333  can_expand: bool = True,
334  media_class: Any = None,
335 ) -> BrowseMedia:
336  """Return BrowseMedia for MediaItem."""
337  if artists := getattr(item, "artists", None):
338  title = f"{artists[0].name} - {item.name}"
339  else:
340  title = item.name
341  img_url = mass.get_media_item_image_url(item)
342 
343  return BrowseMedia(
344  media_class=media_class or item.media_type.value,
345  media_content_id=item.uri,
346  media_content_type=MediaType.MUSIC,
347  title=title,
348  can_play=True,
349  can_expand=can_expand,
350  thumbnail=img_url,
351  )
BrowseMedia build_main_listing(HomeAssistant hass)
BrowseMedia build_album_items_listing(MusicAssistantClient mass, str identifier)
BrowseMedia build_playlist_items_listing(MusicAssistantClient mass, str identifier)
BrowseMedia build_artists_listing(MusicAssistantClient mass)
BrowseMedia build_albums_listing(MusicAssistantClient mass)
BrowseMedia build_tracks_listing(MusicAssistantClient mass)
BrowseMedia async_browse_media(HomeAssistant hass, MusicAssistantClient mass, str|None media_content_id, str|None media_content_type)
BrowseMedia build_item(MusicAssistantClient mass, MediaItemType item, bool can_expand=True, Any media_class=None)
BrowseMedia build_radio_listing(MusicAssistantClient mass)
BrowseMedia build_playlists_listing(MusicAssistantClient mass)
BrowseMedia build_artist_items_listing(MusicAssistantClient mass, str identifier)