spotifywebapiPython 1.0.148__py3-none-any.whl → 1.0.150__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.

Files changed (38) hide show
  1. {spotifywebapiPython-1.0.148.dist-info → spotifywebapiPython-1.0.150.dist-info}/METADATA +5 -9
  2. {spotifywebapiPython-1.0.148.dist-info → spotifywebapiPython-1.0.150.dist-info}/RECORD +38 -29
  3. spotifywebapipython/__init__.py +0 -2
  4. spotifywebapipython/const.py +1 -1
  5. spotifywebapipython/docs/__init__.html +25 -4
  6. spotifywebapipython/docs/index.html +26 -2
  7. spotifywebapipython/docs/search.js +1 -1
  8. spotifywebapipython/docs/spotifywebapipython/const.html +3 -3
  9. spotifywebapipython/docs/spotifywebapipython/saappmessages.html +2 -2
  10. spotifywebapipython/docs/spotifywebapipython/sautils.html +8 -8
  11. spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +2 -2
  12. spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +2 -2
  13. spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +2 -2
  14. spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +514 -741
  15. spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +3 -3
  16. spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +2 -2
  17. spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +2 -2
  18. spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +466 -0
  19. spotifywebapipython/models/spotifyconnectdevice.py +84 -4
  20. spotifywebapipython/models/spotifyconnectdevices.py +16 -307
  21. spotifywebapipython/models/zeroconfdiscoveryresult.py +1 -42
  22. spotifywebapipython/sautils.py +0 -1
  23. spotifywebapipython/spotifyclient.py +1380 -1834
  24. spotifywebapipython/spotifyconnect/__init__.py +19 -0
  25. spotifywebapipython/{spotifyconnectdeviceeventargs.py → spotifyconnect/spotifyconnectdeviceeventargs.py} +1 -1
  26. spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +1994 -0
  27. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +336 -0
  28. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastcontroller.py +609 -0
  29. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +317 -0
  30. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfexceptions.py +10 -0
  31. spotifywebapipython/spotifyconnect/spotifyconnectzeroconflistener.py +335 -0
  32. spotifywebapipython/spotifydiscovery.py +0 -1
  33. spotifywebapipython/spotifywebplayertoken.py +344 -0
  34. spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +4 -52
  35. spotifywebapipython/zeroconfapi/zeroconfresponse.py +8 -0
  36. {spotifywebapiPython-1.0.148.dist-info → spotifywebapiPython-1.0.150.dist-info}/LICENSE +0 -0
  37. {spotifywebapiPython-1.0.148.dist-info → spotifywebapiPython-1.0.150.dist-info}/WHEEL +0 -0
  38. {spotifywebapiPython-1.0.148.dist-info → spotifywebapiPython-1.0.150.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)