1607 lines
71 KiB
Python
1607 lines
71 KiB
Python
import voluptuous as vol
|
|
|
|
from homeassistant.components.media_player import MediaPlayerEntityFeature
|
|
from homeassistant.const import (
|
|
STATE_IDLE,
|
|
STATE_OFF,
|
|
STATE_ON,
|
|
STATE_PAUSED,
|
|
STATE_PLAYING,
|
|
STATE_UNKNOWN,
|
|
)
|
|
from homeassistant.core import State, ServiceResponse
|
|
from homeassistant.helpers import config_validation as cv
|
|
from homeassistant.helpers.intent import (
|
|
Intent,
|
|
IntentResponse,
|
|
IntentResponseErrorCode,
|
|
)
|
|
|
|
from smartinspectpython.siauto import SILevel, SIColors
|
|
|
|
from spotifywebapipython import SpotifyMediaTypes
|
|
|
|
from ..appmessages import STAppMessages
|
|
from ..intent_loader import IntentLoader
|
|
from ..utils import get_id_from_uri
|
|
from ..const import (
|
|
CONF_LIMIT_TOTAL_MINIMUM,
|
|
CONF_TEXT,
|
|
CONF_VALUE,
|
|
DOMAIN,
|
|
INTENT_SEARCH_PLAY_CONTROL,
|
|
PLATFORM_SPOTIFYPLUS,
|
|
RESPONSE_ERROR_SEARCH_NO_CRITERIA,
|
|
RESPONSE_ERROR_SEARCH_PLAY_CONTROL_INVALID,
|
|
RESPONSE_PLAY_ALBUM,
|
|
RESPONSE_PLAY_ALBUM_WITH_ARTIST,
|
|
RESPONSE_PLAY_AUDIOBOOK,
|
|
RESPONSE_PLAY_AUDIOBOOK_WITH_AUTHOR,
|
|
RESPONSE_PLAY_FAVORITE_TRACKS,
|
|
RESPONSE_PLAY_FAVORITE_TRACKS_FOR_ARTIST,
|
|
RESPONSE_PLAY_PLAYLIST,
|
|
RESPONSE_PLAY_PODCAST,
|
|
RESPONSE_PLAY_PODCAST_EPISODE,
|
|
RESPONSE_PLAY_TRACK,
|
|
RESPONSE_PLAY_TRACK_WITH_ARTIST,
|
|
RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_ALBUM,
|
|
RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_AUDIOBOOK,
|
|
RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_PLAYLIST,
|
|
RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_PODCAST,
|
|
RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_PODCAST_EPISODE,
|
|
RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_TRACK,
|
|
SERVICE_SPOTIFY_GET_EPISODE,
|
|
SERVICE_SPOTIFY_GET_SHOW_EPISODES,
|
|
SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_CONTEXT,
|
|
SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_TRACKS,
|
|
SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_TRACK_FAVORITES,
|
|
SERVICE_SPOTIFY_SEARCH_ALBUMS,
|
|
SERVICE_SPOTIFY_SEARCH_AUDIOBOOKS,
|
|
SERVICE_SPOTIFY_SEARCH_EPISODES,
|
|
SERVICE_SPOTIFY_SEARCH_PLAYLISTS,
|
|
SERVICE_SPOTIFY_SEARCH_SHOWS,
|
|
SERVICE_SPOTIFY_SEARCH_TRACKS,
|
|
SLOT_ALBUM_TITLE,
|
|
SLOT_ALBUM_URL,
|
|
SLOT_AREA,
|
|
SLOT_ARTIST_TITLE,
|
|
SLOT_ARTIST_URL,
|
|
SLOT_AUDIOBOOK_TITLE,
|
|
SLOT_AUDIOBOOK_URL,
|
|
SLOT_AUTHOR_TITLE,
|
|
SLOT_CRITERIA_ARG_1,
|
|
SLOT_CRITERIA_ARG_2,
|
|
SLOT_DELAY,
|
|
SLOT_DEVICE_NAME,
|
|
SLOT_EPISODE_TITLE,
|
|
SLOT_EPISODE_URL,
|
|
SLOT_FLOOR,
|
|
SLOT_LATEST_EPISODE,
|
|
SLOT_LIMIT_TOTAL,
|
|
SLOT_NAME,
|
|
SLOT_PLAYLIST_TITLE,
|
|
SLOT_PLAYLIST_URL,
|
|
SLOT_PLAYER_SHUFFLE_MODE,
|
|
SLOT_PODCAST_TITLE,
|
|
SLOT_PODCAST_URL,
|
|
SLOT_PREFERRED_AREA_ID,
|
|
SLOT_PREFERRED_FLOOR_ID,
|
|
SLOT_SEARCH_CRITERIA,
|
|
SLOT_SPOTIFYPLUS_SEARCH_PLAY_CONTROL,
|
|
SLOT_SPOTIFYPLUS_SEARCH_PLAY_CONTROL_MULTI,
|
|
SLOT_SPOTIFYPLUS_SEARCH_PLAY_CONTROL_SINGLE,
|
|
SLOT_TRACK_TITLE,
|
|
SLOT_TRACK_URL,
|
|
SPOTIFY_WEB_URL_PFX,
|
|
)
|
|
|
|
from .spotifyplusintenthandler import SpotifyPlusIntentHandler
|
|
|
|
|
|
class SpotifyPlusSearchPlayControl_Handler(SpotifyPlusIntentHandler):
|
|
"""
|
|
Handles intents for SpotifyPlusSearchPlayControl.
|
|
"""
|
|
def __init__(self, intentLoader:IntentLoader) -> None:
|
|
"""
|
|
Initializes a new instance of the IntentHandler class.
|
|
"""
|
|
# invoke base class method.
|
|
super().__init__(intentLoader)
|
|
|
|
# set intent handler basics.
|
|
self.description = "Controls media search and play functions."
|
|
self.intent_type = INTENT_SEARCH_PLAY_CONTROL
|
|
self.platforms = {PLATFORM_SPOTIFYPLUS}
|
|
|
|
|
|
@property
|
|
def slot_schema(self) -> dict | None:
|
|
"""
|
|
Returns the slot schema for this intent.
|
|
"""
|
|
return {
|
|
|
|
# slots that determine which media player entity will be used.
|
|
vol.Optional(SLOT_NAME): cv.string,
|
|
vol.Optional(SLOT_AREA): cv.string,
|
|
vol.Optional(SLOT_FLOOR): cv.string,
|
|
vol.Optional(SLOT_PREFERRED_AREA_ID): cv.string,
|
|
vol.Optional(SLOT_PREFERRED_FLOOR_ID): cv.string,
|
|
|
|
# slots for other service arguments.
|
|
vol.Optional(SLOT_SPOTIFYPLUS_SEARCH_PLAY_CONTROL_MULTI): cv.string,
|
|
vol.Optional(SLOT_SPOTIFYPLUS_SEARCH_PLAY_CONTROL_SINGLE): cv.string,
|
|
vol.Optional(SLOT_DELAY, default=0.50): vol.Any(None, vol.All(vol.Coerce(float), vol.Range(min=0, max=10.0))),
|
|
vol.Optional(SLOT_DEVICE_NAME): cv.string,
|
|
vol.Optional(SLOT_CRITERIA_ARG_1): cv.string,
|
|
vol.Optional(SLOT_CRITERIA_ARG_2): cv.string,
|
|
vol.Optional(SLOT_PLAYER_SHUFFLE_MODE): cv.string,
|
|
}
|
|
|
|
|
|
async def async_HandleIntent(
|
|
self,
|
|
intentObj: Intent,
|
|
intentResponse: IntentResponse
|
|
) -> IntentResponse:
|
|
"""
|
|
Handles the intent.
|
|
|
|
Args:
|
|
intentObj (Intent):
|
|
Intent object.
|
|
intentResponse (IntentResponse)
|
|
Intent response object.
|
|
|
|
Returns:
|
|
An IntentResponse object.
|
|
"""
|
|
# get optional arguments (if provided).
|
|
search_play_control_text:str = intentObj.slots.get(SLOT_SPOTIFYPLUS_SEARCH_PLAY_CONTROL_SINGLE, {}).get(CONF_TEXT, None)
|
|
search_play_control:str = intentObj.slots.get(SLOT_SPOTIFYPLUS_SEARCH_PLAY_CONTROL_SINGLE, {}).get(CONF_VALUE, None)
|
|
if (search_play_control is None):
|
|
search_play_control_text:str = intentObj.slots.get(SLOT_SPOTIFYPLUS_SEARCH_PLAY_CONTROL_MULTI, {}).get(CONF_TEXT, None)
|
|
search_play_control:str = intentObj.slots.get(SLOT_SPOTIFYPLUS_SEARCH_PLAY_CONTROL_MULTI, {}).get(CONF_VALUE, None)
|
|
|
|
# update slot with returned info.
|
|
intentObj.slots[SLOT_SPOTIFYPLUS_SEARCH_PLAY_CONTROL] = { CONF_VALUE: search_play_control, CONF_TEXT: search_play_control_text }
|
|
|
|
# process based on media type.
|
|
if (search_play_control == "artist_album"):
|
|
return await self.async_ProcessArtistAlbum(intentObj, intentResponse)
|
|
|
|
elif (search_play_control == "artist_track"):
|
|
return await self.async_ProcessArtistTrack(intentObj, intentResponse)
|
|
|
|
elif (search_play_control == "album"):
|
|
return await self.async_ProcessAlbum(intentObj, intentResponse)
|
|
|
|
elif (search_play_control == "audiobook"):
|
|
return await self.async_ProcessAudiobook(intentObj, intentResponse)
|
|
|
|
elif (search_play_control == "playlist"):
|
|
return await self.async_ProcessPlaylist(intentObj, intentResponse)
|
|
|
|
elif (search_play_control == "podcast"):
|
|
return await self.async_ProcessPodcast(intentObj, intentResponse)
|
|
|
|
elif (search_play_control == "podcast_episode"):
|
|
return await self.async_ProcessPodcastEpisode(intentObj, intentResponse)
|
|
|
|
elif (search_play_control == "track"):
|
|
return await self.async_ProcessTrack(intentObj, intentResponse)
|
|
|
|
elif (search_play_control in ["favorite_tracks","favorite_tracks_artist"]):
|
|
return await self.async_ProcessFavoriteTracks(intentObj, intentResponse)
|
|
|
|
else:
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_ERROR_SEARCH_PLAY_CONTROL_INVALID, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
|
|
|
|
|
async def async_ProcessAlbum(
|
|
self,
|
|
intentObj: Intent,
|
|
intentResponse: IntentResponse,
|
|
) -> IntentResponse:
|
|
"""
|
|
Handles the intent.
|
|
|
|
Args:
|
|
intentObj (Intent):
|
|
Intent object.
|
|
intentResponse (IntentResponse)
|
|
Intent response object.
|
|
|
|
Returns:
|
|
An IntentResponse object.
|
|
"""
|
|
try:
|
|
|
|
# trace.
|
|
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
# invoke base class method to resolve the player entity and its state.
|
|
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
|
intentObj,
|
|
intentResponse,
|
|
desiredFeatures=None,
|
|
desiredStates=None,
|
|
desiredStateResponseKey=None,
|
|
requiresSpotifyPremium=True,
|
|
)
|
|
|
|
# if media player was not resolved, then we are done;
|
|
# note that the base class method above already called `async_set_speech` with a response.
|
|
if playerEntityState is None:
|
|
return intentResponse
|
|
|
|
# get optional arguments (if provided).
|
|
album_name = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_TEXT, None)
|
|
album_uri = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_VALUE, None)
|
|
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
|
device_name = intentObj.slots.get(SLOT_DEVICE_NAME, {}).get(CONF_VALUE, None)
|
|
player_shuffle_mode = intentObj.slots.get(SLOT_PLAYER_SHUFFLE_MODE, {}).get(CONF_VALUE, "on")
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_ALBUM_TITLE] = { CONF_VALUE: album_uri, CONF_TEXT: album_name }
|
|
|
|
# was a uri supplied?
|
|
if (album_uri) and (album_uri.startswith(f"spotify:{SpotifyMediaTypes.ALBUM.value}")):
|
|
|
|
# if a uri was supplied, then it's from a pre-configured list of values; in this case,
|
|
# we will bypass the search since it's (assumed to be) a valid uri value.
|
|
|
|
# get id portion of spotify uri value.
|
|
album_id:str = get_id_from_uri(album_uri)
|
|
|
|
# load returned info that we care about.
|
|
album_title = album_name
|
|
album_url = f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.ALBUM.value}/{album_id}"
|
|
|
|
# artist variables are unknwon, since only the album name / uri were supplied.
|
|
artist_name = None
|
|
artist_title = artist_name
|
|
artist_url = SPOTIFY_WEB_URL_PFX
|
|
artist_uri = None
|
|
|
|
else:
|
|
|
|
# if no search criteria then we are done.
|
|
if (album_name is None):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_ERROR_SEARCH_NO_CRITERIA, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_SEARCH_ALBUMS
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"criteria": album_name,
|
|
"limit_total": CONF_LIMIT_TOTAL_MINIMUM,
|
|
"include_external": "audio"
|
|
}
|
|
|
|
# search spotify catalog for matching track name.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
search_result:ServiceResponse = await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=True,
|
|
)
|
|
self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_SEARCH_ALBUMS result", search_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
|
|
|
# if no matching items, then return appropriate response.
|
|
items_count:int = search_result.get("result",{}).get("items_count", 0)
|
|
if (items_count == 0):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_TRACK)
|
|
|
|
# load returned info that we care about.
|
|
album_title:str = search_result.get("result",{}).get("items")[0].get("name", "unknown")
|
|
album_uri:str = search_result.get("result",{}).get("items")[0].get("uri", "unknown")
|
|
album_url:str = search_result.get("result",{}).get("items")[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
|
|
|
artist_name:str = search_result.get("result",{}).get("items")[0].get("artists")[0].get("name", "unknown")
|
|
artist_title:str = search_result.get("result",{}).get("items")[0].get("artists")[0].get("name", "unknown")
|
|
artist_uri:str = search_result.get("result",{}).get("items")[0].get("artists")[0].get("uri", "unknown")
|
|
artist_url:str = search_result.get("result",{}).get("items")[0].get("artists")[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_ARTIST_TITLE] = { CONF_VALUE: artist_uri, CONF_TEXT: artist_title }
|
|
intentObj.slots[SLOT_ARTIST_URL] = { CONF_VALUE: artist_url, CONF_TEXT: "Spotify" }
|
|
intentObj.slots[SLOT_ALBUM_TITLE] = { CONF_VALUE: album_uri, CONF_TEXT: album_title }
|
|
intentObj.slots[SLOT_ALBUM_URL] = { CONF_VALUE: album_url, CONF_TEXT: "Spotify" }
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_CONTEXT
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"context_uri": album_uri,
|
|
"shuffle": True if player_shuffle_mode == "on" else False,
|
|
"device_id": device_name,
|
|
"delay": delay,
|
|
}
|
|
|
|
# play the media.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=False,
|
|
)
|
|
|
|
# set appropriate response.
|
|
responseKey = RESPONSE_PLAY_ALBUM_WITH_ARTIST
|
|
if (artist_name is None):
|
|
responseKey = RESPONSE_PLAY_ALBUM
|
|
|
|
# return intent response.
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, responseKey)
|
|
|
|
finally:
|
|
|
|
# trace.
|
|
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
|
|
async def async_ProcessArtistAlbum(
|
|
self,
|
|
intentObj: Intent,
|
|
intentResponse: IntentResponse,
|
|
) -> IntentResponse:
|
|
"""
|
|
Handles the intent.
|
|
|
|
Args:
|
|
intentObj (Intent):
|
|
Intent object.
|
|
intentResponse (IntentResponse)
|
|
Intent response object.
|
|
|
|
Returns:
|
|
An IntentResponse object.
|
|
"""
|
|
try:
|
|
|
|
# trace.
|
|
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
# invoke base class method to resolve the player entity and its state.
|
|
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
|
intentObj,
|
|
intentResponse,
|
|
desiredFeatures=None,
|
|
desiredStates=None,
|
|
desiredStateResponseKey=None,
|
|
requiresSpotifyPremium=True,
|
|
)
|
|
|
|
# if media player was not resolved, then we are done;
|
|
# note that the base class method above already called `async_set_speech` with a response.
|
|
if playerEntityState is None:
|
|
return intentResponse
|
|
|
|
# get optional arguments (if provided).
|
|
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
|
device_name = intentObj.slots.get(SLOT_DEVICE_NAME, {}).get(CONF_VALUE, None)
|
|
album_name = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_TEXT, None)
|
|
album_uri = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_VALUE, None)
|
|
artist_name = intentObj.slots.get(SLOT_CRITERIA_ARG_2, {}).get(CONF_TEXT, None)
|
|
artist_uri = intentObj.slots.get(SLOT_CRITERIA_ARG_2, {}).get(CONF_VALUE, None)
|
|
player_shuffle_mode = intentObj.slots.get(SLOT_PLAYER_SHUFFLE_MODE, {}).get(CONF_VALUE, "on")
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_ARTIST_TITLE] = { CONF_VALUE: artist_uri, CONF_TEXT: artist_name }
|
|
intentObj.slots[SLOT_ALBUM_TITLE] = { CONF_VALUE: album_uri, CONF_TEXT: album_name }
|
|
|
|
# was a uri supplied?
|
|
if (album_uri) and (album_uri.startswith(f"spotify:{SpotifyMediaTypes.ALBUM.value}")):
|
|
|
|
# if a uri was supplied, then it's from a pre-configured list of values; in this case,
|
|
# we will bypass the search since it's (assumed to be) a valid uri value.
|
|
|
|
# get id portion of spotify uri value.
|
|
album_id:str = get_id_from_uri(album_uri)
|
|
|
|
# load returned info that we care about.
|
|
album_title = album_name
|
|
album_url = f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.ALBUM.value}/{album_id}"
|
|
|
|
# artist variables are unknwon, since only the album name / uri were supplied.
|
|
artist_title = artist_name
|
|
artist_url = SPOTIFY_WEB_URL_PFX
|
|
|
|
else:
|
|
|
|
# if no search criteria then we are done.
|
|
if (album_name is None) and (artist_name is None):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_ERROR_SEARCH_NO_CRITERIA, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
|
|
|
# format criteria if searching by artist.
|
|
artist_criteria = ""
|
|
if artist_name is not None:
|
|
artist_criteria = f" artist:{artist_name}"
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_SEARCH_ALBUMS
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"criteria": album_name + artist_criteria,
|
|
"limit_total": CONF_LIMIT_TOTAL_MINIMUM,
|
|
"include_external": "audio"
|
|
}
|
|
|
|
# update slots with search criteria.
|
|
intentObj.slots[SLOT_SEARCH_CRITERIA] = { CONF_VALUE: "", CONF_TEXT: svcData["criteria"] }
|
|
|
|
# search spotify catalog for matching album name.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
search_result:ServiceResponse = await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=True,
|
|
)
|
|
self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_SEARCH_ALBUMS result", search_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
|
|
|
# if no matching items, then return appropriate response.
|
|
items_count:int = search_result.get("result",{}).get("items_count", 0)
|
|
if (items_count == 0):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_ALBUM)
|
|
|
|
# load returned info that we care about.
|
|
album_title:str = search_result.get("result",{}).get("items")[0].get("name", "unknown")
|
|
album_uri:str = search_result.get("result",{}).get("items")[0].get("uri", "unknown")
|
|
album_url:str = search_result.get("result",{}).get("items")[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
|
|
|
artist_title:str = search_result.get("result",{}).get("items")[0].get("artists")[0].get("name", "unknown")
|
|
artist_uri:str = search_result.get("result",{}).get("items")[0].get("artists")[0].get("uri", "unknown")
|
|
artist_url:str = search_result.get("result",{}).get("items")[0].get("artists")[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_ARTIST_TITLE] = { CONF_VALUE: artist_uri, CONF_TEXT: artist_title }
|
|
intentObj.slots[SLOT_ARTIST_URL] = { CONF_VALUE: artist_url, CONF_TEXT: "Spotify" }
|
|
intentObj.slots[SLOT_ALBUM_TITLE] = { CONF_VALUE: album_uri, CONF_TEXT: album_title }
|
|
intentObj.slots[SLOT_ALBUM_URL] = { CONF_VALUE: album_url, CONF_TEXT: "Spotify" }
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_CONTEXT
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"context_uri": album_uri,
|
|
"shuffle": True if player_shuffle_mode == "on" else False,
|
|
"device_id": device_name,
|
|
"delay": delay,
|
|
}
|
|
|
|
# play the media.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=False,
|
|
)
|
|
|
|
# set appropriate response.
|
|
responseKey = RESPONSE_PLAY_ALBUM_WITH_ARTIST
|
|
if (artist_name is None):
|
|
responseKey = RESPONSE_PLAY_ALBUM
|
|
|
|
# return intent response.
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, responseKey)
|
|
|
|
finally:
|
|
|
|
# trace.
|
|
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
|
|
async def async_ProcessArtistTrack(
|
|
self,
|
|
intentObj: Intent,
|
|
intentResponse: IntentResponse,
|
|
) -> IntentResponse:
|
|
"""
|
|
Handles the intent.
|
|
|
|
Args:
|
|
intentObj (Intent):
|
|
Intent object.
|
|
intentResponse (IntentResponse)
|
|
Intent response object.
|
|
|
|
Returns:
|
|
An IntentResponse object.
|
|
"""
|
|
try:
|
|
|
|
# trace.
|
|
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
# invoke base class method to resolve the player entity and its state.
|
|
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
|
intentObj,
|
|
intentResponse,
|
|
desiredFeatures=None,
|
|
desiredStates=None,
|
|
desiredStateResponseKey=None,
|
|
requiresSpotifyPremium=True,
|
|
)
|
|
|
|
# if media player was not resolved, then we are done;
|
|
# note that the base class method above already called `async_set_speech` with a response.
|
|
if playerEntityState is None:
|
|
return intentResponse
|
|
|
|
# get optional arguments (if provided).
|
|
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
|
device_name = intentObj.slots.get(SLOT_DEVICE_NAME, {}).get(CONF_VALUE, None)
|
|
artist_name = intentObj.slots.get(SLOT_CRITERIA_ARG_2, {}).get(CONF_TEXT, None)
|
|
artist_uri = intentObj.slots.get(SLOT_CRITERIA_ARG_2, {}).get(CONF_VALUE, None)
|
|
track_name = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_TEXT, None)
|
|
track_uri = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_VALUE, None)
|
|
player_shuffle_mode = intentObj.slots.get(SLOT_PLAYER_SHUFFLE_MODE, {}).get(CONF_VALUE, "on")
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_ARTIST_TITLE] = { CONF_VALUE: artist_uri, CONF_TEXT: artist_name }
|
|
intentObj.slots[SLOT_TRACK_TITLE] = { CONF_VALUE: track_uri, CONF_TEXT: track_name }
|
|
|
|
# was a uri supplied?
|
|
if (track_uri) and (track_uri.startswith(f"spotify:{SpotifyMediaTypes.TRACK.value}")):
|
|
|
|
# if a uri was supplied, then it's from a pre-configured list of values; in this case,
|
|
# we will bypass the search since it's (assumed to be) a valid uri value.
|
|
|
|
# get id portion of spotify uri value.
|
|
track_id:str = get_id_from_uri(track_uri)
|
|
|
|
# load returned info that we care about.
|
|
track_title = track_name
|
|
track_url = f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.TRACK.value}/{track_id}"
|
|
|
|
# artist variables are unknwon, since only the track name / uri were supplied.
|
|
artist_title = artist_name
|
|
artist_url = SPOTIFY_WEB_URL_PFX
|
|
|
|
else:
|
|
|
|
# if no search criteria then we are done.
|
|
if (track_name is None) and (artist_name is None):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_ERROR_SEARCH_NO_CRITERIA, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
|
|
|
# format criteria if searching by artist.
|
|
artist_criteria = ""
|
|
if artist_name is not None:
|
|
artist_criteria = f" artist:{artist_name}"
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_SEARCH_TRACKS
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"criteria": track_name + artist_criteria,
|
|
"limit_total": CONF_LIMIT_TOTAL_MINIMUM,
|
|
"include_external": "audio"
|
|
}
|
|
|
|
# update slots with search criteria.
|
|
intentObj.slots[SLOT_SEARCH_CRITERIA] = { CONF_VALUE: "", CONF_TEXT: svcData["criteria"] }
|
|
|
|
# search spotify catalog for matching track name.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
search_result:ServiceResponse = await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=True,
|
|
)
|
|
self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_SEARCH_TRACKS result", search_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
|
|
|
# if no matching items, then return appropriate response.
|
|
items_count:int = search_result.get("result",{}).get("items_count", 0)
|
|
if (items_count == 0):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_TRACK)
|
|
|
|
# load returned info that we care about.
|
|
track_title:str = search_result.get("result",{}).get("items")[0].get("name", "unknown")
|
|
track_uri:str = search_result.get("result",{}).get("items")[0].get("uri", "unknown")
|
|
track_url:str = search_result.get("result",{}).get("items")[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
|
|
|
artist_title:str = search_result.get("result",{}).get("items")[0].get("artists")[0].get("name", "unknown")
|
|
artist_uri:str = search_result.get("result",{}).get("items")[0].get("artists")[0].get("uri", "unknown")
|
|
artist_url:str = search_result.get("result",{}).get("items")[0].get("artists")[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_ARTIST_TITLE] = { CONF_VALUE: artist_uri, CONF_TEXT: artist_title }
|
|
intentObj.slots[SLOT_ARTIST_URL] = { CONF_VALUE: artist_url, CONF_TEXT: "Spotify" }
|
|
intentObj.slots[SLOT_TRACK_TITLE] = { CONF_VALUE: track_uri, CONF_TEXT: track_title }
|
|
intentObj.slots[SLOT_TRACK_URL] = { CONF_VALUE: track_url, CONF_TEXT: "Spotify" }
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_TRACKS
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"uris": track_uri,
|
|
"shuffle": True if player_shuffle_mode == "on" else False,
|
|
"device_id": device_name,
|
|
"delay": delay,
|
|
}
|
|
|
|
# play the media.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=False,
|
|
)
|
|
|
|
# set appropriate response.
|
|
responseKey = RESPONSE_PLAY_TRACK_WITH_ARTIST
|
|
if (artist_name is None):
|
|
responseKey = RESPONSE_PLAY_TRACK
|
|
|
|
# return intent response.
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, responseKey)
|
|
|
|
finally:
|
|
|
|
# trace.
|
|
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
|
|
async def async_ProcessAudiobook(
|
|
self,
|
|
intentObj: Intent,
|
|
intentResponse: IntentResponse,
|
|
) -> IntentResponse:
|
|
"""
|
|
Handles the intent.
|
|
|
|
Args:
|
|
intentObj (Intent):
|
|
Intent object.
|
|
intentResponse (IntentResponse)
|
|
Intent response object.
|
|
|
|
Returns:
|
|
An IntentResponse object.
|
|
"""
|
|
try:
|
|
|
|
# trace.
|
|
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
# invoke base class method to resolve the player entity and its state.
|
|
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
|
intentObj,
|
|
intentResponse,
|
|
desiredFeatures=None,
|
|
desiredStates=None,
|
|
desiredStateResponseKey=None,
|
|
requiresSpotifyPremium=True,
|
|
)
|
|
|
|
# if media player was not resolved, then we are done;
|
|
# note that the base class method above already called `async_set_speech` with a response.
|
|
if playerEntityState is None:
|
|
return intentResponse
|
|
|
|
# get optional arguments (if provided).
|
|
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
|
device_name = intentObj.slots.get(SLOT_DEVICE_NAME, {}).get(CONF_VALUE, None)
|
|
audiobook_name = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_TEXT, None)
|
|
audiobook_uri = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_VALUE, None)
|
|
player_shuffle_mode = intentObj.slots.get(SLOT_PLAYER_SHUFFLE_MODE, {}).get(CONF_VALUE, "on")
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_AUDIOBOOK_TITLE] = { CONF_VALUE: audiobook_uri, CONF_TEXT: audiobook_name }
|
|
|
|
# was a uri supplied?
|
|
if (audiobook_uri) and (audiobook_uri.startswith(f"spotify:{SpotifyMediaTypes.SHOW.value}")):
|
|
|
|
# if a uri was supplied, then it's from a pre-configured list of values; in this case,
|
|
# we will bypass the search since it's (assumed to be) a valid uri value.
|
|
|
|
# get id portion of spotify uri value.
|
|
audiobook_id:str = get_id_from_uri(audiobook_uri)
|
|
|
|
# load returned info that we care about.
|
|
audiobook_title = audiobook_name
|
|
audiobook_url = f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.SHOW.value}/{audiobook_id}"
|
|
author_title = None
|
|
|
|
else:
|
|
|
|
# if no search criteria then we are done.
|
|
if (audiobook_name is None):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_ERROR_SEARCH_NO_CRITERIA, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_SEARCH_AUDIOBOOKS
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"criteria": audiobook_name,
|
|
"limit_total": CONF_LIMIT_TOTAL_MINIMUM,
|
|
"include_external": "audio"
|
|
}
|
|
|
|
# update slots with search criteria.
|
|
intentObj.slots[SLOT_SEARCH_CRITERIA] = { CONF_VALUE: "", CONF_TEXT: svcData["criteria"] }
|
|
|
|
# search spotify catalog for matching audiobook name.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
search_result:ServiceResponse = await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=True,
|
|
)
|
|
self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_SEARCH_AUDIOBOOKS result", search_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
|
|
|
# if no matching items, then return appropriate response.
|
|
items_count:int = search_result.get("result",{}).get("items_count", 0)
|
|
if (items_count == 0):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_AUDIOBOOK)
|
|
|
|
# load returned info that we care about.
|
|
audiobook_title:str = search_result.get("result",{}).get("items")[0].get("name", "unknown")
|
|
audiobook_uri:str = search_result.get("result",{}).get("items")[0].get("uri", "unknown")
|
|
audiobook_url:str = search_result.get("result",{}).get("items")[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
|
|
|
author_title:str = search_result.get("result",{}).get("items")[0].get("authors")[0].get("name", "unknown")
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_AUDIOBOOK_TITLE] = { CONF_VALUE: audiobook_uri, CONF_TEXT: audiobook_title }
|
|
intentObj.slots[SLOT_AUDIOBOOK_URL] = { CONF_VALUE: audiobook_url, CONF_TEXT: "Spotify" }
|
|
intentObj.slots[SLOT_AUTHOR_TITLE] = { CONF_VALUE: author_title, CONF_TEXT: author_title }
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_CONTEXT
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"context_uri": audiobook_uri,
|
|
"shuffle": True if player_shuffle_mode == "on" else False,
|
|
"device_id": device_name,
|
|
"delay": delay,
|
|
}
|
|
|
|
# play the media.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=False,
|
|
)
|
|
|
|
# set appropriate response.
|
|
responseKey = RESPONSE_PLAY_AUDIOBOOK_WITH_AUTHOR
|
|
if (author_title is None):
|
|
responseKey = RESPONSE_PLAY_AUDIOBOOK
|
|
|
|
# return intent response.
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, responseKey)
|
|
|
|
finally:
|
|
|
|
# trace.
|
|
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
|
|
async def async_ProcessPlaylist(
|
|
self,
|
|
intentObj: Intent,
|
|
intentResponse: IntentResponse,
|
|
) -> IntentResponse:
|
|
"""
|
|
Handles the intent.
|
|
|
|
Args:
|
|
intentObj (Intent):
|
|
Intent object.
|
|
intentResponse (IntentResponse)
|
|
Intent response object.
|
|
|
|
Returns:
|
|
An IntentResponse object.
|
|
"""
|
|
try:
|
|
|
|
# trace.
|
|
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
# invoke base class method to resolve the player entity and its state.
|
|
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
|
intentObj,
|
|
intentResponse,
|
|
desiredFeatures=None,
|
|
desiredStates=None,
|
|
desiredStateResponseKey=None,
|
|
requiresSpotifyPremium=True,
|
|
)
|
|
|
|
# if media player was not resolved, then we are done;
|
|
# note that the base class method above already called `async_set_speech` with a response.
|
|
if playerEntityState is None:
|
|
return intentResponse
|
|
|
|
# get optional arguments (if provided).
|
|
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
|
device_name = intentObj.slots.get(SLOT_DEVICE_NAME, {}).get(CONF_VALUE, None)
|
|
playlist_name = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_TEXT, None)
|
|
playlist_uri = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_VALUE, None)
|
|
player_shuffle_mode = intentObj.slots.get(SLOT_PLAYER_SHUFFLE_MODE, {}).get(CONF_VALUE, "on")
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_PLAYLIST_TITLE] = { CONF_VALUE: playlist_uri, CONF_TEXT: playlist_name }
|
|
|
|
# was a uri supplied?
|
|
if (playlist_uri) and (playlist_uri.startswith(f"spotify:{SpotifyMediaTypes.PLAYLIST.value}")):
|
|
|
|
# if a uri was supplied, then it's from a pre-configured list of values; in this case,
|
|
# we will bypass the search since it's (assumed to be) a valid uri value.
|
|
|
|
# get id portion of spotify uri value.
|
|
playlist_id:str = get_id_from_uri(playlist_uri)
|
|
|
|
# load returned info that we care about.
|
|
playlist_title = playlist_name
|
|
playlist_url = f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.PLAYLIST.value}/{playlist_id}"
|
|
|
|
else:
|
|
|
|
# if no search criteria then we are done.
|
|
if (playlist_name is None):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_ERROR_SEARCH_NO_CRITERIA, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_SEARCH_PLAYLISTS
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"criteria": playlist_name,
|
|
"limit_total": CONF_LIMIT_TOTAL_MINIMUM,
|
|
"include_external": "audio"
|
|
}
|
|
|
|
# update slots with search criteria.
|
|
intentObj.slots[SLOT_SEARCH_CRITERIA] = { CONF_VALUE: "", CONF_TEXT: svcData["criteria"] }
|
|
|
|
# search spotify catalog for matching playlist name.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
search_result:ServiceResponse = await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=True,
|
|
)
|
|
self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_SEARCH_PLAYLISTS result", search_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
|
|
|
# if no matching items, then return appropriate response.
|
|
items_count:int = search_result.get("result",{}).get("items_count", 0)
|
|
if (items_count == 0):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_PLAYLIST)
|
|
|
|
# load returned info that we care about.
|
|
playlist_title:str = search_result.get("result",{}).get("items")[0].get("name", "unknown")
|
|
playlist_uri:str = search_result.get("result",{}).get("items")[0].get("uri", "unknown")
|
|
playlist_url:str = search_result.get("result",{}).get("items")[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_PLAYLIST_TITLE] = { CONF_VALUE: playlist_uri, CONF_TEXT: playlist_title }
|
|
intentObj.slots[SLOT_PLAYLIST_URL] = { CONF_VALUE: playlist_url, CONF_TEXT: "Spotify" }
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_CONTEXT
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"context_uri": playlist_uri,
|
|
"shuffle": True if player_shuffle_mode == "on" else False,
|
|
"device_id": device_name,
|
|
"delay": delay,
|
|
}
|
|
|
|
# play the media.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=False,
|
|
)
|
|
|
|
# return intent response.
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAY_PLAYLIST)
|
|
|
|
finally:
|
|
|
|
# trace.
|
|
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
|
|
async def async_ProcessPodcast(
|
|
self,
|
|
intentObj: Intent,
|
|
intentResponse: IntentResponse,
|
|
) -> IntentResponse:
|
|
"""
|
|
Handles the intent.
|
|
|
|
Args:
|
|
intentObj (Intent):
|
|
Intent object.
|
|
intentResponse (IntentResponse)
|
|
Intent response object.
|
|
|
|
Returns:
|
|
An IntentResponse object.
|
|
"""
|
|
try:
|
|
|
|
# trace.
|
|
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
# invoke base class method to resolve the player entity and its state.
|
|
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
|
intentObj,
|
|
intentResponse,
|
|
desiredFeatures=None,
|
|
desiredStates=None,
|
|
desiredStateResponseKey=None,
|
|
requiresSpotifyPremium=True,
|
|
)
|
|
|
|
# if media player was not resolved, then we are done;
|
|
# note that the base class method above already called `async_set_speech` with a response.
|
|
if playerEntityState is None:
|
|
return intentResponse
|
|
|
|
# get optional arguments (if provided).
|
|
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
|
device_name = intentObj.slots.get(SLOT_DEVICE_NAME, {}).get(CONF_VALUE, None)
|
|
latest_episode = intentObj.slots.get(SLOT_LATEST_EPISODE, {}).get(CONF_VALUE, None)
|
|
podcast_name = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_TEXT, None)
|
|
podcast_uri = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_VALUE, None)
|
|
player_shuffle_mode = intentObj.slots.get(SLOT_PLAYER_SHUFFLE_MODE, {}).get(CONF_VALUE, "on")
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_PODCAST_TITLE] = { CONF_VALUE: podcast_uri, CONF_TEXT: podcast_name }
|
|
|
|
# was a uri supplied?
|
|
if (podcast_uri) and (podcast_uri.startswith(f"spotify:{SpotifyMediaTypes.SHOW.value}")):
|
|
|
|
# if a uri was supplied, then it's from a pre-configured list of values; in this case,
|
|
# we will bypass the search since it's (assumed to be) a valid uri value.
|
|
|
|
# get id portion of spotify uri value.
|
|
podcast_id:str = get_id_from_uri(podcast_uri)
|
|
|
|
# load returned info that we care about.
|
|
# note that the title will be the slot list text value, as the SERVICE_SPOTIFY_GET_SHOW_EPISODES
|
|
# call does not return show information.
|
|
podcast_title = podcast_name
|
|
podcast_url = f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.SHOW.value}/{podcast_id}"
|
|
|
|
else:
|
|
|
|
# if no search criteria then we are done.
|
|
if (podcast_name is None):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_ERROR_SEARCH_NO_CRITERIA, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_SEARCH_SHOWS
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"criteria": podcast_name,
|
|
"limit_total": CONF_LIMIT_TOTAL_MINIMUM,
|
|
"include_external": "audio"
|
|
}
|
|
|
|
# update slots with search criteria.
|
|
intentObj.slots[SLOT_SEARCH_CRITERIA] = { CONF_VALUE: "", CONF_TEXT: svcData["criteria"] }
|
|
|
|
# search spotify catalog for matching podcast name.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
search_result:ServiceResponse = await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=True,
|
|
)
|
|
self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_SEARCH_SHOWS result", search_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
|
|
|
# if no matching items, then return appropriate response.
|
|
items_count:int = search_result.get("result",{}).get("items_count", 0)
|
|
if (items_count == 0):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_PODCAST)
|
|
|
|
# load returned info that we care about.
|
|
podcast_title:str = search_result.get("result",{}).get("items")[0].get("name", "unknown")
|
|
podcast_uri:str = search_result.get("result",{}).get("items")[0].get("uri", "unknown")
|
|
podcast_url:str = search_result.get("result",{}).get("items")[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
|
|
|
# get id portion of spotify uri value.
|
|
podcast_id:str = get_id_from_uri(podcast_uri)
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_PODCAST_TITLE] = { CONF_VALUE: podcast_uri, CONF_TEXT: podcast_title }
|
|
intentObj.slots[SLOT_PODCAST_URL] = { CONF_VALUE: podcast_url, CONF_TEXT: "Spotify" }
|
|
|
|
# # are we playing the latest episode?
|
|
# # if so, then get the latest episode data.
|
|
# # if not, then last played episode will resume where it left off.
|
|
# if (latest_episode == "on"):
|
|
|
|
# # trace.
|
|
# self.logsi.LogVerbose("Playing latest podcast episode", colorValue=SIColors.Khaki)
|
|
|
|
# # set service name and build parameters.
|
|
# svcName:str = SERVICE_SPOTIFY_GET_SHOW_EPISODES
|
|
# svcData:dict = \
|
|
# {
|
|
# "entity_id": playerEntityState.entity_id,
|
|
# "show_id": podcast_id,
|
|
# "limit_total": 1,
|
|
# }
|
|
|
|
# # get latest podcast episode data.
|
|
# self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
# search_result:ServiceResponse = await intentObj.hass.services.async_call(
|
|
# DOMAIN,
|
|
# svcName,
|
|
# svcData,
|
|
# blocking=True,
|
|
# context=intentObj.context,
|
|
# return_response=True,
|
|
# )
|
|
# self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_GET_SHOW_EPISODES result", search_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
|
|
|
# # if no matching items, then return appropriate response.
|
|
# items_count:int = search_result.get("result",{}).get("items_count", 0)
|
|
# if (items_count == 0):
|
|
# return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_PODCAST_EPISODE)
|
|
|
|
# # load returned info that we care about.
|
|
# episode_title:str = search_result.get("result",{}).get("items")[0].get("name", "unknown")
|
|
# episode_uri:str = search_result.get("result",{}).get("items")[0].get("uri", "unknown")
|
|
# episode_url:str = search_result.get("result",{}).get("items")[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
|
|
|
# # update slots with returned info.
|
|
# intentObj.slots[SLOT_EPISODE_TITLE] = { CONF_VALUE: episode_uri, CONF_TEXT: episode_title }
|
|
# intentObj.slots[SLOT_EPISODE_URL] = { CONF_VALUE: episode_url, CONF_TEXT: "Spotify" }
|
|
|
|
# # set service name and build parameters.
|
|
# svcName:str = SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_TRACKS
|
|
# svcData:dict = \
|
|
# {
|
|
# "entity_id": playerEntityState.entity_id,
|
|
# "uris": episode_uri,
|
|
# "shuffle": True if player_shuffle_mode == "on" else False,
|
|
# "device_id": device_name,
|
|
# "delay": delay,
|
|
# }
|
|
|
|
# # play the media.
|
|
# self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
# await intentObj.hass.services.async_call(
|
|
# DOMAIN,
|
|
# svcName,
|
|
# svcData,
|
|
# blocking=True,
|
|
# context=intentObj.context,
|
|
# return_response=False,
|
|
# )
|
|
|
|
# # return intent response.
|
|
# return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAY_PODCAST_EPISODE)
|
|
|
|
# else:
|
|
|
|
# # trace.
|
|
# self.logsi.LogVerbose("Resuming play of podcast episode last played", colorValue=SIColors.Khaki)
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_CONTEXT
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"context_uri": podcast_uri,
|
|
"shuffle": True if player_shuffle_mode == "on" else False,
|
|
"device_id": device_name,
|
|
"delay": delay,
|
|
"play_show_latest_episode": True if latest_episode == "on" else False,
|
|
}
|
|
|
|
# play the media.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=False,
|
|
)
|
|
|
|
# return intent response.
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_PLAY_PODCAST)
|
|
|
|
finally:
|
|
|
|
# trace.
|
|
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
|
|
async def async_ProcessPodcastEpisode(
|
|
self,
|
|
intentObj: Intent,
|
|
intentResponse: IntentResponse,
|
|
) -> IntentResponse:
|
|
"""
|
|
Handles the intent.
|
|
|
|
Args:
|
|
intentObj (Intent):
|
|
Intent object.
|
|
intentResponse (IntentResponse)
|
|
Intent response object.
|
|
|
|
Returns:
|
|
An IntentResponse object.
|
|
"""
|
|
try:
|
|
|
|
# trace.
|
|
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
# invoke base class method to resolve the player entity and its state.
|
|
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
|
intentObj,
|
|
intentResponse,
|
|
desiredFeatures=None,
|
|
desiredStates=None,
|
|
desiredStateResponseKey=None,
|
|
requiresSpotifyPremium=True,
|
|
)
|
|
|
|
# if media player was not resolved, then we are done;
|
|
# note that the base class method above already called `async_set_speech` with a response.
|
|
if playerEntityState is None:
|
|
return intentResponse
|
|
|
|
# get optional arguments (if provided).
|
|
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
|
device_name = intentObj.slots.get(SLOT_DEVICE_NAME, {}).get(CONF_VALUE, None)
|
|
podcast_name = intentObj.slots.get(SLOT_CRITERIA_ARG_2, {}).get(CONF_TEXT, None)
|
|
podcast_uri = intentObj.slots.get(SLOT_CRITERIA_ARG_2, {}).get(CONF_VALUE, None)
|
|
episode_name = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_TEXT, None)
|
|
episode_uri = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_VALUE, None)
|
|
player_shuffle_mode = intentObj.slots.get(SLOT_PLAYER_SHUFFLE_MODE, {}).get(CONF_VALUE, "on")
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_PODCAST_TITLE] = { CONF_VALUE: podcast_uri, CONF_TEXT: podcast_name }
|
|
intentObj.slots[SLOT_EPISODE_TITLE] = { CONF_VALUE: episode_uri, CONF_TEXT: episode_name }
|
|
|
|
# was a uri supplied?
|
|
if (episode_uri) and (episode_uri.startswith(f"spotify:{SpotifyMediaTypes.EPISODE.value}")):
|
|
|
|
# if a uri was supplied, then it's from a pre-configured list of values; in this case,
|
|
# we will bypass the search since it's (assumed to be) a valid uri value.
|
|
|
|
# get id portion of spotify uri value.
|
|
episode_id:str = get_id_from_uri(episode_uri)
|
|
|
|
# load returned info that we care about.
|
|
episode_title = episode_name
|
|
episode_url = f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.EPISODE.value}/{episode_id}"
|
|
|
|
# podcast variables are unknwon, since only the track name / uri were supplied.
|
|
podcast_title = podcast_name
|
|
podcast_url = SPOTIFY_WEB_URL_PFX
|
|
|
|
else:
|
|
|
|
# if no search criteria then we are done.
|
|
if (episode_name is None) and (podcast_name is None):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_ERROR_SEARCH_NO_CRITERIA, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
|
|
|
# format criteria if searching by podcast.
|
|
podcast_criteria = ""
|
|
if podcast_name is not None:
|
|
podcast_criteria = f", {podcast_name}"
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_SEARCH_EPISODES
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"criteria": episode_name + podcast_criteria,
|
|
"limit_total": CONF_LIMIT_TOTAL_MINIMUM,
|
|
"include_external": "audio"
|
|
}
|
|
|
|
# update slots with search criteria.
|
|
intentObj.slots[SLOT_SEARCH_CRITERIA] = { CONF_VALUE: "", CONF_TEXT: svcData["criteria"] }
|
|
|
|
# search spotify catalog for matching episode name.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
search_result:ServiceResponse = await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=True,
|
|
)
|
|
self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_SEARCH_EPISODES result", search_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
|
|
|
# if no matching items, then return appropriate response.
|
|
items_count:int = search_result.get("result",{}).get("items_count", 0)
|
|
if (items_count == 0):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_PODCAST_EPISODE)
|
|
|
|
# load returned info that we care about.
|
|
episode_title:str = search_result.get("result",{}).get("items")[0].get("name", "unknown")
|
|
episode_uri:str = search_result.get("result",{}).get("items")[0].get("uri", "unknown")
|
|
episode_url:str = search_result.get("result",{}).get("items")[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
|
|
|
# get id portion of spotify uri value.
|
|
episode_id:str = get_id_from_uri(episode_uri)
|
|
|
|
# note that search response does not contain the parent show details, so
|
|
# we need to call "get_episode" to retrieve them.
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_GET_EPISODE
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"episode_id": episode_id,
|
|
}
|
|
|
|
# get episode details.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
search_result:ServiceResponse = await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=True,
|
|
)
|
|
self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_GET_EPISODE result", search_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
|
|
|
# load returned info that we care about.
|
|
podcast_title:str = search_result.get("result",{}).get("show").get("name", "unknown")
|
|
podcast_uri:str = search_result.get("result",{}).get("show").get("uri", "unknown")
|
|
podcast_url:str = search_result.get("result",{}).get("show").get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_PODCAST_TITLE] = { CONF_VALUE: podcast_uri, CONF_TEXT: podcast_title }
|
|
intentObj.slots[SLOT_PODCAST_URL] = { CONF_VALUE: podcast_url, CONF_TEXT: "Spotify" }
|
|
intentObj.slots[SLOT_EPISODE_TITLE] = { CONF_VALUE: episode_uri, CONF_TEXT: episode_title }
|
|
intentObj.slots[SLOT_EPISODE_URL] = { CONF_VALUE: episode_url, CONF_TEXT: "Spotify" }
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_TRACKS
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"uris": episode_uri,
|
|
"shuffle": True if player_shuffle_mode == "on" else False,
|
|
"device_id": device_name,
|
|
"delay": delay,
|
|
}
|
|
|
|
# play the media.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=False,
|
|
)
|
|
|
|
# set appropriate response.
|
|
responseKey = RESPONSE_PLAY_PODCAST_EPISODE
|
|
if (podcast_name is None):
|
|
responseKey = RESPONSE_PLAY_PODCAST
|
|
|
|
# return intent response.
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, responseKey)
|
|
|
|
finally:
|
|
|
|
# trace.
|
|
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
|
|
async def async_ProcessTrack(
|
|
self,
|
|
intentObj: Intent,
|
|
intentResponse: IntentResponse,
|
|
) -> IntentResponse:
|
|
"""
|
|
Handles the intent.
|
|
|
|
Args:
|
|
intentObj (Intent):
|
|
Intent object.
|
|
intentResponse (IntentResponse)
|
|
Intent response object.
|
|
|
|
Returns:
|
|
An IntentResponse object.
|
|
"""
|
|
try:
|
|
|
|
# trace.
|
|
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
# invoke base class method to resolve the player entity and its state.
|
|
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
|
intentObj,
|
|
intentResponse,
|
|
desiredFeatures=None,
|
|
desiredStates=None,
|
|
desiredStateResponseKey=None,
|
|
requiresSpotifyPremium=True,
|
|
)
|
|
|
|
# if media player was not resolved, then we are done;
|
|
# note that the base class method above already called `async_set_speech` with a response.
|
|
if playerEntityState is None:
|
|
return intentResponse
|
|
|
|
# get optional arguments (if provided).
|
|
track_name = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_TEXT, None)
|
|
track_uri = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_VALUE, None)
|
|
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
|
device_name = intentObj.slots.get(SLOT_DEVICE_NAME, {}).get(CONF_VALUE, None)
|
|
player_shuffle_mode = intentObj.slots.get(SLOT_PLAYER_SHUFFLE_MODE, {}).get(CONF_VALUE, "on")
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_TRACK_TITLE] = { CONF_VALUE: track_uri, CONF_TEXT: track_name }
|
|
|
|
# was a uri supplied?
|
|
if (track_uri) and (track_uri.startswith(f"spotify:{SpotifyMediaTypes.TRACK.value}")):
|
|
|
|
# if a uri was supplied, then it's from a pre-configured list of values; in this case,
|
|
# we will bypass the search since it's (assumed to be) a valid uri value.
|
|
|
|
# get id portion of spotify uri value.
|
|
track_id:str = get_id_from_uri(track_uri)
|
|
|
|
# load returned info that we care about.
|
|
track_title = track_name
|
|
track_url = f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.TRACK.value}/{track_id}"
|
|
|
|
# artist variables are unknwon, since only the track name / uri were supplied.
|
|
artist_name = None
|
|
artist_title = artist_name
|
|
artist_url = SPOTIFY_WEB_URL_PFX
|
|
artist_uri = None
|
|
|
|
else:
|
|
|
|
# if no search criteria then we are done.
|
|
if (track_name is None):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_ERROR_SEARCH_NO_CRITERIA, IntentResponseErrorCode.FAILED_TO_HANDLE)
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_SEARCH_TRACKS
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"criteria": track_name,
|
|
"limit_total": CONF_LIMIT_TOTAL_MINIMUM,
|
|
"include_external": "audio"
|
|
}
|
|
|
|
# search spotify catalog for matching track name.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
search_result:ServiceResponse = await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=True,
|
|
)
|
|
self.logsi.LogDictionary(SILevel.Verbose, "SERVICE_SPOTIFY_SEARCH_TRACKS result", search_result, prettyPrint=True, colorValue=SIColors.Khaki)
|
|
|
|
# if no matching items, then return appropriate response.
|
|
items_count:int = search_result.get("result",{}).get("items_count", 0)
|
|
if (items_count == 0):
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, RESPONSE_SPOTIFY_SEARCH_NO_ITEMS_TRACK)
|
|
|
|
# load returned info that we care about.
|
|
track_title:str = search_result.get("result",{}).get("items")[0].get("name", "unknown")
|
|
track_uri:str = search_result.get("result",{}).get("items")[0].get("uri", "unknown")
|
|
track_url:str = search_result.get("result",{}).get("items")[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
|
|
|
artist_name:str = search_result.get("result",{}).get("items")[0].get("artists")[0].get("name", "unknown")
|
|
artist_title:str = search_result.get("result",{}).get("items")[0].get("artists")[0].get("name", "unknown")
|
|
artist_uri:str = search_result.get("result",{}).get("items")[0].get("artists")[0].get("uri", "unknown")
|
|
artist_url:str = search_result.get("result",{}).get("items")[0].get("artists")[0].get("external_urls", {}).get("spotify", SPOTIFY_WEB_URL_PFX)
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_ARTIST_TITLE] = { CONF_VALUE: artist_uri, CONF_TEXT: artist_title }
|
|
intentObj.slots[SLOT_ARTIST_URL] = { CONF_VALUE: artist_url, CONF_TEXT: "Spotify" }
|
|
intentObj.slots[SLOT_TRACK_TITLE] = { CONF_VALUE: track_uri, CONF_TEXT: track_title }
|
|
intentObj.slots[SLOT_TRACK_URL] = { CONF_VALUE: track_url, CONF_TEXT: "Spotify" }
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_TRACKS
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"uris": track_uri,
|
|
"shuffle": True if player_shuffle_mode == "on" else False,
|
|
"device_id": device_name,
|
|
"delay": delay,
|
|
}
|
|
|
|
# play the media.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=False,
|
|
)
|
|
|
|
# set appropriate response.
|
|
responseKey = RESPONSE_PLAY_TRACK_WITH_ARTIST
|
|
if (artist_name is None):
|
|
responseKey = RESPONSE_PLAY_TRACK
|
|
|
|
# return intent response.
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, responseKey)
|
|
|
|
finally:
|
|
|
|
# trace.
|
|
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
|
|
async def async_ProcessFavoriteTracks(
|
|
self,
|
|
intentObj: Intent,
|
|
intentResponse: IntentResponse,
|
|
) -> IntentResponse:
|
|
"""
|
|
Handles the intent.
|
|
|
|
Args:
|
|
intentObj (Intent):
|
|
Intent object.
|
|
intentResponse (IntentResponse)
|
|
Intent response object.
|
|
|
|
Returns:
|
|
An IntentResponse object.
|
|
"""
|
|
try:
|
|
|
|
# trace.
|
|
self.logsi.EnterMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|
|
|
|
# invoke base class method to resolve the player entity and its state.
|
|
playerEntityState:State = await super().async_GetMatchingPlayerState(
|
|
intentObj,
|
|
intentResponse,
|
|
desiredFeatures=None,
|
|
desiredStates=None,
|
|
desiredStateResponseKey=None,
|
|
requiresSpotifyPremium=True,
|
|
)
|
|
|
|
# if media player was not resolved, then we are done;
|
|
# note that the base class method above already called `async_set_speech` with a response.
|
|
if playerEntityState is None:
|
|
return intentResponse
|
|
|
|
# get optional arguments (if provided).
|
|
delay = intentObj.slots.get(SLOT_DELAY, {}).get(CONF_VALUE, None)
|
|
device_name = intentObj.slots.get(SLOT_DEVICE_NAME, {}).get(CONF_VALUE, None)
|
|
artist_name = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_TEXT, None)
|
|
artist_uri = intentObj.slots.get(SLOT_CRITERIA_ARG_1, {}).get(CONF_VALUE, None)
|
|
player_shuffle_mode = intentObj.slots.get(SLOT_PLAYER_SHUFFLE_MODE, {}).get(CONF_VALUE, "on")
|
|
limit_total = intentObj.slots.get(SLOT_LIMIT_TOTAL, {}).get(CONF_VALUE, None)
|
|
|
|
artist_title = artist_name
|
|
|
|
# was a uri supplied?
|
|
if (artist_uri) and (artist_uri.startswith(f"spotify:{SpotifyMediaTypes.ARTIST.value}")):
|
|
|
|
# if a uri was supplied, then it's from a pre-configured list of values; in this case,
|
|
# we will bypass the search since it's (assumed to be) a valid uri value.
|
|
|
|
# get id portion of spotify uri value.
|
|
artist_id:str = get_id_from_uri(artist_uri)
|
|
|
|
# load returned info that we care about.
|
|
artist_url = f"{SPOTIFY_WEB_URL_PFX}/{SpotifyMediaTypes.ARTIST.value}/{artist_id}"
|
|
|
|
else:
|
|
|
|
artist_url = f"{SPOTIFY_WEB_URL_PFX}"
|
|
|
|
# update slots with returned info.
|
|
intentObj.slots[SLOT_ARTIST_TITLE] = { CONF_VALUE: artist_uri, CONF_TEXT: artist_title }
|
|
intentObj.slots[SLOT_ARTIST_URL] = { CONF_VALUE: artist_url, CONF_TEXT: "Spotify" }
|
|
|
|
# set service name and build parameters.
|
|
svcName:str = SERVICE_SPOTIFY_PLAYER_MEDIA_PLAY_TRACK_FAVORITES
|
|
svcData:dict = \
|
|
{
|
|
"entity_id": playerEntityState.entity_id,
|
|
"filter_artist": artist_uri or "", # will be a uri if slot artist_names list used; otherwise, just artist text or none.
|
|
"shuffle": True if player_shuffle_mode == "on" else False,
|
|
"device_id": device_name,
|
|
"delay": delay,
|
|
"limit_total": limit_total,
|
|
}
|
|
|
|
# play the media.
|
|
self.logsi.LogVerbose(STAppMessages.MSG_SERVICE_EXECUTE % (svcName, playerEntityState.entity_id), colorValue=SIColors.Khaki)
|
|
await intentObj.hass.services.async_call(
|
|
DOMAIN,
|
|
svcName,
|
|
svcData,
|
|
blocking=True,
|
|
context=intentObj.context,
|
|
return_response=False,
|
|
)
|
|
|
|
# set appropriate response.
|
|
responseKey = RESPONSE_PLAY_FAVORITE_TRACKS_FOR_ARTIST
|
|
if (artist_name is None):
|
|
responseKey = RESPONSE_PLAY_FAVORITE_TRACKS
|
|
|
|
# return intent response.
|
|
return await self.ReturnResponseByKey(intentObj, intentResponse, responseKey)
|
|
|
|
finally:
|
|
|
|
# trace.
|
|
self.logsi.LeaveMethod(SILevel.Debug, colorValue=SIColors.Khaki)
|