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 import contextlib
6 from typing import Any
7 
8 from pysqueezebox import Player
9 
10 from homeassistant.components import media_source
12  BrowseError,
13  BrowseMedia,
14  MediaClass,
15  MediaPlayerEntity,
16  MediaType,
17 )
18 from homeassistant.core import HomeAssistant
19 from homeassistant.helpers.network import is_internal_request
20 
21 LIBRARY = [
22  "Favorites",
23  "Artists",
24  "Albums",
25  "Tracks",
26  "Playlists",
27  "Genres",
28  "New Music",
29 ]
30 
31 MEDIA_TYPE_TO_SQUEEZEBOX = {
32  "Favorites": "favorites",
33  "Artists": "artists",
34  "Albums": "albums",
35  "Tracks": "titles",
36  "Playlists": "playlists",
37  "Genres": "genres",
38  "New Music": "new music",
39  MediaType.ALBUM: "album",
40  MediaType.ARTIST: "artist",
41  MediaType.TRACK: "title",
42  MediaType.PLAYLIST: "playlist",
43  MediaType.GENRE: "genre",
44 }
45 
46 SQUEEZEBOX_ID_BY_TYPE = {
47  MediaType.ALBUM: "album_id",
48  MediaType.ARTIST: "artist_id",
49  MediaType.TRACK: "track_id",
50  MediaType.PLAYLIST: "playlist_id",
51  MediaType.GENRE: "genre_id",
52  "Favorites": "item_id",
53 }
54 
55 CONTENT_TYPE_MEDIA_CLASS: dict[str | MediaType, dict[str, MediaClass | None]] = {
56  "Favorites": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK},
57  "Artists": {"item": MediaClass.DIRECTORY, "children": MediaClass.ARTIST},
58  "Albums": {"item": MediaClass.DIRECTORY, "children": MediaClass.ALBUM},
59  "Tracks": {"item": MediaClass.DIRECTORY, "children": MediaClass.TRACK},
60  "Playlists": {"item": MediaClass.DIRECTORY, "children": MediaClass.PLAYLIST},
61  "Genres": {"item": MediaClass.DIRECTORY, "children": MediaClass.GENRE},
62  "New Music": {"item": MediaClass.DIRECTORY, "children": MediaClass.ALBUM},
63  MediaType.ALBUM: {"item": MediaClass.ALBUM, "children": MediaClass.TRACK},
64  MediaType.ARTIST: {"item": MediaClass.ARTIST, "children": MediaClass.ALBUM},
65  MediaType.TRACK: {"item": MediaClass.TRACK, "children": None},
66  MediaType.GENRE: {"item": MediaClass.GENRE, "children": MediaClass.ARTIST},
67  MediaType.PLAYLIST: {"item": MediaClass.PLAYLIST, "children": MediaClass.TRACK},
68 }
69 
70 CONTENT_TYPE_TO_CHILD_TYPE = {
71  MediaType.ALBUM: MediaType.TRACK,
72  MediaType.PLAYLIST: MediaType.PLAYLIST,
73  MediaType.ARTIST: MediaType.ALBUM,
74  MediaType.GENRE: MediaType.ARTIST,
75  "Artists": MediaType.ARTIST,
76  "Albums": MediaType.ALBUM,
77  "Tracks": MediaType.TRACK,
78  "Playlists": MediaType.PLAYLIST,
79  "Genres": MediaType.GENRE,
80  "Favorites": None, # can only be determined after inspecting the item
81  "New Music": MediaType.ALBUM,
82 }
83 
84 BROWSE_LIMIT = 1000
85 
86 
88  entity: MediaPlayerEntity, player: Player, payload: dict[str, str | None]
89 ) -> BrowseMedia:
90  """Create response payload for search described by payload."""
91 
92  internal_request = is_internal_request(entity.hass)
93 
94  search_id = payload["search_id"]
95  search_type = payload["search_type"]
96  assert (
97  search_type is not None
98  ) # async_browse_media will not call this function if search_type is None
99  media_class = CONTENT_TYPE_MEDIA_CLASS[search_type]
100 
101  children = None
102 
103  if search_id and search_id != search_type:
104  browse_id = (SQUEEZEBOX_ID_BY_TYPE[search_type], search_id)
105  else:
106  browse_id = None
107 
108  result = await player.async_browse(
109  MEDIA_TYPE_TO_SQUEEZEBOX[search_type],
110  limit=BROWSE_LIMIT,
111  browse_id=browse_id,
112  )
113 
114  if result is not None and result.get("items"):
115  item_type = CONTENT_TYPE_TO_CHILD_TYPE[search_type]
116 
117  children = []
118  for item in result["items"]:
119  item_id = str(item["id"])
120  item_thumbnail: str | None = None
121  if item_type:
122  child_item_type: MediaType | str = item_type
123  child_media_class = CONTENT_TYPE_MEDIA_CLASS[item_type]
124  can_expand = child_media_class["children"] is not None
125  can_play = True
126 
127  if search_type == "Favorites":
128  if "album_id" in item:
129  item_id = str(item["album_id"])
130  child_item_type = MediaType.ALBUM
131  child_media_class = CONTENT_TYPE_MEDIA_CLASS[MediaType.ALBUM]
132  can_expand = True
133  can_play = True
134  elif item["hasitems"]:
135  child_item_type = "Favorites"
136  child_media_class = CONTENT_TYPE_MEDIA_CLASS["Favorites"]
137  can_expand = True
138  can_play = False
139  else:
140  child_item_type = "Favorites"
141  child_media_class = CONTENT_TYPE_MEDIA_CLASS[MediaType.TRACK]
142  can_expand = False
143  can_play = True
144 
145  if artwork_track_id := item.get("artwork_track_id"):
146  if internal_request:
147  item_thumbnail = player.generate_image_url_from_track_id(
148  artwork_track_id
149  )
150  elif item_type is not None:
151  item_thumbnail = entity.get_browse_image_url(
152  item_type, item_id, artwork_track_id
153  )
154  else:
155  item_thumbnail = item.get("image_url") # will not be proxied by HA
156 
157  assert child_media_class["item"] is not None
158  children.append(
159  BrowseMedia(
160  title=item["title"],
161  media_class=child_media_class["item"],
162  media_content_id=item_id,
163  media_content_type=child_item_type,
164  can_play=can_play,
165  can_expand=can_expand,
166  thumbnail=item_thumbnail,
167  )
168  )
169 
170  if children is None:
171  raise BrowseError(f"Media not found: {search_type} / {search_id}")
172 
173  assert media_class["item"] is not None
174  if not search_id:
175  search_id = search_type
176  return BrowseMedia(
177  title=result.get("title"),
178  media_class=media_class["item"],
179  children_media_class=media_class["children"],
180  media_content_id=search_id,
181  media_content_type=search_type,
182  can_play=search_type != "Favorites",
183  children=children,
184  can_expand=True,
185  )
186 
187 
188 async def library_payload(hass: HomeAssistant, player: Player) -> BrowseMedia:
189  """Create response payload to describe contents of library."""
190  library_info: dict[str, Any] = {
191  "title": "Music Library",
192  "media_class": MediaClass.DIRECTORY,
193  "media_content_id": "library",
194  "media_content_type": "library",
195  "can_play": False,
196  "can_expand": True,
197  "children": [],
198  }
199 
200  for item in LIBRARY:
201  media_class = CONTENT_TYPE_MEDIA_CLASS[item]
202 
203  result = await player.async_browse(
204  MEDIA_TYPE_TO_SQUEEZEBOX[item],
205  limit=1,
206  )
207  if result is not None and result.get("items") is not None:
208  assert media_class["children"] is not None
209  library_info["children"].append(
210  BrowseMedia(
211  title=item,
212  media_class=media_class["children"],
213  media_content_id=item,
214  media_content_type=item,
215  can_play=item != "Favorites",
216  can_expand=True,
217  )
218  )
219 
220  with contextlib.suppress(media_source.BrowseError):
221  browse = await media_source.async_browse_media(
222  hass, None, content_filter=media_source_content_filter
223  )
224  # If domain is None, it's overview of available sources
225  if browse.domain is None:
226  library_info["children"].extend(browse.children)
227  else:
228  library_info["children"].append(browse)
229 
230  return BrowseMedia(**library_info)
231 
232 
233 def media_source_content_filter(item: BrowseMedia) -> bool:
234  """Content filter for media sources."""
235  return item.media_content_type.startswith("audio/")
236 
237 
238 async def generate_playlist(player: Player, payload: dict[str, str]) -> list | None:
239  """Generate playlist from browsing payload."""
240  media_type = payload["search_type"]
241  media_id = payload["search_id"]
242 
243  if media_type not in SQUEEZEBOX_ID_BY_TYPE:
244  raise BrowseError(f"Media type not supported: {media_type}")
245 
246  browse_id = (SQUEEZEBOX_ID_BY_TYPE[media_type], media_id)
247  result = await player.async_browse(
248  "titles", limit=BROWSE_LIMIT, browse_id=browse_id
249  )
250  if result and "items" in result:
251  items: list = result["items"]
252  return items
253  raise BrowseError(f"Media not found: {media_type} / {media_id}")
BrowseMedia library_payload(HomeAssistant hass, Player player)
BrowseMedia build_item_response(MediaPlayerEntity entity, Player player, dict[str, str|None] payload)
Definition: browse_media.py:89
list|None generate_playlist(Player player, dict[str, str] payload)
bool is_internal_request(HomeAssistant hass)
Definition: network.py:31