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

149 lines
4.5 KiB
Python

"""
Controller to interface with Spotify.
"""
from __future__ import annotations
import hashlib
import json
import logging
import threading
import requests
import json
import hashlib
from .const import APP_SPOTIFY
from .error import LaunchError
import requests
from pychromecast.controllers import BaseController
from .const import APP_SPOTIFY
APP_NAMESPACE = "urn:x-cast:com.spotify.chromecast.secure.v1"
TYPE_GET_INFO = "getInfo"
TYPE_GET_INFO_RESPONSE = "getInfoResponse"
TYPE_ADD_USER = "addUser"
TYPE_ADD_USER_RESPONSE = "addUserResponse"
TYPE_ADD_USER_ERROR = "addUserError"
# pylint: disable=too-many-instance-attributes
class SpotifyController(BaseController):
"""Controller to interact with Spotify namespace."""
def __init__(self, castDevice, access_token=None, expires=None):
super(SpotifyController, self).__init__(APP_NAMESPACE, APP_SPOTIFY)
self.logger = logging.getLogger(__name__)
self.session_started = False
self.access_token = access_token
self.expires = expires
self.is_launched = False
self.device = None
self.credential_error = False
self.waiting = threading.Event()
self.castDevice = castDevice
def receive_message(self, _message, data: dict):
"""
Handle the auth flow and active player selection.
Called when a message is received.
"""
if data["type"] == TYPE_GET_INFO_RESPONSE:
self.device = self.getSpotifyDeviceID()
self.client = data["payload"]["clientID"]
headers = {
"authority": "spclient.wg.spotify.com",
"authorization": "Bearer {}".format(self.access_token),
"content-type": "text/plain;charset=UTF-8",
}
request_body = json.dumps(
{"clientId": self.client, "deviceId": self.device}
)
response = requests.post(
"https://spclient.wg.spotify.com/device-auth/v1/refresh",
headers=headers,
data=request_body,
)
json_resp = response.json()
self.send_message(
{
"type": TYPE_ADD_USER,
"payload": {
"blob": json_resp["accessToken"],
"tokenType": "accesstoken",
},
}
)
if data["type"] == TYPE_ADD_USER_RESPONSE:
self.is_launched = True
self.waiting.set()
if data["type"] == TYPE_ADD_USER_ERROR:
self.device = None
self.credential_error = True
self.waiting.set()
return True
def launch_app(self, timeout=10):
"""
Launch Spotify application.
Will raise a LaunchError exception if there is no response from the
Spotify app within timeout seconds.
"""
if self.access_token is None or self.expires is None:
raise ValueError("access_token and expires cannot be empty")
def callback(*_):
"""Callback function"""
self.send_message(
{
"type": TYPE_GET_INFO,
"payload": {
"remoteName": self.castDevice.cast_info.friendly_name,
"deviceID": self.getSpotifyDeviceID(),
"deviceAPI_isGroup": False,
},
}
)
self.device = None
self.credential_error = False
self.waiting.clear()
self.launch(callback_function=callback)
counter = 0
while counter < (timeout + 1):
if self.is_launched:
return
self.waiting.wait(1)
counter += 1
if not self.is_launched:
raise LaunchError(
"Timeout when waiting for status response from Spotify app"
)
# pylint: disable=too-many-locals
def quick_play(self, **kwargs):
"""
Launches the spotify controller and returns when it's ready.
To actually play media, another application using spotify
connect is required.
"""
self.access_token = kwargs["access_token"]
self.expires = kwargs["expires"]
self.launch_app(timeout=20)
def getSpotifyDeviceID(self) -> str:
"""
Retrieve the Spotify deviceID from provided chromecast info
"""
return hashlib.md5(self.castDevice.cast_info.friendly_name.encode()).hexdigest()