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