spotifywebapiPython 1.0.64__py3-none-any.whl → 1.0.66__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.64.dist-info → spotifywebapiPython-1.0.66.dist-info}/METADATA +1 -1
- {spotifywebapiPython-1.0.64.dist-info → spotifywebapiPython-1.0.66.dist-info}/RECORD +22 -22
- spotifywebapipython/const.py +1 -1
- spotifywebapipython/docs/__init__.html +14 -2
- spotifywebapipython/docs/index.html +2 -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 +2 -2
- 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 +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifydiscovery.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapiauthenticationerror.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapierror.html +2 -2
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +125 -49
- spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +9 -1
- spotifywebapipython/zeroconfapi/zeroconfresponse.py +12 -3
- {spotifywebapiPython-1.0.64.dist-info → spotifywebapiPython-1.0.66.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.64.dist-info → spotifywebapiPython-1.0.66.dist-info}/WHEEL +0 -0
- {spotifywebapiPython-1.0.64.dist-info → spotifywebapiPython-1.0.66.dist-info}/top_level.txt +0 -0
|
@@ -68,10 +68,6 @@ class ZeroconfConnect:
|
|
|
68
68
|
True if the host device utilizes HTTPS Secure Sockets Layer (SSL) support;
|
|
69
69
|
otherwise, False to utilize HTTP.
|
|
70
70
|
Default is False (HTTP).
|
|
71
|
-
|
|
72
|
-
The URI value consists of an IP Address, port, CPath, and version value that are used to send
|
|
73
|
-
requests to / receive responses from a headless Spotify Connect device. These values can be
|
|
74
|
-
obtained from a Zeroconf discovery process.
|
|
75
71
|
"""
|
|
76
72
|
# validations.
|
|
77
73
|
if (version is None):
|
|
@@ -191,7 +187,7 @@ class ZeroconfConnect:
|
|
|
191
187
|
|
|
192
188
|
if _logsi.IsOn(SILevel.Verbose):
|
|
193
189
|
if isinstance(responseData, dict):
|
|
194
|
-
_logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json dict)" % (response.status_code, response.reason, endpoint), responseData)
|
|
190
|
+
_logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json dict)" % (response.status_code, response.reason, endpoint), responseData, prettyPrint=True)
|
|
195
191
|
elif isinstance(responseData, list):
|
|
196
192
|
_logsi.LogArray(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json array)" % (response.status_code, response.reason, endpoint), responseData)
|
|
197
193
|
else:
|
|
@@ -219,7 +215,6 @@ class ZeroconfConnect:
|
|
|
219
215
|
# just raise a new exception with the non-JSON response data.
|
|
220
216
|
raise SpotifyZeroconfApiError(response.status_code, responseUTF8, methodName, response.reason, _logsi)
|
|
221
217
|
|
|
222
|
-
errCode:str = None
|
|
223
218
|
errMessage:str = None
|
|
224
219
|
|
|
225
220
|
# at this point we know that the response contains JSON data, as
|
|
@@ -232,14 +227,15 @@ class ZeroconfConnect:
|
|
|
232
227
|
|
|
233
228
|
# taken from: https://developer.spotify.com/documentation/commercial-hardware/implementation/guides/zeroconf
|
|
234
229
|
# HTTP Status codes
|
|
230
|
+
# Note It is permissible to send HTTP status code 200 in all cases, as long as a valid JSON reply is returned that contains the status, statusString, and spotifyError strings.
|
|
235
231
|
# The following table shows the different responses returned by the HTTP server to a client's request:
|
|
236
|
-
|
|
237
232
|
# Status name Status HTTP Status Status string Response to Usage
|
|
238
233
|
# Ok 101 200 OK or ERROR-OK All Successful operation
|
|
239
234
|
# Bad 102 400 ERROR-BAD-REQUEST All Web server problem or critically malformed request
|
|
240
235
|
# Unknown 103 500 ERROR-UNKNOWN All Fallback when no other error applies
|
|
241
236
|
# NotImplemented 104 501 ERROR-NOT-IMPLEMENTED All Server does not implement this feature
|
|
242
237
|
# LoginFailed 202 200 ERROR-LOGIN-FAILED addUser Spotify returned error when trying to login
|
|
238
|
+
# InvalidPublicKey 203 xxx ERROR-INVALID-PUBLICKEY addUser ZeroConf login failed due to an invalid public key
|
|
243
239
|
# MissingAction 301 400 ERROR-MISSING-ACTION All Web request has no action parameter
|
|
244
240
|
# InvalidAction 302 400 ERROR-INVALID-ACTION All Web request has unrecognized action parameter
|
|
245
241
|
# InvalidArguments 303 400 ERROR-INVALID-ARGUMENTS All Incorrect or insufficient arguments supplied for requested action
|
|
@@ -267,6 +263,7 @@ class ZeroconfConnect:
|
|
|
267
263
|
self,
|
|
268
264
|
username:str,
|
|
269
265
|
password:str,
|
|
266
|
+
loginId:str=None,
|
|
270
267
|
delay:float=0.50
|
|
271
268
|
) -> ZeroconfResponse:
|
|
272
269
|
"""
|
|
@@ -276,11 +273,15 @@ class ZeroconfConnect:
|
|
|
276
273
|
|
|
277
274
|
Args:
|
|
278
275
|
username (str):
|
|
279
|
-
Spotify Connect user name to login with.
|
|
276
|
+
Spotify Connect user name to login with (e.g. "yourspotifyusername").
|
|
280
277
|
This MUST match the account name (or one of them) that was used to configure Spotify Connect
|
|
281
278
|
on the manufacturer device.
|
|
282
279
|
password (str):
|
|
283
280
|
Spotify Connect user password to login with.
|
|
281
|
+
loginId (str):
|
|
282
|
+
Spotify Connect login id to login with (e.g. "31l77fd87g8h9j00k89f07jf87ge").
|
|
283
|
+
This is also known as the canonical user id value.
|
|
284
|
+
This MUST be the value that relates to the `username` argument.
|
|
284
285
|
delay (float):
|
|
285
286
|
Time delay (in seconds) to wait AFTER issuing the command to the device.
|
|
286
287
|
This delay will give the spotify zeroconf api time to process the change before
|
|
@@ -297,6 +298,14 @@ class ZeroconfConnect:
|
|
|
297
298
|
This will first issue a call to the `getInfo` Spotify Zeroconf API endpoint to retrieve the Spotify
|
|
298
299
|
Connect device id associated with the device. It will then issue a call to the `addUser` Spotify Zeroconf
|
|
299
300
|
API endpoint to add the user to the device.
|
|
301
|
+
|
|
302
|
+
Some Spotify Connect device types will be "woken up" with the initial `addUser` request; when this happens,
|
|
303
|
+
the initial request will return a 203 status (ERROR-INVALID-PUBLICKEY), and return a valid public key in the
|
|
304
|
+
response. This public key is then used to submit another `addUser` request to connect to the device with the
|
|
305
|
+
newly returned public key. When this happens, a `ZeroconfResponse` object is returned with the result of
|
|
306
|
+
the final `addUser` request. If only one `addUser` request is processed, then a `ZeroconfGetInfo` object is
|
|
307
|
+
returned with the result of the `addUser` request. Note that `ZeroconfGetInfo' inherits from `ZeroconfResponse`,
|
|
308
|
+
so you can always treat the result of this method as a `ZeroconfResponse` object.
|
|
300
309
|
|
|
301
310
|
The login (on the device) is performed asynchronously, so the return result only indicates whether the library
|
|
302
311
|
is able to perform the login attempt. You should issue a call to the Spotify Web API `Get Available Devices`
|
|
@@ -329,6 +338,7 @@ class ZeroconfConnect:
|
|
|
329
338
|
apiMethodParms.AppendKeyValue("CPath", self._CPath)
|
|
330
339
|
apiMethodParms.AppendKeyValue("Version", self._Version)
|
|
331
340
|
apiMethodParms.AppendKeyValue("Uri", self._Uri)
|
|
341
|
+
apiMethodParms.AppendKeyValue("loginId", loginId)
|
|
332
342
|
apiMethodParms.AppendKeyValue("username", username)
|
|
333
343
|
apiMethodParms.AppendKeyValue("password (with mask)", passwordMaskString(password))
|
|
334
344
|
apiMethodParms.AppendKeyValue("delay", delay)
|
|
@@ -343,49 +353,55 @@ class ZeroconfConnect:
|
|
|
343
353
|
|
|
344
354
|
# get the current device id from the device via Spotify ZeroConf API `getInfo` endpoint.
|
|
345
355
|
info:ZeroconfGetInfo = self.GetInformation()
|
|
346
|
-
|
|
347
|
-
# formulate the blob.
|
|
348
|
-
credentials:Credentials = Credentials(username, password)
|
|
349
|
-
builder = BlobBuilder(credentials, info.DeviceId, info.PublicKey)
|
|
350
|
-
blob = builder.build()
|
|
351
|
-
|
|
352
|
-
# set request endpoint.
|
|
353
|
-
endpoint:str = self.GetEndpoint('addUser')
|
|
354
|
-
|
|
355
|
-
# set request headers.
|
|
356
|
-
reqHeaders:dict = {
|
|
357
|
-
'Content-Type': 'application/x-www-form-urlencoded',
|
|
358
|
-
'Connection': 'close'
|
|
359
|
-
}
|
|
360
|
-
_logsi.LogDictionary(SILevel.Debug, "ZeroconfConnect http request: '%s' (headers)" % (endpoint), reqHeaders)
|
|
361
|
-
|
|
362
|
-
# set request parameters.
|
|
363
|
-
reqData={
|
|
364
|
-
'action': 'addUser',
|
|
365
|
-
'VERSION': self._Version,
|
|
366
|
-
'userName': credentials.username.decode('ascii'),
|
|
367
|
-
'clientKey': int_to_b64str(builder.dh_keys.public_key),
|
|
368
|
-
'blob': blob
|
|
369
|
-
}
|
|
370
|
-
_logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http request: '%s' (data)" % (endpoint), reqData)
|
|
371
356
|
|
|
372
|
-
# execute
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
# check response for errors, and return json response.
|
|
381
|
-
responseData:dict = self._CheckResponseForErrors(response, apiMethodName, endpoint)
|
|
357
|
+
# execute the Spotify Zeroconf API addUser request.
|
|
358
|
+
responseData:dict = self._ConnectAddUser(
|
|
359
|
+
info,
|
|
360
|
+
username,
|
|
361
|
+
password,
|
|
362
|
+
loginId,
|
|
363
|
+
apiMethodName)
|
|
382
364
|
|
|
383
365
|
# process results.
|
|
384
|
-
|
|
366
|
+
# we will process the results with a `ZeroconfGetInfo` object, in case a publicKey was returned.
|
|
367
|
+
result = ZeroconfGetInfo(root=responseData)
|
|
385
368
|
|
|
369
|
+
# does result contain a public key?
|
|
370
|
+
# some spotify connect devices need to be "woken up" with the initial `addUser` request.
|
|
371
|
+
# when this happens, the initial request will return a 203 status (ERROR-INVALID-PUBLICKEY), and
|
|
372
|
+
# return a valid public key in the response. we can then take this public key and try the
|
|
373
|
+
# `addUser` request again.
|
|
374
|
+
if (result.Status == 203) and (result.SpotifyError == 0): # and (result.StatusString == "ERROR-INVALID-PUBLICKEY"):
|
|
375
|
+
|
|
376
|
+
# trace.
|
|
377
|
+
_logsi.LogObject(SILevel.Verbose, '%s result (%s)' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
|
|
378
|
+
_logsi.LogVerbose("Spotify Connect addUser request returned an ERROR-INVALID-PUBLICKEY response for Device id '%s'; addUser will be retried with the returned public key" % (info.DeviceId))
|
|
379
|
+
|
|
380
|
+
# give the device a little time to wake up.
|
|
381
|
+
WAKEUP_DELAY:float = 0.25
|
|
382
|
+
_logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % WAKEUP_DELAY)
|
|
383
|
+
time.sleep(WAKEUP_DELAY)
|
|
384
|
+
|
|
385
|
+
# move the newly returned public key to the getInfo results.
|
|
386
|
+
# try the addUser request again, this time with a (hopefully) valid public key.
|
|
387
|
+
info.PublicKey = result.PublicKey
|
|
388
|
+
self._ConnectAddUser(
|
|
389
|
+
info,
|
|
390
|
+
username,
|
|
391
|
+
password,
|
|
392
|
+
loginId,
|
|
393
|
+
apiMethodName)
|
|
394
|
+
|
|
395
|
+
# process results.
|
|
396
|
+
result = ZeroconfResponse(root=responseData)
|
|
397
|
+
|
|
386
398
|
# trace.
|
|
387
|
-
_logsi.LogObject(SILevel.Verbose, '%s result
|
|
388
|
-
|
|
399
|
+
_logsi.LogObject(SILevel.Verbose, '%s result (%s)' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
|
|
400
|
+
|
|
401
|
+
# if result status is not ok, then raise an exception.
|
|
402
|
+
if (result.Status != 101):
|
|
403
|
+
raise SpotifyZeroconfApiError(result.Status, result.ToString(), apiMethodName, result.StatusString, _logsi)
|
|
404
|
+
|
|
389
405
|
# give spotify zeroconf api time to process the change.
|
|
390
406
|
if delay > 0:
|
|
391
407
|
_logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % delay)
|
|
@@ -411,6 +427,56 @@ class ZeroconfConnect:
|
|
|
411
427
|
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
412
428
|
|
|
413
429
|
|
|
430
|
+
def _ConnectAddUser(
|
|
431
|
+
self,
|
|
432
|
+
info:ZeroconfGetInfo,
|
|
433
|
+
username:str,
|
|
434
|
+
password:str,
|
|
435
|
+
loginId:str,
|
|
436
|
+
apiMethodName:str,
|
|
437
|
+
) -> dict:
|
|
438
|
+
"""
|
|
439
|
+
"""
|
|
440
|
+
# formulate the blob.
|
|
441
|
+
credentials:Credentials = Credentials(username, password)
|
|
442
|
+
builder = BlobBuilder(credentials, info.DeviceId, info.PublicKey)
|
|
443
|
+
blob = builder.build()
|
|
444
|
+
|
|
445
|
+
# set request endpoint.
|
|
446
|
+
endpoint:str = self.GetEndpoint('addUser')
|
|
447
|
+
|
|
448
|
+
# set request headers.
|
|
449
|
+
reqHeaders:dict = {
|
|
450
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
451
|
+
'Connection': 'close'
|
|
452
|
+
}
|
|
453
|
+
_logsi.LogDictionary(SILevel.Debug, "ZeroconfConnect http request: '%s' (headers)" % (endpoint), reqHeaders)
|
|
454
|
+
|
|
455
|
+
# set request parameters.
|
|
456
|
+
reqData={
|
|
457
|
+
'action': 'addUser',
|
|
458
|
+
'VERSION': self._Version,
|
|
459
|
+
'userName': credentials.username.decode('ascii'), # user name (e.g. "youremail@mail.com")
|
|
460
|
+
'clientKey': int_to_b64str(builder.dh_keys.public_key),
|
|
461
|
+
'blob': blob,
|
|
462
|
+
'tokenType': 'default', # NOTE - not the same as info.TokenType!
|
|
463
|
+
'loginId': loginId or credentials.username.decode('ascii'), # canonical login id (e.g. "31l77fd87g8h9j00k89f07jf87ge")
|
|
464
|
+
}
|
|
465
|
+
_logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http request: '%s' (data)" % (endpoint), reqData)
|
|
466
|
+
|
|
467
|
+
# execute spotify zeroconf api request.
|
|
468
|
+
response = requests.post(
|
|
469
|
+
self._Uri,
|
|
470
|
+
timeout=10,
|
|
471
|
+
headers=reqHeaders,
|
|
472
|
+
data=reqData # send data in POST request body
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
# check response for initial errors, and return json response.
|
|
476
|
+
responseData:dict = self._CheckResponseForErrors(response, apiMethodName, endpoint)
|
|
477
|
+
return responseData
|
|
478
|
+
|
|
479
|
+
|
|
414
480
|
def Disconnect(
|
|
415
481
|
self,
|
|
416
482
|
delay:float=0.50
|
|
@@ -487,14 +553,18 @@ class ZeroconfConnect:
|
|
|
487
553
|
data=reqData # send data in POST request body
|
|
488
554
|
)
|
|
489
555
|
|
|
490
|
-
# check response for errors, and return json response.
|
|
556
|
+
# check response for initial errors, and return json response.
|
|
491
557
|
responseData:dict = self._CheckResponseForErrors(response, apiMethodName, endpoint)
|
|
492
558
|
|
|
493
559
|
# process results.
|
|
494
560
|
result = ZeroconfResponse(root=responseData)
|
|
495
561
|
|
|
496
562
|
# trace.
|
|
497
|
-
_logsi.LogObject(SILevel.Verbose, '%s result
|
|
563
|
+
_logsi.LogObject(SILevel.Verbose, '%s result (%s)' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
|
|
564
|
+
|
|
565
|
+
# if result status is not ok, then raise an exception.
|
|
566
|
+
if (result.Status != 101):
|
|
567
|
+
raise SpotifyZeroconfApiError(result.Status, result.ToString(), apiMethodName, result.StatusString, _logsi)
|
|
498
568
|
|
|
499
569
|
# give spotify zeroconf api time to process the change.
|
|
500
570
|
if delay > 0:
|
|
@@ -621,7 +691,7 @@ class ZeroconfConnect:
|
|
|
621
691
|
params=reqParams
|
|
622
692
|
)
|
|
623
693
|
|
|
624
|
-
# check response for errors, and return json response.
|
|
694
|
+
# check response for initial errors, and return json response.
|
|
625
695
|
responseData:dict = self._CheckResponseForErrors(response, apiMethodName, endpoint)
|
|
626
696
|
|
|
627
697
|
# process results.
|
|
@@ -629,6 +699,12 @@ class ZeroconfConnect:
|
|
|
629
699
|
|
|
630
700
|
# trace.
|
|
631
701
|
_logsi.LogObject(SILevel.Verbose, '%s result (%s) - "%s" (%s)' % (apiMethodName, type(result).__name__, result.RemoteName, result.DeviceId), result, excludeNonPublic=True)
|
|
702
|
+
|
|
703
|
+
# if result status is not ok, then raise an exception.
|
|
704
|
+
if (result.Status != 101):
|
|
705
|
+
raise SpotifyZeroconfApiError(result.Status, result.ToString(), apiMethodName, result.StatusString, _logsi)
|
|
706
|
+
|
|
707
|
+
# return result to caller.
|
|
632
708
|
return result
|
|
633
709
|
|
|
634
710
|
except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
|
|
@@ -286,6 +286,14 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
286
286
|
"""
|
|
287
287
|
return self._PublicKey
|
|
288
288
|
|
|
289
|
+
@PublicKey.setter
|
|
290
|
+
def PublicKey(self, value:str):
|
|
291
|
+
"""
|
|
292
|
+
Sets the PublicKey property value.
|
|
293
|
+
"""
|
|
294
|
+
if isinstance(value, str):
|
|
295
|
+
self._PublicKey = value
|
|
296
|
+
|
|
289
297
|
|
|
290
298
|
@property
|
|
291
299
|
def RemoteName(self) -> str:
|
|
@@ -343,7 +351,7 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
343
351
|
@property
|
|
344
352
|
def TokenType(self) -> str:
|
|
345
353
|
"""
|
|
346
|
-
Token type provided by the client (e.g. "accesstoken").
|
|
354
|
+
Token type provided by the client (e.g. "accesstoken", "authorization_code", etc).
|
|
347
355
|
"""
|
|
348
356
|
return self._TokenType
|
|
349
357
|
|
|
@@ -29,11 +29,14 @@ class ZeroconfResponse:
|
|
|
29
29
|
|
|
30
30
|
else:
|
|
31
31
|
|
|
32
|
+
# some devices return the `status` and `spotifyError` values as string,
|
|
33
|
+
# while others return them as numeric; use the property setters to
|
|
34
|
+
# convert them to numeric if they are strings that contain a numeric value.
|
|
32
35
|
self._ResponseSource = root.get('responseSource', None)
|
|
33
|
-
self.
|
|
34
|
-
self.
|
|
36
|
+
self.SpotifyError = root.get('spotifyError', None)
|
|
37
|
+
self.Status = root.get('status', None)
|
|
35
38
|
self._StatusString = root.get('statusString', None)
|
|
36
|
-
|
|
39
|
+
|
|
37
40
|
|
|
38
41
|
def __repr__(self) -> str:
|
|
39
42
|
return self.ToString()
|
|
@@ -65,6 +68,9 @@ class ZeroconfResponse:
|
|
|
65
68
|
"""
|
|
66
69
|
if isinstance(value, int):
|
|
67
70
|
self._SpotifyError = value
|
|
71
|
+
elif isinstance(value, str):
|
|
72
|
+
if value.isnumeric():
|
|
73
|
+
self._SpotifyError = int(value)
|
|
68
74
|
|
|
69
75
|
|
|
70
76
|
@property
|
|
@@ -81,6 +87,9 @@ class ZeroconfResponse:
|
|
|
81
87
|
"""
|
|
82
88
|
if isinstance(value, int):
|
|
83
89
|
self._Status = value
|
|
90
|
+
elif isinstance(value, str):
|
|
91
|
+
if value.isnumeric():
|
|
92
|
+
self._Status = int(value)
|
|
84
93
|
|
|
85
94
|
|
|
86
95
|
@property
|
|
File without changes
|
|
File without changes
|
|
File without changes
|