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

238 lines
9.5 KiB
Python

"""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