424 lines
14 KiB
Python
424 lines
14 KiB
Python
"""main module for spotcast homeassistant utility"""
|
|
|
|
from __future__ import annotations
|
|
|
|
__version__ = "4.0.1"
|
|
|
|
import collections
|
|
import logging
|
|
import time
|
|
|
|
import homeassistant.core as ha_core
|
|
from homeassistant.components import websocket_api
|
|
from homeassistant.const import CONF_ENTITY_ID, CONF_OFFSET, CONF_REPEAT
|
|
from homeassistant.core import callback
|
|
from homeassistant.exceptions import HomeAssistantError
|
|
|
|
from .const import (
|
|
CONF_ACCOUNTS,
|
|
CONF_DEVICE_NAME,
|
|
CONF_FORCE_PLAYBACK,
|
|
CONF_IGNORE_FULLY_PLAYED,
|
|
CONF_RANDOM,
|
|
CONF_SHUFFLE,
|
|
CONF_SP_DC,
|
|
CONF_SP_KEY,
|
|
CONF_SPOTIFY_ACCOUNT,
|
|
CONF_SPOTIFY_ALBUM_NAME,
|
|
CONF_SPOTIFY_ARTIST_NAME,
|
|
CONF_SPOTIFY_AUDIOBOOK_NAME,
|
|
CONF_SPOTIFY_CATEGORY,
|
|
CONF_SPOTIFY_COUNTRY,
|
|
CONF_SPOTIFY_DEVICE_ID,
|
|
CONF_SPOTIFY_EPISODE_NAME,
|
|
CONF_SPOTIFY_GENRE_NAME,
|
|
CONF_SPOTIFY_LIMIT,
|
|
CONF_SPOTIFY_PLAYLIST_NAME,
|
|
CONF_SPOTIFY_SHOW_NAME,
|
|
CONF_SPOTIFY_TRACK_NAME,
|
|
CONF_SPOTIFY_URI,
|
|
CONF_START_VOL,
|
|
DOMAIN,
|
|
SCHEMA_PLAYLISTS,
|
|
SCHEMA_WS_ACCOUNTS,
|
|
SCHEMA_WS_CASTDEVICES,
|
|
SCHEMA_WS_DEVICES,
|
|
SCHEMA_WS_PLAYER,
|
|
CONF_START_POSITION,
|
|
SERVICE_START_COMMAND_SCHEMA,
|
|
SPOTCAST_CONFIG_SCHEMA,
|
|
WS_TYPE_SPOTCAST_ACCOUNTS,
|
|
WS_TYPE_SPOTCAST_CASTDEVICES,
|
|
WS_TYPE_SPOTCAST_DEVICES,
|
|
WS_TYPE_SPOTCAST_PLAYER,
|
|
WS_TYPE_SPOTCAST_PLAYLISTS,
|
|
)
|
|
from .helpers import (
|
|
add_tracks_to_queue,
|
|
async_wrap,
|
|
get_cast_devices,
|
|
get_random_playlist_from_category,
|
|
get_search_results,
|
|
get_spotify_devices,
|
|
get_spotify_install_status,
|
|
get_spotify_media_player,
|
|
is_empty_str,
|
|
is_valid_uri,
|
|
url_to_spotify_uri,
|
|
)
|
|
from .spotcast_controller import SpotcastController
|
|
|
|
CONFIG_SCHEMA = SPOTCAST_CONFIG_SCHEMA
|
|
DEBUG = True
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
def setup(hass: ha_core.HomeAssistant, config: collections.OrderedDict) -> bool:
|
|
"""setup method for integration with Home Assistant
|
|
|
|
Args:
|
|
hass (ha_core.HomeAssistant): the HomeAssistant object of the
|
|
server
|
|
config (collections.OrderedDict): the configuration of the
|
|
server
|
|
|
|
Returns:
|
|
bool: returns a bollean based on if the setup wroked or not
|
|
"""
|
|
|
|
# get spotify core integration status
|
|
# if return false, could indicate a bad spotify integration. Race
|
|
# condition doesn't permit us to abort setup, see #258
|
|
if not get_spotify_install_status(hass):
|
|
_LOGGER.debug(
|
|
"Spotify integration was not found, please verify integration is "
|
|
"functionnal. Could result in python error..."
|
|
)
|
|
|
|
# Setup the Spotcast service.
|
|
conf = config[DOMAIN]
|
|
|
|
sp_dc = conf[CONF_SP_DC]
|
|
sp_key = conf[CONF_SP_KEY]
|
|
accounts = conf.get(CONF_ACCOUNTS)
|
|
|
|
spotcast_controller = SpotcastController(hass, sp_dc, sp_key, accounts)
|
|
|
|
if DOMAIN not in hass.data:
|
|
hass.data[DOMAIN] = {}
|
|
hass.data[DOMAIN]["controller"] = spotcast_controller
|
|
|
|
@callback
|
|
def websocket_handle_playlists(
|
|
hass: ha_core.HomeAssistant,
|
|
connection,
|
|
msg: str
|
|
):
|
|
@async_wrap
|
|
def get_playlist():
|
|
"""Handle to get playlist"""
|
|
playlist_type = msg.get("playlist_type")
|
|
country_code = msg.get("country_code")
|
|
locale = msg.get("locale", "en")
|
|
limit = msg.get("limit", 10)
|
|
account = msg.get("account", None)
|
|
|
|
_LOGGER.debug("websocket_handle_playlists msg: %s", msg)
|
|
resp = spotcast_controller.get_playlists(
|
|
account, playlist_type, country_code, locale, limit
|
|
)
|
|
connection.send_message(
|
|
websocket_api.result_message(msg["id"], resp))
|
|
|
|
hass.async_add_job(get_playlist())
|
|
|
|
@callback
|
|
def websocket_handle_devices(
|
|
hass: ha_core.HomeAssistant,
|
|
connection,
|
|
msg: str,
|
|
):
|
|
@async_wrap
|
|
def get_devices():
|
|
"""Handle to get devices. Only for default account"""
|
|
account = msg.get("account", None)
|
|
client = spotcast_controller.get_spotify_client(account)
|
|
me_resp = client._get("me") # pylint: disable=W0212
|
|
spotify_media_player = get_spotify_media_player(
|
|
hass, me_resp["id"])
|
|
resp = get_spotify_devices(spotify_media_player, hass)
|
|
connection.send_message(
|
|
websocket_api.result_message(msg["id"], resp))
|
|
|
|
hass.async_add_job(get_devices())
|
|
|
|
@callback
|
|
def websocket_handle_player(
|
|
hass: ha_core.HomeAssistant,
|
|
connection,
|
|
msg: str,
|
|
):
|
|
@async_wrap
|
|
def get_player():
|
|
"""Handle to get player"""
|
|
account = msg.get("account", None)
|
|
_LOGGER.debug("websocket_handle_player msg: %s", msg)
|
|
client = spotcast_controller.get_spotify_client(account)
|
|
resp = client._get("me/player") # pylint: disable=W0212
|
|
connection.send_message(
|
|
websocket_api.result_message(msg["id"], resp))
|
|
|
|
hass.async_add_job(get_player())
|
|
|
|
@callback
|
|
def websocket_handle_accounts(
|
|
hass: ha_core.HomeAssistant,
|
|
connection,
|
|
msg: str,
|
|
):
|
|
"""Handle to get accounts"""
|
|
_LOGGER.debug("websocket_handle_accounts msg: %s", msg)
|
|
resp = list(accounts.keys()) if accounts is not None else []
|
|
resp.append("default")
|
|
connection.send_message(websocket_api.result_message(msg["id"], resp))
|
|
|
|
@callback
|
|
def websocket_handle_castdevices(
|
|
hass: ha_core.HomeAssistant,
|
|
connection, msg: str
|
|
):
|
|
"""Handle to get cast devices for debug purposes"""
|
|
_LOGGER.debug("websocket_handle_castdevices msg: %s", msg)
|
|
|
|
known_devices = get_cast_devices(hass)
|
|
_LOGGER.debug("%s", known_devices)
|
|
resp = [
|
|
{
|
|
"uuid": str(cast_info.cast_info.uuid),
|
|
"model_name": cast_info.cast_info.model_name,
|
|
"friendly_name": cast_info.cast_info.friendly_name,
|
|
}
|
|
for cast_info in known_devices
|
|
]
|
|
|
|
connection.send_message(websocket_api.result_message(msg["id"], resp))
|
|
|
|
def start_casting(call: ha_core.ServiceCall):
|
|
"""service called."""
|
|
uri = call.data.get(CONF_SPOTIFY_URI)
|
|
category = call.data.get(CONF_SPOTIFY_CATEGORY)
|
|
country = call.data.get(CONF_SPOTIFY_COUNTRY)
|
|
limit = call.data.get(CONF_SPOTIFY_LIMIT)
|
|
artistName = call.data.get(CONF_SPOTIFY_ARTIST_NAME)
|
|
albumName = call.data.get(CONF_SPOTIFY_ALBUM_NAME)
|
|
playlistName = call.data.get(CONF_SPOTIFY_PLAYLIST_NAME)
|
|
trackName = call.data.get(CONF_SPOTIFY_TRACK_NAME)
|
|
showName = call.data.get(CONF_SPOTIFY_SHOW_NAME)
|
|
episodeName = call.data.get(CONF_SPOTIFY_EPISODE_NAME)
|
|
audiobookName = call.data.get(CONF_SPOTIFY_AUDIOBOOK_NAME)
|
|
genreName = call.data.get(CONF_SPOTIFY_GENRE_NAME)
|
|
random_song = call.data.get(CONF_RANDOM, False)
|
|
repeat = call.data.get(CONF_REPEAT, False)
|
|
shuffle = call.data.get(CONF_SHUFFLE, False)
|
|
start_volume = call.data.get(CONF_START_VOL)
|
|
spotify_device_id = call.data.get(CONF_SPOTIFY_DEVICE_ID)
|
|
position = call.data.get(CONF_OFFSET)
|
|
start_position = call.data.get(CONF_START_POSITION)
|
|
force_playback = call.data.get(CONF_FORCE_PLAYBACK)
|
|
account = call.data.get(CONF_SPOTIFY_ACCOUNT)
|
|
ignore_fully_played = call.data.get(CONF_IGNORE_FULLY_PLAYED)
|
|
device_name = call.data.get(CONF_DEVICE_NAME)
|
|
entity_id = call.data.get(CONF_ENTITY_ID)
|
|
|
|
try: # yes this is ugly, quick fix while working on V4
|
|
|
|
# if no market information try to get global setting
|
|
if is_empty_str(country):
|
|
try:
|
|
country = config[DOMAIN][CONF_SPOTIFY_COUNTRY]
|
|
except KeyError:
|
|
country = None
|
|
|
|
client = spotcast_controller.get_spotify_client(account)
|
|
|
|
# verify the uri provided and clean-up if required
|
|
if not is_empty_str(uri):
|
|
|
|
# remove ? from badly formatted URI
|
|
uri = uri.split("?")[0]
|
|
|
|
if uri.startswith("http"):
|
|
try:
|
|
u = url_to_spotify_uri(uri)
|
|
_LOGGER.debug(
|
|
"converted web URL %s to spotify URI %s", uri, u)
|
|
uri = u
|
|
except ValueError:
|
|
_LOGGER.error(
|
|
"invalid web URL provided, could not convert to spotify URI: %s", uri)
|
|
|
|
if not is_valid_uri(uri):
|
|
_LOGGER.error("Invalid URI provided, aborting casting")
|
|
return
|
|
|
|
# force first two elements of uri to lowercase
|
|
uri = uri.split(":")
|
|
uri[0] = uri[0].lower()
|
|
uri[1] = uri[1].lower()
|
|
uri = ":".join(uri)
|
|
|
|
# first, rely on spotify id given in config otherwise get one
|
|
if not spotify_device_id:
|
|
spotify_device_id = spotcast_controller.get_spotify_device_id(
|
|
account, spotify_device_id, device_name, entity_id
|
|
)
|
|
|
|
if start_position is not None:
|
|
start_position *= 1000
|
|
|
|
if (
|
|
is_empty_str(uri)
|
|
and len(
|
|
list(
|
|
filter(
|
|
lambda x: not is_empty_str(x),
|
|
[
|
|
artistName,
|
|
playlistName,
|
|
trackName,
|
|
showName,
|
|
episodeName,
|
|
audiobookName,
|
|
genreName,
|
|
category,
|
|
],
|
|
)
|
|
)
|
|
)
|
|
== 0
|
|
):
|
|
_LOGGER.debug("Transfering playback")
|
|
current_playback = client.current_playback()
|
|
if current_playback is not None:
|
|
_LOGGER.debug("Current_playback from spotify: %s",
|
|
current_playback)
|
|
force_playback = True
|
|
_LOGGER.debug("Force playback: %s", force_playback)
|
|
client.transfer_playback(
|
|
device_id=spotify_device_id, force_play=force_playback
|
|
)
|
|
elif not is_empty_str(category):
|
|
uri = get_random_playlist_from_category(
|
|
client, category, country, limit)
|
|
|
|
if uri is None:
|
|
_LOGGER.error("No playlist returned. Stop service call")
|
|
return None
|
|
|
|
spotcast_controller.play(
|
|
client,
|
|
spotify_device_id,
|
|
uri,
|
|
random_song,
|
|
position,
|
|
ignore_fully_played,
|
|
start_position,
|
|
)
|
|
else:
|
|
searchResults = []
|
|
if is_empty_str(uri):
|
|
# get uri from search request
|
|
searchResults = get_search_results(
|
|
spotify_client=client,
|
|
limit=limit,
|
|
artistName=artistName,
|
|
country=country,
|
|
albumName=albumName,
|
|
playlistName=playlistName,
|
|
trackName=trackName,
|
|
showName=showName,
|
|
episodeName=episodeName,
|
|
audiobookName=audiobookName,
|
|
genreName=genreName,
|
|
)
|
|
# play the first track
|
|
if len(searchResults) > 0:
|
|
uri = searchResults[0]["uri"]
|
|
|
|
spotcast_controller.play(
|
|
client,
|
|
spotify_device_id,
|
|
uri,
|
|
random_song,
|
|
position,
|
|
ignore_fully_played,
|
|
start_position,
|
|
)
|
|
|
|
if len(searchResults) > 1:
|
|
add_tracks_to_queue(client, searchResults[1:])
|
|
|
|
if start_volume <= 100:
|
|
_LOGGER.debug("Setting volume to %d", start_volume)
|
|
time.sleep(2)
|
|
client.volume(volume_percent=start_volume,
|
|
device_id=spotify_device_id)
|
|
if shuffle:
|
|
_LOGGER.debug("Turning shuffle on")
|
|
time.sleep(3)
|
|
client.shuffle(state=shuffle, device_id=spotify_device_id)
|
|
if repeat:
|
|
_LOGGER.debug("Turning repeat on")
|
|
time.sleep(3)
|
|
client.repeat(state=repeat, device_id=spotify_device_id)
|
|
|
|
except Exception as exc:
|
|
if DEBUG:
|
|
raise exc
|
|
|
|
raise HomeAssistantError(exc) from exc
|
|
|
|
# Register websocket and service
|
|
websocket_api.async_register_command(
|
|
hass=hass,
|
|
command_or_handler=WS_TYPE_SPOTCAST_PLAYLISTS,
|
|
handler=websocket_handle_playlists,
|
|
schema=SCHEMA_PLAYLISTS,
|
|
)
|
|
websocket_api.async_register_command(
|
|
hass=hass,
|
|
command_or_handler=WS_TYPE_SPOTCAST_DEVICES,
|
|
handler=websocket_handle_devices,
|
|
schema=SCHEMA_WS_DEVICES,
|
|
)
|
|
websocket_api.async_register_command(
|
|
hass=hass,
|
|
command_or_handler=WS_TYPE_SPOTCAST_PLAYER,
|
|
handler=websocket_handle_player,
|
|
schema=SCHEMA_WS_PLAYER,
|
|
)
|
|
|
|
websocket_api.async_register_command(
|
|
hass=hass,
|
|
command_or_handler=WS_TYPE_SPOTCAST_ACCOUNTS,
|
|
handler=websocket_handle_accounts,
|
|
schema=SCHEMA_WS_ACCOUNTS,
|
|
)
|
|
|
|
websocket_api.async_register_command(
|
|
hass=hass,
|
|
command_or_handler=WS_TYPE_SPOTCAST_CASTDEVICES,
|
|
handler=websocket_handle_castdevices,
|
|
schema=SCHEMA_WS_CASTDEVICES,
|
|
)
|
|
|
|
hass.services.register(
|
|
domain=DOMAIN,
|
|
service="start",
|
|
service_func=start_casting,
|
|
schema=SERVICE_START_COMMAND_SCHEMA,
|
|
)
|
|
|
|
return True
|