1 """Browse media for forked-daapd."""
3 from __future__
import annotations
5 from collections.abc
import Sequence
6 from dataclasses
import dataclass
7 from typing
import TYPE_CHECKING, Any, cast
8 from urllib.parse
import quote, unquote
18 from .const
import CAN_PLAY_TYPE, URI_SCHEMA
21 from .
import media_player
23 MEDIA_TYPE_DIRECTORY =
"directory"
26 "Albums": (MediaClass.ALBUM, MediaType.ALBUM,
""),
27 "Artists": (MediaClass.ARTIST, MediaType.ARTIST,
""),
28 "Playlists": (MediaClass.PLAYLIST, MediaType.PLAYLIST,
""),
29 "Albums by Genre": (MediaClass.GENRE, MediaType.GENRE, MediaType.ALBUM),
30 "Tracks by Genre": (MediaClass.GENRE, MediaType.GENRE, MediaType.TRACK),
31 "Artists by Genre": (MediaClass.GENRE, MediaType.GENRE, MediaType.ARTIST),
32 "Directories": (MediaClass.DIRECTORY, MEDIA_TYPE_DIRECTORY,
""),
34 MEDIA_TYPE_TO_MEDIA_CLASS = {
35 MediaType.ALBUM: MediaClass.ALBUM,
36 MediaType.APP: MediaClass.APP,
37 MediaType.ARTIST: MediaClass.ARTIST,
38 MediaType.TRACK: MediaClass.TRACK,
39 MediaType.PLAYLIST: MediaClass.PLAYLIST,
40 MediaType.GENRE: MediaClass.GENRE,
41 MEDIA_TYPE_DIRECTORY: MediaClass.DIRECTORY,
52 OWNTONE_TYPE_TO_MEDIA_TYPE = {
53 "track": MediaType.TRACK,
54 "playlist": MediaType.PLAYLIST,
55 "artist": MediaType.ARTIST,
56 "album": MediaType.ALBUM,
57 "genre": MediaType.GENRE,
58 MediaType.APP: MediaType.APP,
59 MEDIA_TYPE_DIRECTORY: MEDIA_TYPE_DIRECTORY,
61 MEDIA_TYPE_TO_OWNTONE_TYPE = {v: k
for k, v
in OWNTONE_TYPE_TO_MEDIA_TYPE.items()}
72 """Class for representing OwnTone media content."""
79 def __init__(self, media_content_id: str) ->
None:
80 """Create MediaContent from media_content_id."""
88 ) = media_content_id.split(
":")
91 self.
typetype = OWNTONE_TYPE_TO_MEDIA_TYPE[self.
typetype]
95 """Create an OwnTone uri."""
96 return f
"library:{MEDIA_TYPE_TO_OWNTONE_TYPE[media_type]}:{quote(id_or_path)}"
101 owntone_uri: str =
"",
102 media_type: str =
"",
103 id_or_path: str =
"",
106 """Create a media_content_id.
108 Either owntone_uri or both type and id_or_path must be specified.
112 return f
"{URI_SCHEMA}:{quote(title)}:{owntone_uri}:{subtype}"
116 """Return whether this media_content_id is from our integration."""
117 return media_content_id[: len(URI_SCHEMA)] == URI_SCHEMA
121 """Convert media_content_id to OwnTone URI."""
122 return ":".join(media_content_id.split(
":")[2:-1])
126 master: media_player.ForkedDaapdMaster,
127 media_content_id: str,
129 """Create response for the given media_content_id."""
132 result: list[dict[str, int | str]] | dict[str, Any] |
None =
None
133 if media_content.type == MediaType.APP:
136 if media_content.type == MEDIA_TYPE_DIRECTORY:
138 directory_path = media_content.id_or_path
140 result = await master.api.get_directory(directory=directory_path)
142 result = await master.api.get_directory()
145 f
"Media not found for {media_content.type} / {media_content_id}"
149 assert isinstance(result, dict)
150 for directory
in result[
"directories"]:
151 path = directory[
"path"]
155 media_class=MediaClass.DIRECTORY,
157 title=path, media_type=MEDIA_TYPE_DIRECTORY, id_or_path=path
159 media_content_type=MEDIA_TYPE_DIRECTORY,
164 result = result[
"tracks"][
"items"] + result[
"playlists"][
"items"]
168 cast(list[dict[str, int | str]], result),
171 if media_content.id_or_path ==
"":
172 if media_content.type == MediaType.ALBUM:
174 await master.api.get_albums()
176 elif media_content.type == MediaType.ARTIST:
177 result = await master.api.get_artists()
178 elif media_content.type == MediaType.GENRE:
179 if result := await master.api.get_genres():
183 MediaType.GENRE, cast(str, item[
"name"])
185 elif media_content.type == MediaType.PLAYLIST:
187 await master.api.get_playlists()
191 f
"Media not found for {media_content.type} / {media_content_id}"
196 cast(list[dict[str, int | str]], result),
200 if media_content.type == MediaType.ALBUM:
201 result = await master.api.get_tracks(album_id=media_content.id_or_path)
202 elif media_content.type == MediaType.ARTIST:
203 result = await master.api.get_albums(artist_id=media_content.id_or_path)
204 elif media_content.type == MediaType.GENRE:
205 if media_content.subtype
in {
210 result = await master.api.get_genre(
211 media_content.id_or_path, media_type=media_content.subtype
213 elif media_content.type == MediaType.PLAYLIST:
214 result = await master.api.get_tracks(playlist_id=media_content.id_or_path)
218 f
"Media not found for {media_content.type} / {media_content_id}"
222 master, media_content, cast(list[dict[str, int | str]], result)
227 master: media_player.ForkedDaapdMaster,
228 media_content: MediaContent,
229 result: list[dict[str, int | str]],
230 children: list[BrowseMedia] |
None =
None,
232 """Convert the results into a browse media response."""
237 if item.get(
"data_kind") ==
"spotify" or (
238 "path" in item
and cast(str, item[
"path"]).startswith(
"spotify")
241 assert isinstance(item[
"uri"], str)
242 media_type = OWNTONE_TYPE_TO_MEDIA_TYPE[item[
"uri"].split(
":")[1]]
243 title = item.get(
"name")
or item.get(
"title")
244 assert isinstance(title, str)
246 title=f
"{media_content.title} / {title}",
247 owntone_uri=item[
"uri"],
248 subtype=media_content.subtype,
250 if artwork := item.get(
"artwork_url"):
252 master.api.full_url(cast(str, artwork))
254 else master.get_browse_image_url(media_type, media_content_id)
261 media_class=MEDIA_TYPE_TO_MEDIA_CLASS[media_type],
262 media_content_id=media_content_id,
263 media_content_type=media_type,
264 can_play=media_type
in CAN_PLAY_TYPE,
265 can_expand=media_type
in CAN_EXPAND_TYPE,
270 title=media_content.id_or_path
271 if media_content.type == MEDIA_TYPE_DIRECTORY
272 else media_content.title,
273 media_class=MEDIA_TYPE_TO_MEDIA_CLASS[media_content.type],
275 media_content_type=media_content.type,
276 can_play=media_content.type
in CAN_PLAY_TYPE,
277 can_expand=media_content.type
in CAN_EXPAND_TYPE,
283 """Return the base of our OwnTone library."""
287 media_class=media_class,
289 title=name, media_type=media_type, subtype=media_subtype
291 media_content_type=MEDIA_TYPE_DIRECTORY,
295 for name, (media_class, media_type, media_subtype)
in TOP_LEVEL_LIBRARY.items()
298 title=
"OwnTone Library",
299 media_class=MediaClass.APP,
301 title=
"OwnTone Library", media_type=MediaType.APP
303 media_content_type=MediaType.APP,
307 thumbnail=
"https://brands.home-assistant.io/_/forked_daapd/logo.png",
311 def library(other: Sequence[BrowseMedia] |
None) -> BrowseMedia:
312 """Create response to describe contents of library."""
316 title=
"OwnTone Library",
317 media_class=MediaClass.APP,
319 title=
"OwnTone Library", media_type=MediaType.APP
321 media_content_type=MediaType.APP,
324 thumbnail=
"https://brands.home-assistant.io/_/forked_daapd/logo.png",
328 top_level_items.extend(other)
332 media_class=MediaClass.DIRECTORY,
334 media_content_type=MEDIA_TYPE_DIRECTORY,
337 children=top_level_items,
None __init__(self, str media_content_id)
bool is_internal_request(HomeAssistant hass)