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

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