Files
HomeAssistance/custom_components/spotifyplus/browse_media.py
T
2026-06-05 22:34:31 -04:00

725 lines
31 KiB
Python

"""Support for Spotify media browsing."""
from __future__ import annotations
from functools import partial
from enum import StrEnum
import logging
import base64
import os
import pickle
from typing import Any
from spotifywebapipython import SpotifyClient
from spotifywebapipython.models import *
from homeassistant.components.media_player import (
BrowseError,
BrowseMedia,
MediaClass,
MediaType,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import IntegrationError
from .const import DOMAIN
# get smartinspect logger reference; create a new session for this module name.
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext
import logging
_logsi:SISession = SIAuto.Si.GetSession(__name__)
if (_logsi == None):
_logsi = SIAuto.Si.AddSession(__name__, True)
_logsi.SystemLogger = logging.getLogger(__name__)
class BrowsableMedia(StrEnum):
"""
Enum of browsable media.
Contains the library root node key value definitions.
"""
# library custom root node title definitions.
SPOTIFY_LIBRARY_INDEX = "spotify_library_index"
SPOTIFY_CATEGORY_PLAYLISTS = "spotify_category_playlists"
SPOTIFY_CATEGORY_PLAYLISTS_MADEFORYOU = "spotify_category_playlists_madeforyou"
SPOTIFY_CATEGORYS = "spotify_categorys"
SPOTIFY_FEATURED_PLAYLISTS = "spotify_featured_playlists"
SPOTIFY_NEW_RELEASES = "spotify_new_releases"
SPOTIFY_USER_FOLLOWED_ARTISTS = "spotify_user_followed_artists"
SPOTIFY_USER_PLAYLISTS = "spotify_user_playlists"
SPOTIFY_USER_RECENTLY_PLAYED = "spotify_user_recently_played"
SPOTIFY_USER_SAVED_ALBUMS = "spotify_user_saved_albums"
SPOTIFY_USER_SAVED_AUDIOBOOKS = "spotify_user_saved_audiobooks"
SPOTIFY_USER_SAVED_SHOWS = "spotify_user_saved_shows"
SPOTIFY_USER_SAVED_TRACKS = "spotify_user_saved_tracks"
SPOTIFY_USER_TOP_ARTISTS = "spotify_user_top_artists"
SPOTIFY_USER_TOP_TRACKS = "spotify_user_top_tracks"
# Spotify Library index definitions, containing media attributes that control content display.
# The order listed is how they are displayed in the media browser.
SPOTIFY_LIBRARY_MAP = {
BrowsableMedia.SPOTIFY_LIBRARY_INDEX.value: {
"title": "SpotifyPlus Media Library",
"image": None,
"parent": MediaClass.DIRECTORY,
"children": MediaClass.DIRECTORY,
"is_index_item": False,
},
BrowsableMedia.SPOTIFY_USER_PLAYLISTS.value: {
"title": "Playlists",
"title_node": "Spotify Playlist Favorites",
"image": f"/local/images/{DOMAIN}_medialib_playlists.png",
"parent": MediaClass.DIRECTORY,
"children": MediaClass.PLAYLIST,
},
BrowsableMedia.SPOTIFY_USER_FOLLOWED_ARTISTS.value: {
"title": "Artists",
"title_node": "Spotify Artists Followed",
"image": f"/local/images/{DOMAIN}_medialib_artists.png",
"parent": MediaClass.DIRECTORY,
"children": MediaClass.ARTIST,
},
BrowsableMedia.SPOTIFY_USER_SAVED_ALBUMS.value: {
"title": "Albums",
"title_node": "Spotify Album Favorites",
"image": f"/local/images/{DOMAIN}_medialib_albums.png",
"parent": MediaClass.DIRECTORY,
"children": MediaClass.ALBUM,
},
BrowsableMedia.SPOTIFY_USER_SAVED_TRACKS.value: {
"title": "Tracks",
"title_node": "Spotify Track Favorites",
"image": f"/local/images/{DOMAIN}_medialib_tracks.png",
"parent": MediaClass.DIRECTORY,
"children": MediaClass.TRACK,
},
BrowsableMedia.SPOTIFY_USER_SAVED_SHOWS.value: {
"title": "Podcasts",
"title_node": "Spotify Podcast Favorites",
"image": f"/local/images/{DOMAIN}_medialib_podcasts.png",
"parent": MediaClass.DIRECTORY,
"children": MediaClass.PODCAST,
},
BrowsableMedia.SPOTIFY_USER_SAVED_AUDIOBOOKS.value: {
"title": "Audiobooks",
"title_node": "Spotify Audiobook Favorites",
"image": f"/local/images/{DOMAIN}_medialib_audiobooks.png",
"parent": MediaClass.DIRECTORY,
"children": MediaClass.APP, # spotify audiobook support
},
BrowsableMedia.SPOTIFY_USER_TOP_ARTISTS.value: {
"title": "Top Artists",
"title_node": "Spotify Top Artists",
"image": f"/local/images/{DOMAIN}_medialib_top_artists.png",
"parent": MediaClass.DIRECTORY,
"children": MediaClass.ARTIST,
},
BrowsableMedia.SPOTIFY_USER_TOP_TRACKS.value: {
"title": "Top Tracks",
"title_node": "Spotify Top Tracks",
"image": f"/local/images/{DOMAIN}_medialib_top_tracks.png",
"parent": MediaClass.DIRECTORY,
"children": MediaClass.TRACK,
},
BrowsableMedia.SPOTIFY_FEATURED_PLAYLISTS.value: {
"title": "Featured Playlists",
"title_node": "Spotify Featured Playlists",
"image": f"/local/images/{DOMAIN}_medialib_featured_playlists.png",
"parent": MediaClass.DIRECTORY,
"children": MediaClass.PLAYLIST,
},
BrowsableMedia.SPOTIFY_NEW_RELEASES.value: {
"title": "New Releases",
"title_node": "Spotify New Releases",
"image": f"/local/images/{DOMAIN}_medialib_new_releases.png",
"parent": MediaClass.DIRECTORY,
"children": MediaClass.ALBUM,
},
BrowsableMedia.SPOTIFY_CATEGORYS.value: {
"title": "Categories",
"title_node": "Spotify Categories ",
"image": f"/local/images/{DOMAIN}_medialib_categorys.png",
"parent": MediaClass.DIRECTORY,
"children": MediaClass.GENRE,
},
BrowsableMedia.SPOTIFY_CATEGORY_PLAYLISTS.value: {
"title": "Category Playlists",
"title_node": "Spotify Category Playlists",
"image": f"/local/images/{DOMAIN}_medialib_category_playlists.png",
"parent": MediaClass.DIRECTORY,
"children": MediaClass.PLAYLIST,
"is_index_item": False,
},
BrowsableMedia.SPOTIFY_CATEGORY_PLAYLISTS_MADEFORYOU.value: {
"title": "Made For You",
"title_node": "Spotify Playlists Made For You",
"image": f"/local/images/{DOMAIN}_medialib_spotify_category_playlists_made_for_you.png",
"parent": MediaClass.DIRECTORY,
"children": MediaClass.PLAYLIST,
},
BrowsableMedia.SPOTIFY_USER_RECENTLY_PLAYED.value: {
"title": "Recently Played",
"title_node": "Spotify Recently Played",
"image": f"/local/images/{DOMAIN}_medialib_recently_played.png",
"parent": MediaClass.DIRECTORY,
"children": MediaClass.TRACK,
},
# the following are HA media types, and will not be displayed in the library index.
# they are required for playing base-level types of media for this library index.
MediaType.ALBUM: {
"parent": MediaClass.ALBUM,
"children": MediaClass.TRACK,
"is_index_item": False,
},
MediaType.ARTIST: {
"parent": MediaClass.ARTIST,
"children": MediaClass.ALBUM,
"is_index_item": False,
},
MediaType.APP: { # spotify audiobook support
"parent": MediaClass.APP,
"children": MediaClass.TRACK,
"is_index_item": False,
},
MediaType.EPISODE: {
"parent": MediaClass.EPISODE,
"children": None,
"is_index_item": False,
},
MediaType.GENRE: {
"parent": MediaClass.PLAYLIST,
"children": MediaClass.TRACK,
"is_index_item": False,
},
MediaType.PLAYLIST: {
"parent": MediaClass.PLAYLIST,
"children": MediaClass.TRACK,
"is_index_item": False,
},
MediaType.PODCAST: {
"parent": MediaClass.PODCAST,
"children": MediaClass.EPISODE,
"is_index_item": False,
},
MediaType.TRACK: {
"parent": MediaClass.TRACK,
"children": None,
"is_index_item": False,
}
}
"""
# Spotify Library index definitions, containing media attributes that control content display.
# The order listed is how they are displayed in the media browser.
"""
BROWSE_LIMIT = 50
""" Max number of items to return from a Spotify Web API query. """
SPOTIFY_BROWSE_LIMIT_TOTAL = 100
""" Max number of items to return from a SpotifyPlus integration request that supports paging. """
CATEGORY_BASE64:str = "category_base64::"
""" Eye-catcher used to denote a serialized ContentItem. """
LOCAL_IMAGE_PREFIX:str = "/local/"
""" Local image prefix value. """
PLAYABLE_MEDIA_TYPES = [
MediaType.PLAYLIST,
MediaType.ALBUM,
MediaType.ARTIST,
MediaType.APP, # spotify audiobook support
MediaType.EPISODE,
MediaType.PODCAST,
MediaType.TRACK,
]
""" Array of all media types that are playable. """
class MissingMediaInformation(BrowseError):
"""Missing media required information."""
class UnknownMediaType(BrowseError):
"""Unknown media type."""
def deserialize_object(txt:str) -> object:
"""
Deserialize an object from a plain text string.
Args:
txt (str):
The serialized version of the object in the form of a string.
Returns:
An object that was deserialized from a base64 string representation.
"""
base64_bytes = txt.encode('ascii')
message_bytes = base64.b64decode(base64_bytes)
obj = pickle.loads(message_bytes)
return obj
@staticmethod
def serialize_object(obj:object) -> str:
"""
Serialize an object into a plain text string.
Args:
obj (object):
The object to serialize.
Returns:
A serialized base64 string representation of the object.
"""
message_bytes = pickle.dumps(obj)
base64_bytes = base64.b64encode(message_bytes)
txt = base64_bytes.decode('ascii')
return txt
async def async_browse_media_library_index(
hass:HomeAssistant,
client:SpotifyClient,
playerName:str,
source:str|None,
libraryMap:dict,
libraryIndex:BrowsableMedia,
media_content_type:str|None,
media_content_id:str|None,
) -> BrowseMedia:
"""
Builds a BrowseMedia object for the top level index page, and all of it's
child nodes.
Args:
hass (HomeAssistant):
HomeAssistant instance.
client (SpotifyClient):
The SpotifyClient instance that will make calls to the device
to retrieve the data for display in the media browser.
playerName (str):
Name of the media player that is calling this method (for tracing purposes).
source (str):
Currently selected source value.
libraryMap (dict):
The library map that contains media content attributes for each library index entry.
libraryIndex (BrowseMedia):
The library index of media content types.
media_content_type (str):
Selected media content type in the media browser.
This value will be None upon the initial entry to the media browser.
media_content_id (str):
Selected media content id in the media browser.
This value will be None upon the initial entry to the media browser.
"""
methodParms:SIMethodParmListContext = None
try:
# trace.
methodParms = _logsi.EnterMethodParmList(SILevel.Debug)
methodParms.AppendKeyValue("playerName", playerName)
methodParms.AppendKeyValue("source", source)
methodParms.AppendKeyValue("libraryMap", libraryMap)
methodParms.AppendKeyValue("libraryIndex", libraryIndex)
methodParms.AppendKeyValue("media_content_type", media_content_type)
methodParms.AppendKeyValue("media_content_id", media_content_id)
_logsi.LogMethodParmList(SILevel.Verbose, "'%s': browsing for media - top level index: '%s'" % (playerName, libraryIndex), methodParms)
# validations.
if source is None:
source = "unknownSource"
# get parent media atttributes based upon selected media content type.
parentAttrs:dict[str, Any] = libraryMap.get(libraryIndex.value, None)
_logsi.LogDictionary(SILevel.Verbose, "'%s': BrowseMedia attributes for parent media content type: '%s'" % (playerName, libraryIndex.value), parentAttrs)
# create the index.
browseMedia:BrowseMedia = BrowseMedia(
can_expand=True,
can_play=False,
children=[],
children_media_class=parentAttrs["children"],
media_class=parentAttrs["parent"],
media_content_id=libraryIndex.value,
media_content_type=libraryIndex.value,
thumbnail=parentAttrs["image"],
title=parentAttrs["title"],
)
# add child items to the index.
for mediaType, childAttrs in libraryMap.items():
# if not an index item then don't bother.
isIndexItem:bool = childAttrs.get("is_index_item", True)
if not isIndexItem:
continue
# trace.
#_logsi.LogDictionary(SILevel.Verbose, "'%s': BrowseMedia attributes for child media content type: '%s'" % (playerName, mediaType), childAttrs)
# if a LOCAL index image was specified, then ensure it exists.
# otherwise, default to null.
image:str = childAttrs.get("image", None)
if image is not None and image.startswith(LOCAL_IMAGE_PREFIX):
imagePath:str = "%s/www/%s" % (hass.config.config_dir, image[len(LOCAL_IMAGE_PREFIX):])
if not os.path.exists(imagePath):
#_logsi.LogVerbose("'%s': could not find logo image path '%s'; image will be reset to null" % (playerName, imagePath))
image = None
browseMediaChild:BrowseMedia = BrowseMedia(
can_expand=True,
can_play=False,
children=None,
children_media_class=childAttrs["children"],
media_class=childAttrs["parent"],
media_content_id=f"{mediaType}",
media_content_type=f"{mediaType}",
thumbnail=image,
title=childAttrs["title"],
)
browseMedia.children.append(browseMediaChild)
_logsi.LogObject(SILevel.Verbose, "'%s': BrowseMedia Child Object: Type='%s', Id='%s', Title='%s'" % (playerName, browseMediaChild.media_content_type, browseMediaChild.media_content_id, browseMediaChild.title), browseMediaChild)
# trace.
_logsi.LogObject(SILevel.Verbose, "'%s': BrowseMedia Parent Object: Type='%s', Id='%s', Title='%s'" % (playerName, browseMedia.media_content_type, browseMedia.media_content_id, browseMedia.title), browseMedia)
return browseMedia
except Exception as ex:
# trace.
_logsi.LogException("'%s': BrowseMedia async_browse_media_library_index exception: %s" % (playerName, str(ex)), ex, logToSystemLogger=False)
raise IntegrationError(str(ex)) from ex
finally:
# trace.
_logsi.LeaveMethod(SILevel.Debug)
def browse_media_node(
hass:HomeAssistant,
client:SpotifyClient,
playerName:str,
source:str|None,
libraryMap:dict,
media_content_type:str|None,
media_content_id:str|None,
) -> BrowseMedia:
"""
Builds a BrowseMedia object for a selected media content type, and all of it's
child nodes.
Args:
hass (HomeAssistant):
HomeAssistant instance.
client (SpotifyClient):
The SpotifyClient instance that will make calls to the device
to retrieve the data for display in the media browser.
playerName (str):
Name of the media player that is calling this method (for tracing purposes).
source (str):
Currently selected source value.
libraryMap (dict):
The library map that contains media content attributes for each library index entry.
media_content_type (str):
Selected media content type in the media browser.
This value will be None upon the initial entry to the media browser.
media_content_id (str):
Selected media content id in the media browser.
This value will be None upon the initial entry to the media browser.
"""
methodParms:SIMethodParmListContext = None
try:
# trace.
methodParms = _logsi.EnterMethodParmList(SILevel.Debug)
methodParms.AppendKeyValue("playerName", playerName)
methodParms.AppendKeyValue("source", source)
methodParms.AppendKeyValue("libraryMap", libraryMap)
methodParms.AppendKeyValue("media_content_type", media_content_type)
methodParms.AppendKeyValue("media_content_id", media_content_id)
_logsi.LogMethodParmList(SILevel.Verbose, "'%s': browsing for media - selected node: '%s'" % (playerName, media_content_type), methodParms)
# validations.
if source is None:
source = "unknownSource"
# initialize child item attributes.
title:str = None
image:str = None
media:object = None
items:list = []
# build selection list based upon the browsable media type.
# - media: will contain the result of the spotify web api call.
# - items: will contain the child items to display for the parent media item.
# - title: the title to display in the media browser.
# - image: the image (if any) to display in the media browser (can be none).
if media_content_type == BrowsableMedia.SPOTIFY_USER_PLAYLISTS:
_logsi.LogVerbose("'%s': querying spotify for Playlist Favorites" % playerName)
media:PlaylistPageSimplified = client.GetPlaylistFavorites(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
items = media.Items
elif media_content_type == BrowsableMedia.SPOTIFY_USER_FOLLOWED_ARTISTS:
_logsi.LogVerbose("'%s': querying spotify for Artists Followed" % playerName)
media:ArtistPage = client.GetArtistsFollowed(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
items = media.Items
elif media_content_type == BrowsableMedia.SPOTIFY_USER_SAVED_ALBUMS:
_logsi.LogVerbose("Getting Spotify user Album favorites")
media:AlbumPageSaved = client.GetAlbumFavorites(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
items = media.GetAlbums()
elif media_content_type == BrowsableMedia.SPOTIFY_USER_SAVED_TRACKS:
_logsi.LogVerbose("Getting Spotify user Track favorites")
media:TrackPageSaved = client.GetTrackFavorites(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
items = media.GetTracks()
elif media_content_type == BrowsableMedia.SPOTIFY_USER_SAVED_SHOWS:
_logsi.LogVerbose("Getting Spotify user Show favorites")
media:ShowPageSaved = client.GetShowFavorites(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
items = media.GetShows()
elif media_content_type == BrowsableMedia.SPOTIFY_USER_SAVED_AUDIOBOOKS:
_logsi.LogVerbose("Getting Spotify user Audiobook favorites")
media:AudiobookPageSimplified = client.GetAudiobookFavorites(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
items = media.Items
elif media_content_type == BrowsableMedia.SPOTIFY_USER_RECENTLY_PLAYED:
_logsi.LogVerbose("Getting Spotify user Recently Played Tracks")
media:PlayHistoryPage = client.GetPlayerRecentTracks(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
items = media.GetTracks()
elif media_content_type == BrowsableMedia.SPOTIFY_USER_TOP_ARTISTS:
_logsi.LogVerbose("Getting Spotify user Top Artists")
media:ArtistPage = client.GetUsersTopArtists(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
items = media.Items
elif media_content_type == BrowsableMedia.SPOTIFY_USER_TOP_TRACKS:
_logsi.LogVerbose("Getting Spotify user Top Tracks")
media:TrackPage = client.GetUsersTopTracks(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
items = media.Items
elif media_content_type == BrowsableMedia.SPOTIFY_FEATURED_PLAYLISTS:
_logsi.LogVerbose("Getting Spotify Featured Playlists")
media:PlaylistPageSimplified
media, message = client.GetFeaturedPlaylists(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
items = media.Items
elif media_content_type == BrowsableMedia.SPOTIFY_CATEGORYS:
_logsi.LogVerbose("Getting Spotify Categories")
media:list[Category] = client.GetBrowseCategorysList(refresh=False)
items = media
# add a "Uri" attribute to each category in the cached category list.
# this is so we can process categories just like other index types, as
# spotify categories are simply playlists of tracks.
category:Category
for category in client.ConfigurationCache["GetBrowseCategorysList"]:
if hasattr(category, "Uri"):
_logsi.LogVerbose("Category Uri's have already been set in the cache; don't need to do it again")
break
setattr(category, "Uri", f"spotify:category:{category.Id}")
_logsi.LogVerbose("Category Uri set: Name='%s', Id='%s', Uri='%s'" % (category.Name, category.Id, category.Uri))
elif media_content_type == BrowsableMedia.SPOTIFY_CATEGORY_PLAYLISTS:
_logsi.LogVerbose("Getting Spotify Category Playlist")
# was a base64 encoded category object supplied? if not, then it's a problem!
if not media_content_id.startswith(CATEGORY_BASE64):
raise ValueError("'%s': media content type '%s' is not a serialized Category object!" % (playerName, media_content_type))
# drop the eye-ctacher and deserialize the category object.
category:Category = Category()
media_content_id = media_content_id[len(CATEGORY_BASE64):]
category = deserialize_object(media_content_id)
_logsi.LogObject(SILevel.Verbose, "'%s': deserialized %s" % (playerName, category.ToString()), category, excludeNonPublic=True)
media_content_id = category.Id
# get the playlists for the category id.
media:PlaylistPageSimplified
media, message = client.GetCategoryPlaylists(media_content_id, limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
items = media.Items
title = category.Name
image = category.ImageUrl
elif media_content_type == BrowsableMedia.SPOTIFY_CATEGORY_PLAYLISTS_MADEFORYOU:
_logsi.LogVerbose("Getting Spotify 'Made For You' Category Playlist")
media_content_id = '0JQ5DAt0tbjZptfcdMSKl3' # special hidden category "Made For You"
media, message = client.GetCategoryPlaylists(media_content_id, limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
items = media.Items
elif media_content_type == BrowsableMedia.SPOTIFY_NEW_RELEASES:
_logsi.LogVerbose("Getting Spotify Album New Releases")
media:AlbumPageSimplified = client.GetAlbumNewReleases(limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
items = media.Items
elif media_content_type == MediaType.ALBUM:
_logsi.LogVerbose("Getting Spotify Album Tracks")
spotifyId:str = SpotifyClient.GetIdFromUri(media_content_id)
media:Album = client.GetAlbum(spotifyId)
items = media.Tracks.Items
title = media.Name
image = media.ImageUrl
elif media_content_type == MediaType.ARTIST:
_logsi.LogVerbose("Getting Spotify Artist Albums")
spotifyId:str = SpotifyClient.GetIdFromUri(media_content_id)
artist:Artist = client.GetArtist(spotifyId) # for cover image
media:AlbumPageSimplified = client.GetArtistAlbums(spotifyId, include_groups='album', limitTotal=SPOTIFY_BROWSE_LIMIT_TOTAL)
items = media.Items
title = artist.Name
image = artist.ImageUrl
elif media_content_type == MediaType.GENRE:
_logsi.LogVerbose("Getting Spotify Genre Playlist")
spotifyId:str = SpotifyClient.GetIdFromUri(media_content_id)
media:Playlist = client.GetPlaylist(spotifyId)
items = media.GetTracks()
title = media.Name
image = media.ImageUrl
elif media_content_type == MediaType.PLAYLIST:
_logsi.LogVerbose("Getting Spotify Playlist")
spotifyId:str = SpotifyClient.GetIdFromUri(media_content_id)
media:Playlist = client.GetPlaylist(spotifyId)
items = media.GetTracks()
title = media.Name
image = media.ImageUrl
elif media_content_type == MediaType.PODCAST:
_logsi.LogVerbose("Getting Spotify Show / Podcast")
spotifyId:str = SpotifyClient.GetIdFromUri(media_content_id)
media:Show = client.GetShow(spotifyId)
items = media.Episodes.Items
title = media.Name
image = media.ImageUrl
elif media_content_type == MediaType.APP: # spotify audiobook support
_logsi.LogVerbose("Getting Spotify Audiobook")
spotifyId:str = SpotifyClient.GetIdFromUri(media_content_id)
media:Audiobook = client.GetAudiobook(spotifyId)
items = media.Chapters.Items
title = media.Name
image = media.ImageUrl
# if media was not set then we are done.
if media is None:
raise ValueError("'%s': could not find media items for content type '%s'" % (playerName, media_content_type))
# set index flag indicating if index media can be played or not.
canPlay:bool = media_content_type in PLAYABLE_MEDIA_TYPES
# track and episode media items cannot be expanded (only played);
# other media types can be expanded to display child items (e.g. Album, Artist, Playlist, etc).
canExpand = media_content_type not in [
MediaType.TRACK,
MediaType.EPISODE,
]
# if a LOCAL index image was specified, then ensure it exists.
# otherwise, default to null.
if image is not None and image.startswith(LOCAL_IMAGE_PREFIX):
imagePath:str = "%s/www/%s" % (hass.config.config_dir, image[len(LOCAL_IMAGE_PREFIX):])
if not os.path.exists(imagePath):
image = None
# get parent media atttributes based upon selected media content type.
parentAttrs:dict[str, Any] = libraryMap.get(media_content_type, None)
_logsi.LogDictionary(SILevel.Verbose, "'%s': BrowseMedia attributes for parent media content type: '%s'" % (playerName, media_content_type), parentAttrs)
# get parent attributes that are not set.
if title is None:
title = parentAttrs.get("title_node", media_content_id)
# create the index.
browseMedia:BrowseMedia = BrowseMedia(
can_expand=canExpand,
can_play=canPlay,
children=[],
children_media_class=parentAttrs["children"],
media_class=parentAttrs["parent"],
media_content_id=media_content_id,
media_content_type=media_content_type,
thumbnail=image,
title=title,
)
# add child items to the index.
for item in items:
# resolve media content type.
mediaType:str = parentAttrs["children"]
# get child media atttributes based upon child item media content type.
childAttrs:dict[str, Any] = libraryMap.get(mediaType, None)
#_logsi.LogDictionary(SILevel.Verbose, "'%s': BrowseMedia attributes for child media content type: '%s'" % (playerName, mediaType), childAttrs)
# set child flag indicating if media can be played or not.
canPlay:bool = mediaType in PLAYABLE_MEDIA_TYPES
# track and episode media items cannot be expanded (only played);
# other media types can be expanded to display child items (e.g. Album, Artist, Playlist, etc).
canExpand = mediaType not in [
MediaType.TRACK,
MediaType.EPISODE,
]
# resolve media content id.
# default the value to the media type.
mediaId:str = mediaType
if canPlay:
# if it is playable then use the Uri value instead, as we know it's a track or episode.
mediaId = item.Uri
elif mediaType == MediaType.GENRE:
# if it's GENRE content, then serialize the Category object and use it instead so that
# we don't have to go get it again - we will deserialize it when the child node is
# selected, and use it to resolve the category Id, Name, and imageUrl values;
mediaId = "%s%s" % (CATEGORY_BASE64, serialize_object(item))
mediaType = BrowsableMedia.SPOTIFY_CATEGORY_PLAYLISTS.value
# build the child node.
browseMediaChild:BrowseMedia = BrowseMedia(
can_expand=canExpand,
can_play=canPlay,
children=None,
children_media_class=childAttrs["children"],
media_class=childAttrs["parent"],
media_content_id=mediaId,
media_content_type=mediaType,
thumbnail=item.ImageUrl,
title=item.Name,
)
# if parent is an audiobook and we are displaying its track items (e.g. chapters), then
# remove child images as they are all the same.
if media_content_type == MediaType.APP and mediaType == MediaType.TRACK:
browseMediaChild.thumbnail = None
# add the child item to the list of children.
browseMedia.children.append(browseMediaChild)
_logsi.LogObject(SILevel.Verbose, "'%s': BrowseMedia Child Object: Type='%s', Id='%s', Title='%s'" % (playerName, browseMediaChild.media_content_type, browseMediaChild.media_content_id, browseMediaChild.title), browseMediaChild)
# trace.
_logsi.LogObject(SILevel.Verbose, "'%s': BrowseMedia Parent Object: Type='%s', Id='%s', Title='%s'" % (playerName, browseMedia.media_content_type, browseMedia.media_content_id, browseMedia.title), browseMedia)
return browseMedia
except Exception as ex:
# trace.
_logsi.LogException("'%s': BrowseMedia browse_media_node exception: %s" % (playerName, str(ex)), ex, logToSystemLogger=False)
raise IntegrationError(str(ex)) from ex
finally:
# trace.
_logsi.LeaveMethod(SILevel.Debug)