spotifywebapiPython 1.0.147__py3-none-any.whl → 1.0.149__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of spotifywebapiPython might be problematic. Click here for more details.
- {spotifywebapiPython-1.0.147.dist-info → spotifywebapiPython-1.0.149.dist-info}/METADATA +5 -9
- {spotifywebapiPython-1.0.147.dist-info → spotifywebapiPython-1.0.149.dist-info}/RECORD +38 -29
- spotifywebapipython/__init__.py +0 -2
- spotifywebapipython/const.py +1 -1
- spotifywebapipython/docs/__init__.html +25 -4
- spotifywebapipython/docs/index.html +26 -2
- spotifywebapipython/docs/search.js +1 -1
- spotifywebapipython/docs/spotifywebapipython/const.html +3 -3
- spotifywebapipython/docs/spotifywebapipython/saappmessages.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/sautils.html +8 -8
- spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +514 -741
- spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +3 -3
- spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +2 -2
- spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +466 -0
- spotifywebapipython/models/spotifyconnectdevice.py +84 -4
- spotifywebapipython/models/spotifyconnectdevices.py +16 -307
- spotifywebapipython/models/zeroconfdiscoveryresult.py +0 -41
- spotifywebapipython/sautils.py +0 -1
- spotifywebapipython/spotifyclient.py +1384 -1835
- spotifywebapipython/spotifyconnect/__init__.py +19 -0
- spotifywebapipython/{spotifyconnectdeviceeventargs.py → spotifyconnect/spotifyconnectdeviceeventargs.py} +1 -1
- spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +1967 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +336 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastcontroller.py +609 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +316 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfexceptions.py +10 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconflistener.py +335 -0
- spotifywebapipython/spotifydiscovery.py +0 -1
- spotifywebapipython/spotifywebplayertoken.py +344 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +4 -52
- spotifywebapipython/zeroconfapi/zeroconfresponse.py +8 -0
- {spotifywebapiPython-1.0.147.dist-info → spotifywebapiPython-1.0.149.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.147.dist-info → spotifywebapiPython-1.0.149.dist-info}/WHEEL +0 -0
- {spotifywebapiPython-1.0.147.dist-info → spotifywebapiPython-1.0.149.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
# external package imports.
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import pychromecast
|
|
6
|
+
from pychromecast import Chromecast
|
|
7
|
+
from pychromecast.controllers import BaseController
|
|
8
|
+
import requests
|
|
9
|
+
import threading
|
|
10
|
+
|
|
11
|
+
# our package imports.
|
|
12
|
+
from .spotifyconnectzeroconfexceptions import SpotifyConnectZeroconfLaunchError
|
|
13
|
+
from spotifywebapipython import SpotifyApiError
|
|
14
|
+
from spotifywebapipython.saappmessages import SAAppMessages
|
|
15
|
+
from spotifywebapipython.zeroconfapi import SpotifyZeroconfApiError, ZeroconfGetInfo, ZeroconfResponse
|
|
16
|
+
from spotifywebapipython.const import (
|
|
17
|
+
SPOTIFY_API_AUTHORIZE_URL,
|
|
18
|
+
SPOTIFY_API_TOKEN_URL,
|
|
19
|
+
SPOTIFY_DESKTOP_APP_CLIENT_ID,
|
|
20
|
+
TRACE_METHOD_RESULT,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# get smartinspect logger reference; create a new session for this module name.
|
|
24
|
+
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext
|
|
25
|
+
import logging
|
|
26
|
+
|
|
27
|
+
_logsi:SISession = SIAuto.Si.GetSession(__name__)
|
|
28
|
+
if (_logsi == None):
|
|
29
|
+
_logsi = SIAuto.Si.AddSession(__name__, True)
|
|
30
|
+
_logsi.SystemLogger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
# constants.
|
|
33
|
+
APP_SPOTIFY = "CC32E753"
|
|
34
|
+
APP_NAMESPACE = "urn:x-cast:com.spotify.chromecast.secure.v1"
|
|
35
|
+
TYPE_GET_INFO = "getInfo"
|
|
36
|
+
TYPE_GET_INFO_ERROR = "getInfoError"
|
|
37
|
+
TYPE_GET_INFO_RESPONSE = "getInfoResponse"
|
|
38
|
+
TYPE_ADD_USER = "addUser"
|
|
39
|
+
TYPE_ADD_USER_RESPONSE = "addUserResponse"
|
|
40
|
+
TYPE_ADD_USER_ERROR = "addUserError"
|
|
41
|
+
TYPE_TRANSFER_SUCCESS_RESPONSE = "transferSuccess"
|
|
42
|
+
TYPE_TRANSFER_ERROR_RESPONSE = "transferError"
|
|
43
|
+
|
|
44
|
+
SPOTIFY_WEB_API_DEVICEAUTH_REFRESH = "https://spclient.wg.spotify.com/device-auth/v1/refresh"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SpotifyConnectZeroconfCastController(BaseController):
|
|
48
|
+
"""
|
|
49
|
+
Controller to interact with Spotify Connect Zeroconf API.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
castDevice:Chromecast,
|
|
55
|
+
accessToken:str=None,
|
|
56
|
+
expiresAt:int=None
|
|
57
|
+
) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Initializes a new instance of the class.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
castDevice (CastDevice):
|
|
63
|
+
Chromecast device target.
|
|
64
|
+
accessToken (str):
|
|
65
|
+
Access token provided to the Spotify Web Player application.
|
|
66
|
+
expiresAt (int):
|
|
67
|
+
DateTime (in epoch seconds) that the authorization token will expire.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
# invoke base class method.
|
|
71
|
+
super().__init__(APP_NAMESPACE, APP_SPOTIFY)
|
|
72
|
+
|
|
73
|
+
# initialize storage.
|
|
74
|
+
self.accessToken:str = accessToken
|
|
75
|
+
self.castDevice:Chromecast = castDevice
|
|
76
|
+
self.clientId:str = None
|
|
77
|
+
self.expiresAt:int = expiresAt
|
|
78
|
+
self.deviceId:str = None
|
|
79
|
+
self.isAddUserError:bool = False
|
|
80
|
+
self.isGetInfoError:bool = False
|
|
81
|
+
self.isLaunched:bool = False
|
|
82
|
+
self.isPlaybackTransferError:bool = False
|
|
83
|
+
self.isMessageException:bool = False
|
|
84
|
+
self.waitAddUser:threading.Event = threading.Event()
|
|
85
|
+
self.waitGetInfo:threading.Event = threading.Event()
|
|
86
|
+
self.waitPlaybackTransfer:threading.Event = threading.Event()
|
|
87
|
+
self.zeroconfGetInfo:ZeroconfGetInfo = None # for GetInformation response
|
|
88
|
+
self.zeroconfResponse:ZeroconfResponse = None # for non-GetInformation responses.
|
|
89
|
+
|
|
90
|
+
# clear wait events.
|
|
91
|
+
self.waitAddUser.clear()
|
|
92
|
+
self.waitGetInfo.clear()
|
|
93
|
+
self.waitPlaybackTransfer.clear()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def launch_app(self,
|
|
97
|
+
timeout:int=15,
|
|
98
|
+
timeoutTransfer:int=20):
|
|
99
|
+
"""
|
|
100
|
+
Launch Spotify Cast Application on the remote cast device.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
timeout (int):
|
|
104
|
+
Number of seconds to wait for the Spotify cast app to be
|
|
105
|
+
launched on the Chromecast device.
|
|
106
|
+
timeoutTransfer (int):
|
|
107
|
+
Number of seconds to wait for the Spotify cast app to receive
|
|
108
|
+
playback control on the Chromecast device.
|
|
109
|
+
|
|
110
|
+
Launches the spotify controller and returns when it's ready.
|
|
111
|
+
To actually play media, another application using spotify
|
|
112
|
+
connect is required.
|
|
113
|
+
|
|
114
|
+
Will raise a SpotifyConnectZeroconfLaunchError exception if there is no
|
|
115
|
+
response from the Spotify app within timeout seconds.
|
|
116
|
+
|
|
117
|
+
Will raise a SpotifyConnectZeroconfLaunchError exception if player transfer
|
|
118
|
+
has not been completed to the Spotify app within timeoutTransfer seconds.
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
# validations.
|
|
122
|
+
if self.accessToken is None or self.expiresAt is None:
|
|
123
|
+
raise ValueError("accessToken and expiresAt cannot be empty")
|
|
124
|
+
|
|
125
|
+
# initialize launch variables.
|
|
126
|
+
self.deviceId = None
|
|
127
|
+
self.isLaunched = False
|
|
128
|
+
self.isAddUserError = False
|
|
129
|
+
self.isGetInfoError = False
|
|
130
|
+
self.isPlaybackTransferError = False
|
|
131
|
+
self.isMessageException = False
|
|
132
|
+
self.waitAddUser.clear()
|
|
133
|
+
self.waitGetInfo.clear()
|
|
134
|
+
self.waitPlaybackTransfer.clear()
|
|
135
|
+
|
|
136
|
+
def callback(*_):
|
|
137
|
+
"""
|
|
138
|
+
Callback function
|
|
139
|
+
"""
|
|
140
|
+
# send the initial getInfo request after launching the app.
|
|
141
|
+
# this will trigger an `addUser` request if the getInfo Response was successful.
|
|
142
|
+
self.GetInformation()
|
|
143
|
+
|
|
144
|
+
# launch the spotify app on the device.
|
|
145
|
+
# this will perform the following steps, initiated via the callback function:
|
|
146
|
+
# - issues a Spotify Connect Zeroconf `getInfo` request to retrieve info about the device.
|
|
147
|
+
# - waits for a `getInfoResponse` message and processes the results.
|
|
148
|
+
# - issues a Spotify Connect Zeroconf `addUser` request to login the user to Spotify Connect.
|
|
149
|
+
# - waits for a `addUserResponse` (or `addUserError`) message and processes the results.
|
|
150
|
+
self.launch(callback_function=callback)
|
|
151
|
+
|
|
152
|
+
# wait for the launched spotify app to fully initialize.
|
|
153
|
+
# this occurs when we receive an `addUserResponse` or `addUserError` message.
|
|
154
|
+
counter = 0
|
|
155
|
+
while counter < (timeout + 1):
|
|
156
|
+
if (self.waitAddUser.wait(1)):
|
|
157
|
+
if (self.isAddUserError):
|
|
158
|
+
raise SpotifyZeroconfApiError(self.zeroconfResponse.Status, "AddUser request failed", "launch_app", self.zeroconfResponse.StatusString)
|
|
159
|
+
break
|
|
160
|
+
if (self.isGetInfoError):
|
|
161
|
+
raise SpotifyZeroconfApiError(self.zeroconfResponse.Status, "GetInformation request failed", "launch_app", self.zeroconfResponse.StatusString)
|
|
162
|
+
if (counter >= timeout):
|
|
163
|
+
raise SpotifyZeroconfApiError(0, "Timed out while waiting for a response", "launch_app", "timeout")
|
|
164
|
+
counter += 1
|
|
165
|
+
|
|
166
|
+
# one more check to ensure that we launched.
|
|
167
|
+
if (not self.isLaunched):
|
|
168
|
+
raise SpotifyConnectZeroconfLaunchError("Spotify Cast App could not be launched")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def receive_message(
|
|
172
|
+
self,
|
|
173
|
+
message,
|
|
174
|
+
data: dict
|
|
175
|
+
) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Called when a message is received from the Spotify cast app.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
message (str):
|
|
181
|
+
Message header values; typical keys are:
|
|
182
|
+
- "protocol_version": cast protocol version (e.g. "CASTV2_1_0").
|
|
183
|
+
- "source_id": source application uuid (e.g. "a23351 ... 82ff84").
|
|
184
|
+
- "destination_id": destination id (e.g. "sender-0").
|
|
185
|
+
- "namespace": cast app namespace (e.g. "urn:x-cast:com.spotify.chromecast.secure.v1").
|
|
186
|
+
- "payload_type": payload type (e.g. "STRING").
|
|
187
|
+
data (dict):
|
|
188
|
+
Message payload dictionary; the important keys are:
|
|
189
|
+
- "type": type of payload (e.g. "getInfoResponse", "addUserResponse", "addUserError", etc).
|
|
190
|
+
- "payload": contains specific fields related to the "type" of message.
|
|
191
|
+
|
|
192
|
+
Handle the auth flow and active player selection.
|
|
193
|
+
"""
|
|
194
|
+
payloadDataType:str = None
|
|
195
|
+
payloadData:dict = {}
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
|
|
199
|
+
# trace.
|
|
200
|
+
_logsi.LogDictionary(SILevel.Verbose, "Chromecast Message received: %s" % message, data, prettyPrint=True)
|
|
201
|
+
|
|
202
|
+
# message argument example: "getInfoResponse"
|
|
203
|
+
# protocol_version: CASTV2_1_0
|
|
204
|
+
# source_id: "a233518a-505f-4d23-abcb-dfce4a82ff84"
|
|
205
|
+
# destination_id: "sender-0"
|
|
206
|
+
# namespace: "urn:x-cast:com.spotify.chromecast.secure.v1"
|
|
207
|
+
# payload_type: STRING
|
|
208
|
+
# payload_utf8: "{\"type\":\"getInfoResponse\",\"payload\":{\"remoteName\":\"Nest Audio 01\",\"deviceID\":\"bddc ... a3de\",\"deviceAPI_isGroup\":false,\"version\":\"2.9.0\",\"publicKey\":\"empty\",\"deviceType\":\"cast_audio\",\"brandDisplayName\":\"google\",\"modelDisplayName\":\"Google_Home\",\"libraryVersion\":\"5.44.1\",\"resolverVersion\":\"1\",\"groupStatus\":\"NONE\",\"tokenType\":\"accesstoken\",\"clientID\":\"d7d ... eae7\",\"productID\":0,\"scope\":\"streaming\",\"availability\":\"\",\"spotifyError\":0,\"status\":101,\"statusString\":\"OK\"}}"
|
|
209
|
+
|
|
210
|
+
# safely get the message payload structure type.
|
|
211
|
+
if (isinstance(data, dict)):
|
|
212
|
+
payloadDataType = data.get("type", None)
|
|
213
|
+
payloadData:dict = data.get("payload", {})
|
|
214
|
+
|
|
215
|
+
# process message payload.
|
|
216
|
+
|
|
217
|
+
# did getInfo succeed?
|
|
218
|
+
if payloadDataType == TYPE_GET_INFO_RESPONSE:
|
|
219
|
+
|
|
220
|
+
# example payload response (Nest Speaker):
|
|
221
|
+
# { 'type': 'getInfoResponse',
|
|
222
|
+
# 'payload': { 'remoteName': 'Nest Audio 01',
|
|
223
|
+
# 'deviceID': 'bddc7 ...',
|
|
224
|
+
# 'deviceAPI_isGroup': False,
|
|
225
|
+
# 'version': '2.9.0',
|
|
226
|
+
# 'publicKey': 'empty',
|
|
227
|
+
# 'deviceType': 'cast_audio',
|
|
228
|
+
# 'brandDisplayName': 'google',
|
|
229
|
+
# 'modelDisplayName': 'Google_Home',
|
|
230
|
+
# 'libraryVersion': '5.44.1',
|
|
231
|
+
# 'resolverVersion': '1',
|
|
232
|
+
# 'groupStatus': 'NONE',
|
|
233
|
+
# 'tokenType': 'accesstoken',
|
|
234
|
+
# 'clientID': 'd7df0 ...',
|
|
235
|
+
# 'productID': 0,
|
|
236
|
+
# 'scope': 'streaming',
|
|
237
|
+
# 'availability': '',
|
|
238
|
+
# 'spotifyError': 0,
|
|
239
|
+
# 'status': 101,
|
|
240
|
+
# 'statusString': 'OK'}}
|
|
241
|
+
|
|
242
|
+
# process message payload data.
|
|
243
|
+
self.zeroconfGetInfo = ZeroconfGetInfo(payloadData)
|
|
244
|
+
if (self.zeroconfGetInfo.PublicKey == 'empty'):
|
|
245
|
+
self.zeroconfGetInfo.PublicKey = None
|
|
246
|
+
self.zeroconfGetInfo.ResponseSource = payloadDataType
|
|
247
|
+
|
|
248
|
+
# trace.
|
|
249
|
+
_logsi.LogObject(SILevel.Verbose, "GetInformation result (ZeroconfGetInfo) - \"%s\" (%s)" % (self.zeroconfGetInfo.RemoteName, self.zeroconfGetInfo.DeviceId), self.zeroconfGetInfo, excludeNonPublic=True)
|
|
250
|
+
|
|
251
|
+
# getInfo response received from the initial launch.
|
|
252
|
+
self.deviceId = self.zeroconfGetInfo.DeviceId
|
|
253
|
+
self.clientId = self.zeroconfGetInfo.ClientId
|
|
254
|
+
|
|
255
|
+
# set status indicators and events.
|
|
256
|
+
self.isGetInfoError = False
|
|
257
|
+
self.waitGetInfo.set()
|
|
258
|
+
|
|
259
|
+
# if cast app is already launched, then we are done.
|
|
260
|
+
if (self.isLaunched):
|
|
261
|
+
return True
|
|
262
|
+
|
|
263
|
+
# otherwise, we are still in the process of launching the cast app via the callback.
|
|
264
|
+
# we will now send the initial addUser request to login to Spotify Connect, and
|
|
265
|
+
# we will wait for a response in the post-launch code.
|
|
266
|
+
self.AddUser()
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
# did getInfo fail?
|
|
270
|
+
if payloadDataType == TYPE_GET_INFO_ERROR:
|
|
271
|
+
|
|
272
|
+
# example payload response (Nest Speaker):
|
|
273
|
+
# { 'type': 'getInfoError',
|
|
274
|
+
# 'payload': { 'status': 303,
|
|
275
|
+
# 'statusString': 'ERROR-INVALID-ARGUMENTS',
|
|
276
|
+
# 'spotifyError': 0}}
|
|
277
|
+
|
|
278
|
+
# process message payload data.
|
|
279
|
+
self.zeroconfResponse = ZeroconfResponse(payloadData)
|
|
280
|
+
self.zeroconfResponse.ResponseSource = payloadDataType
|
|
281
|
+
|
|
282
|
+
# set status indicators and events.
|
|
283
|
+
self.isGetInfoError = True
|
|
284
|
+
self.deviceId = None
|
|
285
|
+
self.waitGetInfo.set()
|
|
286
|
+
if (not self.isLaunched):
|
|
287
|
+
# if app is not launched, then init is waiting on the `waitAddUser` event!
|
|
288
|
+
self.waitAddUser.set()
|
|
289
|
+
return True
|
|
290
|
+
|
|
291
|
+
# did addUser succeed?
|
|
292
|
+
if payloadDataType == TYPE_ADD_USER_RESPONSE:
|
|
293
|
+
|
|
294
|
+
# example payload response (Nest Speaker):
|
|
295
|
+
# { 'type': 'addUserResponse',
|
|
296
|
+
# 'payload': { 'status': 101,
|
|
297
|
+
# 'statusString': 'OK',
|
|
298
|
+
# 'spotifyError': 0,
|
|
299
|
+
# 'user': {'id': '31l77 ...'},
|
|
300
|
+
# 'deviceId': 'bddc ...'}}
|
|
301
|
+
|
|
302
|
+
# process message payload data.
|
|
303
|
+
self.zeroconfResponse = ZeroconfResponse(payloadData)
|
|
304
|
+
self.zeroconfResponse.ResponseSource = payloadDataType
|
|
305
|
+
|
|
306
|
+
try:
|
|
307
|
+
# stash the `user` value into the getInfo `ActiveUser` property as well.
|
|
308
|
+
if (self.zeroconfGetInfo.ActiveUser is None) or (self.zeroconfGetInfo.ActiveUser == ""):
|
|
309
|
+
self.zeroconfGetInfo.ActiveUser = payloadData["user"]["id"]
|
|
310
|
+
except:
|
|
311
|
+
pass # ignore errors.
|
|
312
|
+
|
|
313
|
+
# set status indicators and events.
|
|
314
|
+
self.isAddUserError = False
|
|
315
|
+
self.isLaunched = True
|
|
316
|
+
self.waitAddUser.set()
|
|
317
|
+
return True
|
|
318
|
+
|
|
319
|
+
# did addUser fail?
|
|
320
|
+
if payloadDataType == TYPE_ADD_USER_ERROR:
|
|
321
|
+
|
|
322
|
+
# example payload response (Nest Speaker):
|
|
323
|
+
# { 'type': 'addUserError',
|
|
324
|
+
# 'payload': { 'status': 303,
|
|
325
|
+
# 'statusString': 'ERROR-INVALID-ARGUMENTS',
|
|
326
|
+
# 'spotifyError': 0}}
|
|
327
|
+
|
|
328
|
+
# process message payload data.
|
|
329
|
+
self.zeroconfResponse = ZeroconfResponse(payloadData)
|
|
330
|
+
self.zeroconfResponse.ResponseSource = payloadDataType
|
|
331
|
+
|
|
332
|
+
# set status indicators and events.
|
|
333
|
+
self.isAddUserError = True
|
|
334
|
+
self.deviceId = None
|
|
335
|
+
self.waitAddUser.set()
|
|
336
|
+
return True
|
|
337
|
+
|
|
338
|
+
# was playback transferred to the device?
|
|
339
|
+
if payloadDataType == TYPE_TRANSFER_SUCCESS_RESPONSE:
|
|
340
|
+
|
|
341
|
+
# example payload response (Nest Speaker):
|
|
342
|
+
# { 'type': 'transferSuccess',
|
|
343
|
+
# 'payload': { 'status': 101,
|
|
344
|
+
# 'statusString': 'OK',
|
|
345
|
+
# 'spotifyError': 0}}
|
|
346
|
+
|
|
347
|
+
# process message payload data.
|
|
348
|
+
self.zeroconfResponse = ZeroconfResponse(payloadData)
|
|
349
|
+
self.zeroconfResponse.ResponseSource = payloadDataType
|
|
350
|
+
|
|
351
|
+
# set status indicators and events.
|
|
352
|
+
self.isPlaybackTransferError = False
|
|
353
|
+
self.waitPlaybackTransfer.set()
|
|
354
|
+
return True
|
|
355
|
+
|
|
356
|
+
# did an error occur transferring playback to the device?
|
|
357
|
+
if payloadDataType == TYPE_TRANSFER_ERROR_RESPONSE:
|
|
358
|
+
|
|
359
|
+
# example payload response (Nest Speaker):
|
|
360
|
+
# { 'type': 'transferError',
|
|
361
|
+
# 'payload': { 'status': 402,
|
|
362
|
+
# 'statusString': 'ERROR-SPOTIFY-ERROR',
|
|
363
|
+
# 'spotifyError': 408}}
|
|
364
|
+
|
|
365
|
+
# process message payload data.
|
|
366
|
+
self.zeroconfResponse = ZeroconfResponse(payloadData)
|
|
367
|
+
self.zeroconfResponse.ResponseSource = payloadDataType
|
|
368
|
+
|
|
369
|
+
# set status indicators and events.
|
|
370
|
+
self.isPlaybackTransferError = True
|
|
371
|
+
self.waitPlaybackTransfer.set()
|
|
372
|
+
return True
|
|
373
|
+
|
|
374
|
+
# trace.
|
|
375
|
+
_logsi.LogDictionary(SILevel.Verbose, "Chromecast Message ignored - payload type value not recognized: \"%s\"" % (payloadDataType), message, prettyPrint=True)
|
|
376
|
+
|
|
377
|
+
return True
|
|
378
|
+
|
|
379
|
+
except Exception as ex:
|
|
380
|
+
|
|
381
|
+
# load zeroconfResponse instance with exception information.
|
|
382
|
+
response: ZeroconfResponse = ZeroconfResponse()
|
|
383
|
+
response.SpotifyError = 0
|
|
384
|
+
response.Status = 1000
|
|
385
|
+
response.StatusString = "ERROR-CHROMECAST-RECEIVE-MESSAGE"
|
|
386
|
+
response.ResponseSource = "SpotifyConnectZeroconfCastController"
|
|
387
|
+
self.zeroconfResponse = response
|
|
388
|
+
|
|
389
|
+
# trace.
|
|
390
|
+
_logsi.LogException(SAAppMessages.UNHANDLED_EXCEPTION.format("receive_message", str(ex)), ex, logToSystemLogger=False)
|
|
391
|
+
|
|
392
|
+
# ignore exceptions, as there is nothing we can do at this point.
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def getSpotifyDeviceID(self) -> str:
|
|
396
|
+
"""
|
|
397
|
+
Retrieve the Spotify deviceID from provided chromecast info
|
|
398
|
+
"""
|
|
399
|
+
deviceId:str = hashlib.md5(self.castDevice.cast_info.friendly_name.encode()).hexdigest()
|
|
400
|
+
return deviceId
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def AddUser(
|
|
404
|
+
self,
|
|
405
|
+
timeout:int=10,
|
|
406
|
+
) -> None:
|
|
407
|
+
"""
|
|
408
|
+
Calls the `addUser` Spotify Zeroconf API endpoint to login the user to Spotify Connect.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
timeout (int):
|
|
412
|
+
Max time to wait (in seconds) for a response to the request.
|
|
413
|
+
Only applies if the `isLaunched=False`.
|
|
414
|
+
|
|
415
|
+
Raises:
|
|
416
|
+
SpotifyZeroconfApiError:
|
|
417
|
+
If the Spotify Zeroconf API request response contains error information.
|
|
418
|
+
|
|
419
|
+
The request will timeout after the specified `timeout` value, and an exception raised if the
|
|
420
|
+
device cannot be reached or does not respond within that time frame.
|
|
421
|
+
</details>
|
|
422
|
+
"""
|
|
423
|
+
apiMethodName:str = 'AddUser'
|
|
424
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
425
|
+
tracePrefix:str = 'Spotify Web Player Device Auth Token Refresh'
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
|
|
429
|
+
# trace.
|
|
430
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
431
|
+
apiMethodParms.AppendKeyValue("timeout", timeout)
|
|
432
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Add User to Spotify Connect Chromecast Zeroconf API (ip=%s:%s)" % (self.castDevice.socket_client.host, self.castDevice.socket_client.port), apiMethodParms)
|
|
433
|
+
|
|
434
|
+
# validations.
|
|
435
|
+
if (timeout is None):
|
|
436
|
+
timeout=10
|
|
437
|
+
|
|
438
|
+
# clear wait event.
|
|
439
|
+
self.waitAddUser.clear()
|
|
440
|
+
self.addUserError = False
|
|
441
|
+
|
|
442
|
+
# prepare to refresh the access token.
|
|
443
|
+
reqHeaders = {
|
|
444
|
+
"authority": "spclient.wg.spotify.com",
|
|
445
|
+
"authorization": "Bearer {}".format(self.accessToken),
|
|
446
|
+
"content-type": "text/plain;charset=UTF-8",
|
|
447
|
+
}
|
|
448
|
+
reqParms = json.dumps(
|
|
449
|
+
{
|
|
450
|
+
"clientId": self.clientId,
|
|
451
|
+
"deviceId": self.deviceId
|
|
452
|
+
}
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# trace.
|
|
456
|
+
_logsi.LogVerbose("Refreshing Spotify web player device authorization token")
|
|
457
|
+
_logsi.LogDictionary(SILevel.Debug, "%s http request: \"%s\" (headers)" % (tracePrefix, SPOTIFY_WEB_API_DEVICEAUTH_REFRESH), reqHeaders)
|
|
458
|
+
_logsi.LogText(SILevel.Verbose, "%s http request: \"%s\" (parms)" % (tracePrefix, SPOTIFY_WEB_API_DEVICEAUTH_REFRESH), reqParms)
|
|
459
|
+
|
|
460
|
+
# refresh the authorization token.
|
|
461
|
+
response = requests.post(
|
|
462
|
+
SPOTIFY_WEB_API_DEVICEAUTH_REFRESH,
|
|
463
|
+
headers=reqHeaders,
|
|
464
|
+
data=reqParms,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# trace.
|
|
468
|
+
if _logsi.IsOn(SILevel.Debug):
|
|
469
|
+
_logsi.LogObject(SILevel.Debug, "%s http response object - type=\"%s\", module=\"%s\"" % (tracePrefix, type(response).__name__, type(response).__module__), response)
|
|
470
|
+
|
|
471
|
+
# trace.
|
|
472
|
+
if _logsi.IsOn(SILevel.Debug):
|
|
473
|
+
_logsi.LogObject(SILevel.Debug, "%s http response [%s-%s] (response)" % (tracePrefix, response.status_code, response.reason), response)
|
|
474
|
+
if (response.headers):
|
|
475
|
+
_logsi.LogCollection(SILevel.Debug, "%s http response [%s-%s] (headers)" % (tracePrefix, response.status_code, response.reason), response.headers.items())
|
|
476
|
+
|
|
477
|
+
# process json response data.
|
|
478
|
+
json_resp = response.json()
|
|
479
|
+
|
|
480
|
+
# send the Spotify Connect Zeroconf `addUser` request.
|
|
481
|
+
# send request to cast app.
|
|
482
|
+
# this will login to the device, which (if successful) will cause the
|
|
483
|
+
# device to be registered to Spotify Connect Zeroconf as well as appear
|
|
484
|
+
# in the Spotify Player device list.
|
|
485
|
+
castMsg:dict = {
|
|
486
|
+
"type": TYPE_ADD_USER,
|
|
487
|
+
"payload": {
|
|
488
|
+
"blob": json_resp["accessToken"],
|
|
489
|
+
"tokenType": "accesstoken",
|
|
490
|
+
},
|
|
491
|
+
}
|
|
492
|
+
_logsi.LogDictionary(SILevel.Debug, "Sending Spotify Connect Zeroconf AddUser message to Chromecast device (ip=%s:%s)" % (self.castDevice.socket_client.host, self.castDevice.socket_client.port), castMsg, prettyPrint=True)
|
|
493
|
+
self.send_message(castMsg)
|
|
494
|
+
|
|
495
|
+
# if we are in the process of launching, then we will wait for a response in the
|
|
496
|
+
# post-launch code as we are still in the launch callback sequence.
|
|
497
|
+
if (not self.isLaunched):
|
|
498
|
+
return
|
|
499
|
+
|
|
500
|
+
# if we are NOT in the process of launching, then we will wait for a response here.
|
|
501
|
+
counter = 0
|
|
502
|
+
while counter < (timeout + 1):
|
|
503
|
+
if (self.waitAddUser.wait(1)):
|
|
504
|
+
if (self.isAddUserError):
|
|
505
|
+
raise SpotifyZeroconfApiError(self.zeroconfResponse.Status, "AddUser request failed", "AddUser", self.zeroconfResponse.StatusString)
|
|
506
|
+
break
|
|
507
|
+
if (counter >= timeout):
|
|
508
|
+
raise SpotifyZeroconfApiError(0, "Timed out while waiting for a response", "AddUser", "timeout")
|
|
509
|
+
counter += 1
|
|
510
|
+
|
|
511
|
+
# if we make it here, then addUser request was processed successfully.
|
|
512
|
+
return
|
|
513
|
+
|
|
514
|
+
except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
|
|
515
|
+
except Exception as ex:
|
|
516
|
+
|
|
517
|
+
# format unhandled exception.
|
|
518
|
+
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
|
|
519
|
+
|
|
520
|
+
finally:
|
|
521
|
+
|
|
522
|
+
# trace.
|
|
523
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def GetInformation(
|
|
527
|
+
self,
|
|
528
|
+
timeout:int=10,
|
|
529
|
+
) -> ZeroconfGetInfo:
|
|
530
|
+
"""
|
|
531
|
+
Calls the `getInfo` Spotify Zeroconf API endpoint to return information about the device.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
timeout (int):
|
|
535
|
+
Max time to wait (in seconds) for a response to the request.
|
|
536
|
+
Only applies if the `isLaunched=False`.
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
A `ZeroconfGetInfo` object that indicates success or failure (see notes below), as well
|
|
540
|
+
as the device information.
|
|
541
|
+
|
|
542
|
+
Raises:
|
|
543
|
+
SpotifyZeroconfApiError:
|
|
544
|
+
If the Spotify Zeroconf API request response contains error information.
|
|
545
|
+
|
|
546
|
+
The request will timeout after the specified `timeout` value, and an exception raised if the
|
|
547
|
+
device cannot be reached or does not respond within that time frame.
|
|
548
|
+
"""
|
|
549
|
+
apiMethodName:str = 'GetInformation'
|
|
550
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
551
|
+
result:ZeroconfGetInfo = None
|
|
552
|
+
|
|
553
|
+
try:
|
|
554
|
+
|
|
555
|
+
# trace.
|
|
556
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
557
|
+
apiMethodParms.AppendKeyValue("timeout", timeout)
|
|
558
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Get information from Spotify Connect Chromecast Zeroconf API (ip=%s:%s)" % (self.castDevice.socket_client.host, self.castDevice.socket_client.port), apiMethodParms)
|
|
559
|
+
|
|
560
|
+
# validations.
|
|
561
|
+
if (timeout is None):
|
|
562
|
+
timeout=10
|
|
563
|
+
|
|
564
|
+
# clear wait event.
|
|
565
|
+
self.waitGetInfo.clear()
|
|
566
|
+
self.isGetInfoError = False
|
|
567
|
+
|
|
568
|
+
# send request to cast app.
|
|
569
|
+
castMsg:dict = {
|
|
570
|
+
"type": TYPE_GET_INFO,
|
|
571
|
+
"payload": {
|
|
572
|
+
"remoteName": self.castDevice.cast_info.friendly_name,
|
|
573
|
+
"deviceID": self.getSpotifyDeviceID(),
|
|
574
|
+
"deviceAPI_isGroup": False,
|
|
575
|
+
},
|
|
576
|
+
}
|
|
577
|
+
_logsi.LogDictionary(SILevel.Debug, "Sending Spotify Connect Zeroconf Getinformation message to Chromecast device (ip=%s:%s)" % (self.castDevice.socket_client.host, self.castDevice.socket_client.port), castMsg, prettyPrint=True)
|
|
578
|
+
self.send_message(castMsg)
|
|
579
|
+
|
|
580
|
+
# if we are in the process of launching, then we will wait for a response in the
|
|
581
|
+
# post-launch code as we are still in the launch callback sequence.
|
|
582
|
+
if (not self.isLaunched):
|
|
583
|
+
return True
|
|
584
|
+
|
|
585
|
+
# if we are NOT in the process of launching, then we will wait for a response here.
|
|
586
|
+
counter = 0
|
|
587
|
+
while counter < (timeout + 1):
|
|
588
|
+
if (self.waitGetInfo.wait(1)):
|
|
589
|
+
if (self.isGetInfoError):
|
|
590
|
+
raise SpotifyZeroconfApiError(self.zeroconfGetInfo.Status, "GetInformation request failed", "GetInformation", self.zeroconfGetInfo.StatusString)
|
|
591
|
+
break
|
|
592
|
+
if (counter >= timeout):
|
|
593
|
+
raise SpotifyZeroconfApiError(0, "Timed out while waiting for a response", "GetInformation", "timeout")
|
|
594
|
+
counter += 1
|
|
595
|
+
|
|
596
|
+
# if we make it here, then getInfo request was processed successfully.
|
|
597
|
+
# return response to the caller.
|
|
598
|
+
return self.zeroconfGetInfo
|
|
599
|
+
|
|
600
|
+
except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
|
|
601
|
+
except Exception as ex:
|
|
602
|
+
|
|
603
|
+
# format unhandled exception.
|
|
604
|
+
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
|
|
605
|
+
|
|
606
|
+
finally:
|
|
607
|
+
|
|
608
|
+
# trace.
|
|
609
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|