149 lines
4.5 KiB
Python
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()
|