Allow browsing the Spotify media player in Sonos (#64921)

This commit is contained in:
Paulus Schoutsen 2022-01-25 12:43:43 -08:00 committed by GitHub
parent f32d9952c8
commit a371f8f788
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 174 additions and 63 deletions

View File

@ -5,7 +5,7 @@
"documentation": "https://www.home-assistant.io/integrations/sonos", "documentation": "https://www.home-assistant.io/integrations/sonos",
"requirements": ["soco==0.26.0"], "requirements": ["soco==0.26.0"],
"dependencies": ["ssdp"], "dependencies": ["ssdp"],
"after_dependencies": ["plex", "zeroconf", "media_source"], "after_dependencies": ["plex", "spotify", "zeroconf", "media_source"],
"zeroconf": ["_sonos._tcp.local."], "zeroconf": ["_sonos._tcp.local."],
"ssdp": [ "ssdp": [
{ {

View File

@ -7,7 +7,7 @@ from functools import partial
import logging import logging
from urllib.parse import quote_plus, unquote from urllib.parse import quote_plus, unquote
from homeassistant.components import media_source from homeassistant.components import media_source, spotify
from homeassistant.components.media_player import BrowseMedia from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
MEDIA_CLASS_DIRECTORY, MEDIA_CLASS_DIRECTORY,
@ -90,6 +90,14 @@ async def async_browse_media(
hass, media_content_id, content_filter=media_source_filter hass, media_content_id, content_filter=media_source_filter
) )
if spotify.is_spotify_media_type(media_content_type):
return await spotify.async_browse_media(
hass, media_content_type, media_content_id, can_play_artist=False
)
if media_content_type == "spotify":
return await spotify.async_browse_media(hass, None, None, can_play_artist=False)
if media_content_type == "library": if media_content_type == "library":
return await hass.async_add_executor_job( return await hass.async_add_executor_job(
library_payload, library_payload,
@ -248,6 +256,19 @@ async def root_payload(
) )
) )
if "spotify" in hass.config.components:
children.append(
BrowseMedia(
title="Spotify",
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id="",
media_content_type="spotify",
thumbnail="https://brands.home-assistant.io/spotify/icon.png",
can_play=False,
can_expand=True,
)
)
if await hass.async_add_executor_job( if await hass.async_add_executor_job(
partial(media.library.browse_by_idstring, "tracks", "", max_items=1) partial(media.library.browse_by_idstring, "tracks", "", max_items=1)
): ):

View File

@ -18,7 +18,7 @@ from soco.core import (
from soco.data_structures import DidlFavorite from soco.data_structures import DidlFavorite
import voluptuous as vol import voluptuous as vol
from homeassistant.components import media_source from homeassistant.components import media_source, spotify
from homeassistant.components.http.auth import async_sign_path from homeassistant.components.http.auth import async_sign_path
from homeassistant.components.media_player import MediaPlayerEntity from homeassistant.components.media_player import MediaPlayerEntity
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
@ -520,6 +520,9 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue.
""" """
if spotify.is_spotify_media_type(media_type):
media_type = spotify.resolve_spotify_media_type(media_type)
if media_source.is_media_source_id(media_id): if media_source.is_media_source_id(media_id):
media_type = MEDIA_TYPE_MUSIC media_type = MEDIA_TYPE_MUSIC
media_id = ( media_id = (

View File

@ -26,8 +26,10 @@ from .const import (
DATA_SPOTIFY_ME, DATA_SPOTIFY_ME,
DATA_SPOTIFY_SESSION, DATA_SPOTIFY_SESSION,
DOMAIN, DOMAIN,
MEDIA_PLAYER_PREFIX,
SPOTIFY_SCOPES, SPOTIFY_SCOPES,
) )
from .media_player import async_browse_media_internal
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
@ -44,6 +46,31 @@ CONFIG_SCHEMA = vol.Schema(
PLATFORMS = [Platform.MEDIA_PLAYER] PLATFORMS = [Platform.MEDIA_PLAYER]
def is_spotify_media_type(media_content_type):
"""Return whether the media_content_type is a valid Spotify media_id."""
return media_content_type.startswith(MEDIA_PLAYER_PREFIX)
def resolve_spotify_media_type(media_content_type):
"""Return actual spotify media_content_type."""
return media_content_type[len(MEDIA_PLAYER_PREFIX) :]
async def async_browse_media(
hass, media_content_type, media_content_id, *, can_play_artist=True
):
"""Browse Spotify media."""
info = list(hass.data[DOMAIN].values())[0]
return await async_browse_media_internal(
hass,
info[DATA_SPOTIFY_CLIENT],
info[DATA_SPOTIFY_ME],
media_content_type,
media_content_id,
can_play_artist=can_play_artist,
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Spotify integration.""" """Set up the Spotify integration."""
if DOMAIN not in config: if DOMAIN not in config:

View File

@ -22,3 +22,5 @@ SPOTIFY_SCOPES = [
"user-read-recently-played", "user-read-recently-played",
"user-follow-read", "user-follow-read",
] ]
MEDIA_PLAYER_PREFIX = "spotify://"

View File

@ -4,12 +4,14 @@ from __future__ import annotations
from asyncio import run_coroutine_threadsafe from asyncio import run_coroutine_threadsafe
import datetime as dt import datetime as dt
from datetime import timedelta from datetime import timedelta
from functools import partial
import logging import logging
import requests import requests
from spotipy import Spotify, SpotifyException from spotipy import Spotify, SpotifyException
from yarl import URL from yarl import URL
from homeassistant.backports.enum import StrEnum
from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity
from homeassistant.components.media_player.const import ( from homeassistant.components.media_player.const import (
MEDIA_CLASS_ALBUM, MEDIA_CLASS_ALBUM,
@ -63,6 +65,7 @@ from .const import (
DATA_SPOTIFY_ME, DATA_SPOTIFY_ME,
DATA_SPOTIFY_SESSION, DATA_SPOTIFY_SESSION,
DOMAIN, DOMAIN,
MEDIA_PLAYER_PREFIX,
SPOTIFY_SCOPES, SPOTIFY_SCOPES,
) )
@ -107,63 +110,86 @@ PLAYABLE_MEDIA_TYPES = [
MEDIA_TYPE_TRACK, MEDIA_TYPE_TRACK,
] ]
class BrowsableMedia(StrEnum):
"""Enum of browsable media."""
CURRENT_USER_PLAYLISTS = "current_user_playlists"
CURRENT_USER_FOLLOWED_ARTISTS = "current_user_followed_artists"
CURRENT_USER_SAVED_ALBUMS = "current_user_saved_albums"
CURRENT_USER_SAVED_TRACKS = "current_user_saved_tracks"
CURRENT_USER_SAVED_SHOWS = "current_user_saved_shows"
CURRENT_USER_RECENTLY_PLAYED = "current_user_recently_played"
CURRENT_USER_TOP_ARTISTS = "current_user_top_artists"
CURRENT_USER_TOP_TRACKS = "current_user_top_tracks"
CATEGORIES = "categories"
FEATURED_PLAYLISTS = "featured_playlists"
NEW_RELEASES = "new_releases"
LIBRARY_MAP = { LIBRARY_MAP = {
"current_user_playlists": "Playlists", BrowsableMedia.CURRENT_USER_PLAYLISTS: "Playlists",
"current_user_followed_artists": "Artists", BrowsableMedia.CURRENT_USER_FOLLOWED_ARTISTS: "Artists",
"current_user_saved_albums": "Albums", BrowsableMedia.CURRENT_USER_SAVED_ALBUMS: "Albums",
"current_user_saved_tracks": "Tracks", BrowsableMedia.CURRENT_USER_SAVED_TRACKS: "Tracks",
"current_user_saved_shows": "Podcasts", BrowsableMedia.CURRENT_USER_SAVED_SHOWS: "Podcasts",
"current_user_recently_played": "Recently played", BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED: "Recently played",
"current_user_top_artists": "Top Artists", BrowsableMedia.CURRENT_USER_TOP_ARTISTS: "Top Artists",
"current_user_top_tracks": "Top Tracks", BrowsableMedia.CURRENT_USER_TOP_TRACKS: "Top Tracks",
"categories": "Categories", BrowsableMedia.CATEGORIES: "Categories",
"featured_playlists": "Featured Playlists", BrowsableMedia.FEATURED_PLAYLISTS: "Featured Playlists",
"new_releases": "New Releases", BrowsableMedia.NEW_RELEASES: "New Releases",
} }
CONTENT_TYPE_MEDIA_CLASS = { CONTENT_TYPE_MEDIA_CLASS = {
"current_user_playlists": { BrowsableMedia.CURRENT_USER_PLAYLISTS: {
"parent": MEDIA_CLASS_DIRECTORY, "parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_PLAYLIST, "children": MEDIA_CLASS_PLAYLIST,
}, },
"current_user_followed_artists": { BrowsableMedia.CURRENT_USER_FOLLOWED_ARTISTS: {
"parent": MEDIA_CLASS_DIRECTORY, "parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_ARTIST, "children": MEDIA_CLASS_ARTIST,
}, },
"current_user_saved_albums": { BrowsableMedia.CURRENT_USER_SAVED_ALBUMS: {
"parent": MEDIA_CLASS_DIRECTORY, "parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_ALBUM, "children": MEDIA_CLASS_ALBUM,
}, },
"current_user_saved_tracks": { BrowsableMedia.CURRENT_USER_SAVED_TRACKS: {
"parent": MEDIA_CLASS_DIRECTORY, "parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_TRACK, "children": MEDIA_CLASS_TRACK,
}, },
"current_user_saved_shows": { BrowsableMedia.CURRENT_USER_SAVED_SHOWS: {
"parent": MEDIA_CLASS_DIRECTORY, "parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_PODCAST, "children": MEDIA_CLASS_PODCAST,
}, },
"current_user_recently_played": { BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED: {
"parent": MEDIA_CLASS_DIRECTORY, "parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_TRACK, "children": MEDIA_CLASS_TRACK,
}, },
"current_user_top_artists": { BrowsableMedia.CURRENT_USER_TOP_ARTISTS: {
"parent": MEDIA_CLASS_DIRECTORY, "parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_ARTIST, "children": MEDIA_CLASS_ARTIST,
}, },
"current_user_top_tracks": { BrowsableMedia.CURRENT_USER_TOP_TRACKS: {
"parent": MEDIA_CLASS_DIRECTORY, "parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_TRACK, "children": MEDIA_CLASS_TRACK,
}, },
"featured_playlists": { BrowsableMedia.FEATURED_PLAYLISTS: {
"parent": MEDIA_CLASS_DIRECTORY, "parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_PLAYLIST, "children": MEDIA_CLASS_PLAYLIST,
}, },
"categories": {"parent": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_GENRE}, BrowsableMedia.CATEGORIES: {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_GENRE,
},
"category_playlists": { "category_playlists": {
"parent": MEDIA_CLASS_DIRECTORY, "parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_PLAYLIST, "children": MEDIA_CLASS_PLAYLIST,
}, },
"new_releases": {"parent": MEDIA_CLASS_DIRECTORY, "children": MEDIA_CLASS_ALBUM}, BrowsableMedia.NEW_RELEASES: {
"parent": MEDIA_CLASS_DIRECTORY,
"children": MEDIA_CLASS_ALBUM,
},
MEDIA_TYPE_PLAYLIST: { MEDIA_TYPE_PLAYLIST: {
"parent": MEDIA_CLASS_PLAYLIST, "parent": MEDIA_CLASS_PLAYLIST,
"children": MEDIA_CLASS_TRACK, "children": MEDIA_CLASS_TRACK,
@ -421,6 +447,9 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
@spotify_exception_handler @spotify_exception_handler
def play_media(self, media_type: str, media_id: str, **kwargs) -> None: def play_media(self, media_type: str, media_id: str, **kwargs) -> None:
"""Play media.""" """Play media."""
if media_type.startswith(MEDIA_PLAYER_PREFIX):
media_type = media_type[len(MEDIA_PLAYER_PREFIX) :]
kwargs = {} kwargs = {}
# Spotify can't handle URI's with query strings or anchors # Spotify can't handle URI's with query strings or anchors
@ -494,57 +523,81 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
) )
raise NotImplementedError raise NotImplementedError
if media_content_type in (None, "library"): return await async_browse_media_internal(
return await self.hass.async_add_executor_job(library_payload) self.hass, self._spotify, self._me, media_content_type, media_content_id
payload = {
"media_content_type": media_content_type,
"media_content_id": media_content_id,
}
response = await self.hass.async_add_executor_job(
build_item_response, self._spotify, self._me, payload
) )
if response is None:
raise BrowseError(
f"Media not found: {media_content_type} / {media_content_id}"
)
return response
def build_item_response(spotify, user, payload): # noqa: C901 async def async_browse_media_internal(
hass,
spotify,
current_user,
media_content_type,
media_content_id,
*,
can_play_artist=True,
):
"""Browse spotify media."""
if media_content_type in (None, f"{MEDIA_PLAYER_PREFIX}library"):
return await hass.async_add_executor_job(
partial(library_payload, can_play_artist=can_play_artist)
)
# Strip prefix
media_content_type = media_content_type[len(MEDIA_PLAYER_PREFIX) :]
payload = {
"media_content_type": media_content_type,
"media_content_id": media_content_id,
}
response = await hass.async_add_executor_job(
partial(
build_item_response,
spotify,
current_user,
payload,
can_play_artist=can_play_artist,
)
)
if response is None:
raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}")
return response
def build_item_response(spotify, user, payload, *, can_play_artist): # noqa: C901
"""Create response payload for the provided media query.""" """Create response payload for the provided media query."""
media_content_type = payload["media_content_type"] media_content_type = payload["media_content_type"]
media_content_id = payload["media_content_id"] media_content_id = payload["media_content_id"]
title = None title = None
image = None image = None
if media_content_type == "current_user_playlists": if media_content_type == BrowsableMedia.CURRENT_USER_PLAYLISTS:
media = spotify.current_user_playlists(limit=BROWSE_LIMIT) media = spotify.current_user_playlists(limit=BROWSE_LIMIT)
items = media.get("items", []) items = media.get("items", [])
elif media_content_type == "current_user_followed_artists": elif media_content_type == BrowsableMedia.CURRENT_USER_FOLLOWED_ARTISTS:
media = spotify.current_user_followed_artists(limit=BROWSE_LIMIT) media = spotify.current_user_followed_artists(limit=BROWSE_LIMIT)
items = media.get("artists", {}).get("items", []) items = media.get("artists", {}).get("items", [])
elif media_content_type == "current_user_saved_albums": elif media_content_type == BrowsableMedia.CURRENT_USER_SAVED_ALBUMS:
media = spotify.current_user_saved_albums(limit=BROWSE_LIMIT) media = spotify.current_user_saved_albums(limit=BROWSE_LIMIT)
items = [item["album"] for item in media.get("items", [])] items = [item["album"] for item in media.get("items", [])]
elif media_content_type == "current_user_saved_tracks": elif media_content_type == BrowsableMedia.CURRENT_USER_SAVED_TRACKS:
media = spotify.current_user_saved_tracks(limit=BROWSE_LIMIT) media = spotify.current_user_saved_tracks(limit=BROWSE_LIMIT)
items = [item["track"] for item in media.get("items", [])] items = [item["track"] for item in media.get("items", [])]
elif media_content_type == "current_user_saved_shows": elif media_content_type == BrowsableMedia.CURRENT_USER_SAVED_SHOWS:
media = spotify.current_user_saved_shows(limit=BROWSE_LIMIT) media = spotify.current_user_saved_shows(limit=BROWSE_LIMIT)
items = [item["show"] for item in media.get("items", [])] items = [item["show"] for item in media.get("items", [])]
elif media_content_type == "current_user_recently_played": elif media_content_type == BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED:
media = spotify.current_user_recently_played(limit=BROWSE_LIMIT) media = spotify.current_user_recently_played(limit=BROWSE_LIMIT)
items = [item["track"] for item in media.get("items", [])] items = [item["track"] for item in media.get("items", [])]
elif media_content_type == "current_user_top_artists": elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_ARTISTS:
media = spotify.current_user_top_artists(limit=BROWSE_LIMIT) media = spotify.current_user_top_artists(limit=BROWSE_LIMIT)
items = media.get("items", []) items = media.get("items", [])
elif media_content_type == "current_user_top_tracks": elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_TRACKS:
media = spotify.current_user_top_tracks(limit=BROWSE_LIMIT) media = spotify.current_user_top_tracks(limit=BROWSE_LIMIT)
items = media.get("items", []) items = media.get("items", [])
elif media_content_type == "featured_playlists": elif media_content_type == BrowsableMedia.FEATURED_PLAYLISTS:
media = spotify.featured_playlists(country=user["country"], limit=BROWSE_LIMIT) media = spotify.featured_playlists(country=user["country"], limit=BROWSE_LIMIT)
items = media.get("playlists", {}).get("items", []) items = media.get("playlists", {}).get("items", [])
elif media_content_type == "categories": elif media_content_type == BrowsableMedia.CATEGORIES:
media = spotify.categories(country=user["country"], limit=BROWSE_LIMIT) media = spotify.categories(country=user["country"], limit=BROWSE_LIMIT)
items = media.get("categories", {}).get("items", []) items = media.get("categories", {}).get("items", [])
elif media_content_type == "category_playlists": elif media_content_type == "category_playlists":
@ -557,7 +610,7 @@ def build_item_response(spotify, user, payload): # noqa: C901
title = category.get("name") title = category.get("name")
image = fetch_image_url(category, key="icons") image = fetch_image_url(category, key="icons")
items = media.get("playlists", {}).get("items", []) items = media.get("playlists", {}).get("items", [])
elif media_content_type == "new_releases": elif media_content_type == BrowsableMedia.NEW_RELEASES:
media = spotify.new_releases(country=user["country"], limit=BROWSE_LIMIT) media = spotify.new_releases(country=user["country"], limit=BROWSE_LIMIT)
items = media.get("albums", {}).get("items", []) items = media.get("albums", {}).get("items", [])
elif media_content_type == MEDIA_TYPE_PLAYLIST: elif media_content_type == MEDIA_TYPE_PLAYLIST:
@ -591,13 +644,13 @@ def build_item_response(spotify, user, payload): # noqa: C901
_LOGGER.debug("Unknown media type received: %s", media_content_type) _LOGGER.debug("Unknown media type received: %s", media_content_type)
return None return None
if media_content_type == "categories": if media_content_type == BrowsableMedia.CATEGORIES:
media_item = BrowseMedia( media_item = BrowseMedia(
title=LIBRARY_MAP.get(media_content_id), title=LIBRARY_MAP.get(media_content_id),
media_class=media_class["parent"], media_class=media_class["parent"],
children_media_class=media_class["children"], children_media_class=media_class["children"],
media_content_id=media_content_id, media_content_id=media_content_id,
media_content_type=media_content_type, media_content_type=MEDIA_PLAYER_PREFIX + media_content_type,
can_play=False, can_play=False,
can_expand=True, can_expand=True,
children=[], children=[],
@ -614,7 +667,7 @@ def build_item_response(spotify, user, payload): # noqa: C901
media_class=MEDIA_CLASS_PLAYLIST, media_class=MEDIA_CLASS_PLAYLIST,
children_media_class=MEDIA_CLASS_TRACK, children_media_class=MEDIA_CLASS_TRACK,
media_content_id=item_id, media_content_id=item_id,
media_content_type="category_playlists", media_content_type=MEDIA_PLAYER_PREFIX + "category_playlists",
thumbnail=fetch_image_url(item, key="icons"), thumbnail=fetch_image_url(item, key="icons"),
can_play=False, can_play=False,
can_expand=True, can_expand=True,
@ -633,14 +686,17 @@ def build_item_response(spotify, user, payload): # noqa: C901
"media_class": media_class["parent"], "media_class": media_class["parent"],
"children_media_class": media_class["children"], "children_media_class": media_class["children"],
"media_content_id": media_content_id, "media_content_id": media_content_id,
"media_content_type": media_content_type, "media_content_type": MEDIA_PLAYER_PREFIX + media_content_type,
"can_play": media_content_type in PLAYABLE_MEDIA_TYPES, "can_play": media_content_type in PLAYABLE_MEDIA_TYPES
and (media_content_type != MEDIA_TYPE_ARTIST or can_play_artist),
"children": [], "children": [],
"can_expand": True, "can_expand": True,
} }
for item in items: for item in items:
try: try:
params["children"].append(item_payload(item)) params["children"].append(
item_payload(item, can_play_artist=can_play_artist)
)
except (MissingMediaInformation, UnknownMediaType): except (MissingMediaInformation, UnknownMediaType):
continue continue
@ -652,7 +708,7 @@ def build_item_response(spotify, user, payload): # noqa: C901
return BrowseMedia(**params) return BrowseMedia(**params)
def item_payload(item): def item_payload(item, *, can_play_artist):
""" """
Create response payload for a single media item. Create response payload for a single media item.
@ -681,8 +737,9 @@ def item_payload(item):
"media_class": media_class["parent"], "media_class": media_class["parent"],
"children_media_class": media_class["children"], "children_media_class": media_class["children"],
"media_content_id": media_id, "media_content_id": media_id,
"media_content_type": media_type, "media_content_type": MEDIA_PLAYER_PREFIX + media_type,
"can_play": media_type in PLAYABLE_MEDIA_TYPES, "can_play": media_type in PLAYABLE_MEDIA_TYPES
and (media_type != MEDIA_TYPE_ARTIST or can_play_artist),
"can_expand": can_expand, "can_expand": can_expand,
} }
@ -694,7 +751,7 @@ def item_payload(item):
return BrowseMedia(**payload) return BrowseMedia(**payload)
def library_payload(): def library_payload(*, can_play_artist):
""" """
Create response payload to describe contents of a specific library. Create response payload to describe contents of a specific library.
@ -704,7 +761,7 @@ def library_payload():
"title": "Media Library", "title": "Media Library",
"media_class": MEDIA_CLASS_DIRECTORY, "media_class": MEDIA_CLASS_DIRECTORY,
"media_content_id": "library", "media_content_id": "library",
"media_content_type": "library", "media_content_type": MEDIA_PLAYER_PREFIX + "library",
"can_play": False, "can_play": False,
"can_expand": True, "can_expand": True,
"children": [], "children": [],
@ -713,7 +770,8 @@ def library_payload():
for item in [{"name": n, "type": t} for t, n in LIBRARY_MAP.items()]: for item in [{"name": n, "type": t} for t, n in LIBRARY_MAP.items()]:
library_info["children"].append( library_info["children"].append(
item_payload( item_payload(
{"name": item["name"], "type": item["type"], "uri": item["type"]} {"name": item["name"], "type": item["type"], "uri": item["type"]},
can_play_artist=can_play_artist,
) )
) )
response = BrowseMedia(**library_info) response = BrowseMedia(**library_info)