spotifywebapiPython 1.0.68__py3-none-any.whl → 1.0.70__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.68.dist-info → spotifywebapiPython-1.0.70.dist-info}/METADATA +1 -1
- {spotifywebapiPython-1.0.68.dist-info → spotifywebapiPython-1.0.70.dist-info}/RECORD +24 -24
- spotifywebapipython/const.py +3 -3
- spotifywebapipython/docs/__init__.html +16 -2
- spotifywebapipython/docs/index.html +2 -2
- spotifywebapipython/docs/search.js +1 -1
- spotifywebapipython/docs/spotifywebapiPython/const.html +5 -5
- 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 +20 -14
- spotifywebapipython/docs/spotifywebapiPython/spotifydiscovery.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapiauthenticationerror.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapierror.html +2 -2
- spotifywebapipython/models/userprofile.py +2 -2
- spotifywebapipython/models/zeroconfdiscoveryresult.py +3 -3
- spotifywebapipython/spotifyclient.py +13 -7
- spotifywebapipython/zeroconfapi/blobbuilder.py +3 -0
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +124 -77
- {spotifywebapiPython-1.0.68.dist-info → spotifywebapiPython-1.0.70.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.68.dist-info → spotifywebapiPython-1.0.70.dist-info}/WHEEL +0 -0
- {spotifywebapiPython-1.0.68.dist-info → spotifywebapiPython-1.0.70.dist-info}/top_level.txt +0 -0
|
@@ -132,9 +132,9 @@ class ZeroconfConnect:
|
|
|
132
132
|
@property
|
|
133
133
|
def Version(self) -> str:
|
|
134
134
|
"""
|
|
135
|
-
Spotify Connect Zeroconf API version number that the device supports (e.g. "2.10.0").
|
|
135
|
+
Spotify Connect Zeroconf API version number that the device supports (e.g. "", "1.0", "2.10.0", etc.).
|
|
136
136
|
"""
|
|
137
|
-
return self._Version
|
|
137
|
+
return self._Version or ''
|
|
138
138
|
|
|
139
139
|
|
|
140
140
|
def _CheckResponseForErrors(self, response:Response, methodName:str, endpoint:str) -> None:
|
|
@@ -190,7 +190,6 @@ class ZeroconfConnect:
|
|
|
190
190
|
_logsi.LogArray(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json array)" % (response.status_code, response.reason, endpoint), responseData)
|
|
191
191
|
else:
|
|
192
192
|
_logsi.LogObject(SILevel.Verbose, "ZeroconfConnect http response [%s-%s]: '%s' (json object)" % (response.status_code, response.reason, endpoint), responseData)
|
|
193
|
-
pass
|
|
194
193
|
|
|
195
194
|
else:
|
|
196
195
|
|
|
@@ -213,8 +212,6 @@ class ZeroconfConnect:
|
|
|
213
212
|
# just raise a new exception with the non-JSON response data.
|
|
214
213
|
raise SpotifyZeroconfApiError(response.status_code, responseUTF8, methodName, response.reason, _logsi)
|
|
215
214
|
|
|
216
|
-
errMessage:str = None
|
|
217
|
-
|
|
218
215
|
# at this point we know that the response contains JSON data, as
|
|
219
216
|
# we would have raised an exception before this if we could not
|
|
220
217
|
# convert the response to JSON!
|
|
@@ -239,20 +236,6 @@ class ZeroconfConnect:
|
|
|
239
236
|
# InvalidArguments 303 400 ERROR-INVALID-ARGUMENTS All Incorrect or insufficient arguments supplied for requested action
|
|
240
237
|
# SpotifyError 402 200 ERROR-SPOTIFY-ERROR All A Spotify API call returned an error not covered by other error messages
|
|
241
238
|
|
|
242
|
-
# # process results.
|
|
243
|
-
# result = ZeroconfResponse(root=responseData)
|
|
244
|
-
|
|
245
|
-
# # if result is ok, then just return the response data.
|
|
246
|
-
# if (result.Status == 101) \
|
|
247
|
-
# or (result.StatusString == 'OK') \
|
|
248
|
-
# or (result.StatusString == 'ERROR-OK'):
|
|
249
|
-
# return responseData
|
|
250
|
-
|
|
251
|
-
# # was an error status string returned?
|
|
252
|
-
# # if so, then we will raise an exception.
|
|
253
|
-
# if (result.StatusString.startswith('ERROR-')):
|
|
254
|
-
# raise SpotifyZeroconfApiError(result.Status, result.ToString(), methodName, result.StatusString, _logsi)
|
|
255
|
-
|
|
256
239
|
# let the calling function process the returned status.
|
|
257
240
|
return responseData
|
|
258
241
|
|
|
@@ -352,10 +335,11 @@ class ZeroconfConnect:
|
|
|
352
335
|
# get the current device id from the device via Spotify ZeroConf API `getInfo` endpoint.
|
|
353
336
|
info:ZeroconfGetInfo = self.GetInformation()
|
|
354
337
|
|
|
355
|
-
#
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
338
|
+
# are we including origin device information (e.g. deviceName, deviceId)?
|
|
339
|
+
# TODO - maybe tailor this to info.BrandDisplayName and info.ModelDisplayName instead?
|
|
340
|
+
includeOriginDeviceInfo:bool = False
|
|
341
|
+
if (info.PublicKey == 'SU5WQUxJRA==') and (info.Availability == 'NOT-LOADED'):
|
|
342
|
+
includeOriginDeviceInfo = True
|
|
359
343
|
|
|
360
344
|
# execute the Spotify Zeroconf API addUser request.
|
|
361
345
|
responseData:dict = self._ConnectAddUser(
|
|
@@ -363,41 +347,65 @@ class ZeroconfConnect:
|
|
|
363
347
|
username,
|
|
364
348
|
password,
|
|
365
349
|
loginId,
|
|
366
|
-
apiMethodName
|
|
350
|
+
apiMethodName,
|
|
351
|
+
includeOriginDeviceInfo=includeOriginDeviceInfo
|
|
352
|
+
)
|
|
367
353
|
|
|
368
354
|
# process results.
|
|
369
355
|
# we will process the results with a `ZeroconfGetInfo` object, in case a publicKey was returned.
|
|
370
356
|
result = ZeroconfGetInfo(root=responseData)
|
|
371
357
|
|
|
372
|
-
#
|
|
373
|
-
#
|
|
374
|
-
#
|
|
375
|
-
#
|
|
376
|
-
#
|
|
358
|
+
# is the device fully available? The info.Availability property can be used to determine this.
|
|
359
|
+
# if the device is fully available (Availability=""), then the addUser request should return
|
|
360
|
+
# immediately - hopefully with a 0 status (OK) to denote the request was successul.
|
|
361
|
+
# if the device is not fully available (Availability="NOT-LOADED"), then the addUser request will
|
|
362
|
+
# probably return a 203 status (ERROR-INVALID-PUBLICKEY) along with a new PublicKey value.
|
|
363
|
+
# when this happens, we need to wait for the device to become fully available before we retry the
|
|
364
|
+
# addUser request again with the new public key.
|
|
365
|
+
|
|
366
|
+
# did we get a new PublicKey value?
|
|
377
367
|
if (result.Status == 203) and (result.StatusString == "ERROR-INVALID-PUBLICKEY"):
|
|
378
368
|
|
|
379
369
|
# trace.
|
|
380
370
|
_logsi.LogObject(SILevel.Verbose, '%s result (%s)' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
|
|
381
371
|
_logsi.LogVerbose("Spotify Connect addUser request returned an ERROR-INVALID-PUBLICKEY response for Device id '%s'; addUser will be retried with the returned publicKey" % (info.DeviceId))
|
|
382
372
|
|
|
383
|
-
#
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
373
|
+
# wait for the device to be fully loaded.
|
|
374
|
+
# once this happens, the device Availability state will change from "NOT-LOADED" to "".
|
|
375
|
+
loopTotalDelay:float = 0
|
|
376
|
+
LOOP_DELAY:float = 0.25
|
|
377
|
+
LOOP_TIMEOUT:float = 5.0
|
|
378
|
+
while True:
|
|
379
|
+
|
|
380
|
+
# wait just a bit between device info queries.
|
|
381
|
+
_logsi.LogVerbose("Delaying for %s seconds to allow Spotify Connect device id '%s' to become available (currently '%s')" % (LOOP_DELAY, info.DeviceId, info.Availability))
|
|
382
|
+
time.sleep(LOOP_DELAY)
|
|
383
|
+
loopTotalDelay = loopTotalDelay + LOOP_DELAY
|
|
384
|
+
|
|
385
|
+
# get device information; if availability status changes then we are done.
|
|
386
|
+
info:ZeroconfGetInfo = self.GetInformation()
|
|
387
|
+
if info.Availability != "NOT-LOADED":
|
|
388
|
+
_logsi.LogVerbose("Spotify Connect Device id '%s' availability status changed from '%s' to '%s' within %f seconds of initial addUser request" % (info.DeviceId, info.Availability, info.Availability, loopTotalDelay))
|
|
389
|
+
break
|
|
390
|
+
|
|
391
|
+
# only check so many times before we give up;
|
|
392
|
+
if (loopTotalDelay > LOOP_TIMEOUT):
|
|
393
|
+
_logsi.LogWarning("Timed out waiting for Spotify Connect device id '%s' availability to change from '%s'; gave up after %f seconds from initial addUser request" % (info.DeviceId, info.Availability, loopTotalDelay))
|
|
394
|
+
break
|
|
387
395
|
|
|
388
|
-
#
|
|
389
|
-
# try the addUser request again, this time with a (hopefully) valid public key.
|
|
390
|
-
info.PublicKey = result.PublicKey
|
|
396
|
+
# now that the device is (hopefully) fully available, try the addUser request again.
|
|
391
397
|
self._ConnectAddUser(
|
|
392
398
|
info,
|
|
393
399
|
username,
|
|
394
400
|
password,
|
|
395
401
|
loginId,
|
|
396
|
-
apiMethodName
|
|
402
|
+
apiMethodName,
|
|
403
|
+
includeOriginDeviceInfo=includeOriginDeviceInfo
|
|
404
|
+
)
|
|
397
405
|
|
|
398
406
|
# process results.
|
|
399
407
|
result = ZeroconfResponse(root=responseData)
|
|
400
|
-
|
|
408
|
+
|
|
401
409
|
# trace.
|
|
402
410
|
_logsi.LogObject(SILevel.Verbose, '%s result (%s)' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
|
|
403
411
|
|
|
@@ -437,48 +445,87 @@ class ZeroconfConnect:
|
|
|
437
445
|
password:str,
|
|
438
446
|
loginId:str,
|
|
439
447
|
apiMethodName:str,
|
|
448
|
+
includeOriginDeviceInfo:bool=False,
|
|
440
449
|
) -> dict:
|
|
441
450
|
"""
|
|
451
|
+
Execute the `addUser` request.
|
|
442
452
|
"""
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
builder = BlobBuilder(credentials, info.DeviceId, info.PublicKey)
|
|
446
|
-
blob = builder.build()
|
|
453
|
+
apiMethodName:str = '_ConnectAddUser'
|
|
454
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
447
455
|
|
|
448
|
-
|
|
449
|
-
endpoint:str = self.GetEndpoint('addUser')
|
|
456
|
+
try:
|
|
450
457
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
458
|
+
# trace.
|
|
459
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
460
|
+
apiMethodParms.AppendKeyValue("includeOriginDeviceInfo", includeOriginDeviceInfo)
|
|
461
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Issuing Spotify Connect Zeroconf addUser request (ip=%s)" % self._HostIpAddress, apiMethodParms)
|
|
462
|
+
|
|
463
|
+
# validations.
|
|
464
|
+
if (includeOriginDeviceInfo is None):
|
|
465
|
+
includeOriginDeviceInfo = False
|
|
457
466
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
'userName': credentials.username.decode('ascii'), # user name (e.g. "youremail@mail.com")
|
|
463
|
-
'clientKey': int_to_b64str(builder.dh_keys.public_key),
|
|
464
|
-
'blob': blob,
|
|
465
|
-
'tokenType': 'default', # NOTE - not the same as info.TokenType!
|
|
466
|
-
# not sure if this is required or not, so commenting out for now.
|
|
467
|
-
#'loginId': loginId or '', # canonical login id (e.g. "31l77fd87g8h9j00k89f07jf87ge")
|
|
468
|
-
}
|
|
469
|
-
_logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http request: '%s' (data)" % (endpoint), reqData)
|
|
470
|
-
|
|
471
|
-
# execute spotify zeroconf api request.
|
|
472
|
-
response = requests.post(
|
|
473
|
-
self._Uri,
|
|
474
|
-
timeout=10,
|
|
475
|
-
headers=reqHeaders,
|
|
476
|
-
data=reqData # send data in POST request body
|
|
477
|
-
)
|
|
467
|
+
# formulate the blob.
|
|
468
|
+
credentials:Credentials = Credentials(username, password)
|
|
469
|
+
builder = BlobBuilder(credentials, info.DeviceId, info.PublicKey)
|
|
470
|
+
blob = builder.build()
|
|
478
471
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
472
|
+
# set request endpoint.
|
|
473
|
+
endpoint:str = self.GetEndpoint('addUser')
|
|
474
|
+
|
|
475
|
+
# set request headers.
|
|
476
|
+
reqHeaders:dict = {
|
|
477
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
478
|
+
'Connection': 'close'
|
|
479
|
+
}
|
|
480
|
+
_logsi.LogDictionary(SILevel.Debug, "ZeroconfConnect http request: '%s' (headers)" % (endpoint), reqHeaders)
|
|
481
|
+
|
|
482
|
+
# set request parameters.
|
|
483
|
+
reqData={
|
|
484
|
+
'action': 'addUser',
|
|
485
|
+
'version': self.Version,
|
|
486
|
+
'tokenType': 'default', # NOTE - not the same as info.TokenType!
|
|
487
|
+
'loginId': loginId or '', # canonical login id (e.g. "31l77fd87g8h9j00k89f07jf87ge")
|
|
488
|
+
'userName': credentials.username.decode('ascii'), # user name (e.g. "youremail@mail.com")
|
|
489
|
+
'clientKey': int_to_b64str(builder.dh_keys.public_key),
|
|
490
|
+
'blob': blob,
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
# are we including origin device information (e.g. deviceName, deviceId)?
|
|
494
|
+
# the `deviceName` and `deviceId` keys are specified by some manufacturers for the
|
|
495
|
+
# `addUser` action. I am not sure if they are required or not, as they are not part
|
|
496
|
+
# of the Spotify Zeroconf API specification for the `addUser` action.
|
|
497
|
+
if (includeOriginDeviceInfo):
|
|
498
|
+
_logsi.LogVerbose("Origin deviceName and deviceId will be included with the addUser request for deviceId '%s'" % (info.DeviceId))
|
|
499
|
+
reqData['deviceName'] = builder._OriginDeviceName
|
|
500
|
+
reqData['deviceId'] = builder._OriginDeviceId
|
|
501
|
+
|
|
502
|
+
# if PublicKey="INVALID" and availability="NOT-LOADED" then adjust the addUser
|
|
503
|
+
# request to remove the blob and clientKey values. this will inform the device to
|
|
504
|
+
# send us back a PublicKey value, which can be used on a subsequent addUser request.
|
|
505
|
+
if (info.PublicKey == 'SU5WQUxJRA==') and (info.Availability == 'NOT-LOADED'):
|
|
506
|
+
_logsi.LogVerbose("Spotify Connect PublicKey='INVALID' and Availability='NOT-LOADED' detected for Device id '%s'; removing blob and clientKey values" % (info.DeviceId))
|
|
507
|
+
reqData['blob'] = ''
|
|
508
|
+
reqData['clientKey'] = ''
|
|
509
|
+
|
|
510
|
+
# trace.
|
|
511
|
+
_logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http request: '%s' (data)" % (endpoint), reqData)
|
|
512
|
+
|
|
513
|
+
# execute spotify zeroconf api request.
|
|
514
|
+
response = requests.post(
|
|
515
|
+
self._Uri,
|
|
516
|
+
timeout=10,
|
|
517
|
+
headers=reqHeaders,
|
|
518
|
+
data=reqData # send data in POST request body
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
# check response for initial errors, and return json response.
|
|
522
|
+
responseData:dict = self._CheckResponseForErrors(response, apiMethodName, endpoint)
|
|
523
|
+
return responseData
|
|
524
|
+
|
|
525
|
+
finally:
|
|
526
|
+
|
|
527
|
+
# trace.
|
|
528
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
482
529
|
|
|
483
530
|
|
|
484
531
|
def Disconnect(
|
|
@@ -554,7 +601,7 @@ class ZeroconfConnect:
|
|
|
554
601
|
# set request parameters.
|
|
555
602
|
reqData={
|
|
556
603
|
'action': 'resetUsers',
|
|
557
|
-
'version': self.
|
|
604
|
+
'version': self.Version,
|
|
558
605
|
}
|
|
559
606
|
_logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http request: '%s' (data)" % (endpoint), reqData)
|
|
560
607
|
|
|
@@ -641,7 +688,7 @@ class ZeroconfConnect:
|
|
|
641
688
|
return "{uri}?action={action}&version={version}".format(
|
|
642
689
|
uri=self._Uri,
|
|
643
690
|
action=action,
|
|
644
|
-
version=self.
|
|
691
|
+
version=self.Version,
|
|
645
692
|
)
|
|
646
693
|
|
|
647
694
|
|
|
@@ -693,7 +740,7 @@ class ZeroconfConnect:
|
|
|
693
740
|
# set request parameters.
|
|
694
741
|
reqParams={
|
|
695
742
|
'action': 'getInfo',
|
|
696
|
-
'version': self.
|
|
743
|
+
'version': self.Version,
|
|
697
744
|
}
|
|
698
745
|
_logsi.LogDictionary(SILevel.Verbose, "ZeroconfConnect http request: '%s' (params)" % (endpoint), reqParams)
|
|
699
746
|
|
|
@@ -752,5 +799,5 @@ class ZeroconfConnect:
|
|
|
752
799
|
|
|
753
800
|
msg = '%s\n URI="%s"' % (msg, str(self._Uri))
|
|
754
801
|
if self._CPath is not None: msg = '%s\n CPath="%s"' % (msg, str(self._CPath))
|
|
755
|
-
if self._Version is not None: msg = '%s\n Version="%s"' % (msg, str(self.
|
|
802
|
+
if self._Version is not None: msg = '%s\n Version="%s"' % (msg, str(self.Version))
|
|
756
803
|
return msg
|
|
File without changes
|
|
File without changes
|
|
File without changes
|