"""Support for Spotify media searching.""" from __future__ import annotations import logging from spotifywebapipython import SpotifyClient, SpotifyMediaTypes from spotifywebapipython.models import * from homeassistant.components.media_player import ( BrowseMedia, MediaClass, SearchMediaQuery, SearchMedia, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import IntegrationError # get smartinspect logger reference; create a new session for this module name. from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SIColors import logging _logsi:SISession = SIAuto.Si.GetSession(__name__) if (_logsi == None): _logsi = SIAuto.Si.AddSession(__name__, True) _logsi.SystemLogger = logging.getLogger(__name__) SPOTIFY_SEARCH_LIMIT_TOTAL = 10 """ Max number of items to return (for each type) from a search request (5). """ def search_media_node( hass:HomeAssistant, client:SpotifyClient, playerName:str, query: SearchMediaQuery, ) -> SearchMedia: """ Searches the Spotify catalog for the requested criteria, and returns a list of BrowseMedia items with the results. 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). query (SearchMediaQuery): Search media query criteria to search for. Note that `media_filter_classes` can also be passed as a dictionary of values, which differs from the method signature which indicates a `list[MediaClass]`. Returns: A `SearchMedia` object that contains the search results. """ methodParms:SIMethodParmListContext = None try: # trace. methodParms = _logsi.EnterMethodParmList(SILevel.Debug) methodParms.AppendKeyValue("playerName", playerName) methodParms.AppendKeyValue("query", query) methodParms.AppendKeyValue("query.media_content_id", query.media_content_id) methodParms.AppendKeyValue("query.media_content_type", query.media_content_type) methodParms.AppendKeyValue("query.media_filter_classes", query.media_filter_classes) methodParms.AppendKeyValue("query.search_query", query.search_query) _logsi.LogMethodParmList(SILevel.Verbose, "'%s': Preparing to search for media" % (playerName), methodParms) result:list[BrowseMedia] = [] # default search criteria type (comma-delimited string of criteria types). criteriaType:str = None # will default to "track" # was query criteria specified? if (query): # was media filter class(es) specified? if (query.media_filter_classes): _logsi.LogVerbose("'%s': Filtering by media filter class: \"%s\"" % (playerName, str(query.media_filter_classes))) # create a comma-delimited string of criteria types. mapping = { MediaClass.ARTIST: SpotifyMediaTypes.ARTIST.value, MediaClass.ALBUM: SpotifyMediaTypes.ALBUM.value, MediaClass.TRACK: SpotifyMediaTypes.TRACK.value, MediaClass.MUSIC: SpotifyMediaTypes.TRACK.value, MediaClass.PLAYLIST: SpotifyMediaTypes.PLAYLIST.value, MediaClass.APP: SpotifyMediaTypes.AUDIOBOOK.value, MediaClass.PODCAST: SpotifyMediaTypes.SHOW.value, MediaClass.EPISODE: SpotifyMediaTypes.EPISODE.value, } media_types = [ mapping[cls] for cls in query.media_filter_classes if cls in mapping ] criteriaType = ','.join(media_types) # comma-delimited string of criteria types # was a media content type specified? # note that this integration does not use media content type, but rather media filter classes. # some generic media players use media content type though, so support it. elif (query.media_content_type): _logsi.LogVerbose("'%s': Filtering by media content type: \"%s\"" % (playerName, str(query.media_content_type))) criteriaType = query.media_content_type # change default "music" to "track". if (isinstance(criteriaType,str)): criteriaType = criteriaType.replace("music","track") criteriaType = criteriaType.replace("MUSIC","track") # search spotify. _logsi.LogVerbose("'%s': Searching for media: \"%s\"" % (playerName, query.search_query)) searchResp:SearchResponse = client.Search(query.search_query, criteriaType, limitTotal=SPOTIFY_SEARCH_LIMIT_TOTAL) # add search results for all media types. _ProcessFoundItems(result, SpotifyMediaTypes.ALBUM, searchResp.Albums.Items) _ProcessFoundItems(result, SpotifyMediaTypes.ARTIST, searchResp.Artists.Items) _ProcessFoundItems(result, SpotifyMediaTypes.AUDIOBOOK, searchResp.Audiobooks.Items) _ProcessFoundItems(result, SpotifyMediaTypes.EPISODE, searchResp.Episodes.Items) _ProcessFoundItems(result, SpotifyMediaTypes.PLAYLIST, searchResp.Playlists.Items) _ProcessFoundItems(result, SpotifyMediaTypes.SHOW, searchResp.Shows.Items) _ProcessFoundItems(result, SpotifyMediaTypes.TRACK, searchResp.Tracks.Items) # return search results. return SearchMedia(result=result) except Exception as ex: # trace. _logsi.LogException("'%s': SearchMedia search_media_node exception: %s" % (playerName, str(ex)), ex, logToSystemLogger=False) raise IntegrationError(str(ex)) from ex finally: # trace. _logsi.LeaveMethod(SILevel.Debug) def _ProcessFoundItems( result:list[BrowseMedia], spotifyMediaType:SpotifyMediaTypes, items:list[SearchResultBase], ) -> None: """ Builds a BrowseMedia object for each search result returned, and appends them to the result collection. Args: result (list[BrowseMedia]): List of BrowseMedia items to append results to. spotifyMediaType (str): Spotify media type. items (list[SearchResultBase]): List of matching search items found. """ # 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:bool = spotifyMediaType not in [ SpotifyMediaTypes.TRACK, SpotifyMediaTypes.EPISODE, ] # get HA media class for Spotify media type. mediaClass:str = _GetMediaClassFromSpotifyMediaType(spotifyMediaType) or MediaClass.DIRECTORY # process found items. item:SearchResultBase for item in items: # build BrowseMedia object for found item. browseMedia:BrowseMedia = BrowseMedia( can_expand=canExpand, can_play=True, children=None, children_media_class=None, media_class=mediaClass, media_content_id=item.Uri, media_content_type=item.Type, thumbnail=item.ImageUrl, title=item.Name ) # trace. _logsi.LogObject(SILevel.Verbose, "Search BrowseMedia Object: Type='%s', Id='%s', Title='%s'" % (browseMedia.media_content_type, browseMedia.media_content_id, browseMedia.title), browseMedia) # append object to results. result.append(browseMedia) def _GetMediaClassFromSpotifyMediaType(media_type:str) -> MediaClass|None: """ Get the appropriate HA media class for a given Spotify media type. """ result:MediaClass = None if (media_type == SpotifyMediaTypes.TRACK.value): result = MediaClass.TRACK elif (media_type == SpotifyMediaTypes.ALBUM.value): result = MediaClass.ALBUM elif (media_type == SpotifyMediaTypes.ARTIST.value): result = MediaClass.ARTIST elif (media_type == SpotifyMediaTypes.PLAYLIST.value): result = MediaClass.PLAYLIST elif (media_type == SpotifyMediaTypes.SHOW.value): result = MediaClass.PODCAST elif (media_type == SpotifyMediaTypes.AUDIOBOOK.value): result = MediaClass.APP elif (media_type == SpotifyMediaTypes.EPISODE.value): result = MediaClass.EPISODE return result def _GetSpotifyMediaTypeFromMediaClass(media_class:str|MediaClass) -> SpotifyMediaTypes|None: """ Get the appropriate Spotify media type for a given HA media class. """ result:SpotifyMediaTypes = None media_class_str = str(media_class) if (media_class_str == MediaClass.ALBUM.value): result = SpotifyMediaTypes.ALBUM elif (media_class_str == MediaClass.ARTIST.value): result = SpotifyMediaTypes.ARTIST elif (media_class_str == MediaClass.APP.value): result = SpotifyMediaTypes.AUDIOBOOK elif (media_class_str == MediaClass.EPISODE.value): result = SpotifyMediaTypes.EPISODE.value elif (media_class_str == MediaClass.PLAYLIST.value): result = SpotifyMediaTypes.PLAYLIST elif (media_class_str == MediaClass.PODCAST.value): result = SpotifyMediaTypes.SHOW elif (media_class_str == MediaClass.TRACK.value): result = SpotifyMediaTypes.TRACK return result