spotifywebapiPython 1.0.72__py3-none-any.whl → 1.0.74__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.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/METADATA +1 -1
- {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/RECORD +30 -29
- spotifywebapipython/const.py +1 -1
- spotifywebapipython/docs/__init__.html +20 -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 +8 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifydiscovery.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapiauthenticationerror.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapierror.html +2 -2
- spotifywebapipython/models/spotifyconnectdevices.py +53 -27
- spotifywebapipython/models/zeroconfdiscoveryresult.py +13 -0
- spotifywebapipython/spotifyclient.py +68 -20
- spotifywebapipython/zeroconfapi/__init__.py +2 -0
- spotifywebapipython/zeroconfapi/authenticationtypes.py +38 -0
- spotifywebapipython/zeroconfapi/blobbuilder.py +333 -66
- spotifywebapipython/zeroconfapi/credentials.py +18 -2
- spotifywebapipython/zeroconfapi/cryptodiffiehellman.py +71 -6
- spotifywebapipython/zeroconfapi/helpers.py +34 -7
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +77 -40
- spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +69 -18
- {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/WHEEL +0 -0
- {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/top_level.txt +0 -0
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import secrets
|
|
2
2
|
|
|
3
|
-
from .helpers import byte_list_to_int
|
|
3
|
+
from .helpers import byte_list_to_int, int_to_bytes, b64_to_int, int_to_b64str
|
|
4
4
|
|
|
5
5
|
class CryptoDiffieHellman:
|
|
6
|
+
"""
|
|
7
|
+
Diffie Hellman cryptography helper class.
|
|
8
|
+
|
|
9
|
+
The following link does a pretty good job of explaining this process:
|
|
10
|
+
https://www.comparitech.com/blog/information-security/diffie-hellman-key-exchange/
|
|
11
|
+
"""
|
|
6
12
|
|
|
7
13
|
DH_GENERATOR = 2
|
|
8
|
-
|
|
14
|
+
DH_PRIME_MODULUS = byte_list_to_int([
|
|
9
15
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2,
|
|
10
16
|
0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67,
|
|
11
17
|
0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e,
|
|
@@ -16,9 +22,68 @@ class CryptoDiffieHellman:
|
|
|
16
22
|
|
|
17
23
|
|
|
18
24
|
def __init__(self):
|
|
19
|
-
|
|
20
|
-
|
|
25
|
+
"""
|
|
26
|
+
Initializes a new instance of the class.
|
|
27
|
+
"""
|
|
28
|
+
# generate our private key value.
|
|
29
|
+
self._PrivateKey = secrets.randbits(95 * 8)
|
|
30
|
+
|
|
31
|
+
# generate a public key that we can share with a partner.
|
|
32
|
+
self._PublicKey:int = pow(CryptoDiffieHellman.DH_GENERATOR, self._PrivateKey, CryptoDiffieHellman.DH_PRIME_MODULUS)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def PublicKey(self) -> int:
|
|
37
|
+
"""
|
|
38
|
+
Returns our public key in an integer format.
|
|
39
|
+
"""
|
|
40
|
+
return self._PublicKey
|
|
41
|
+
|
|
42
|
+
@PublicKey.setter
|
|
43
|
+
def PublicKey(self, value:int):
|
|
44
|
+
"""
|
|
45
|
+
Sets the PublicKey property value.
|
|
46
|
+
"""
|
|
47
|
+
if isinstance(value, int):
|
|
48
|
+
self._PublicKey = value
|
|
49
|
+
|
|
21
50
|
|
|
51
|
+
@property
|
|
52
|
+
def PublicKeyBase64String(self) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Returns our public key in a base64 encoded string format.
|
|
55
|
+
"""
|
|
56
|
+
return int_to_b64str(self._PublicKey)
|
|
57
|
+
|
|
22
58
|
|
|
23
|
-
def
|
|
24
|
-
|
|
59
|
+
def SharedSecret(self, remotePublicKey:int):
|
|
60
|
+
"""
|
|
61
|
+
Returns a shared secret value that is calculated based upon the specified `remotePublicKey`
|
|
62
|
+
value, our private key value, and a shared Modulus value.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
remotePublicKey (int):
|
|
66
|
+
The Remote clients PublicKey value.
|
|
67
|
+
|
|
68
|
+
The shared secret value can be calculated as:
|
|
69
|
+
SharedSecret = remotePublicKey(base,g) ** PrivateKey(secret,a) % DH_PRIME(modulus,p)
|
|
70
|
+
SharedSecret = g**a mod p
|
|
71
|
+
SharedSecret = base ** secret % modulus
|
|
72
|
+
|
|
73
|
+
- base = remotePublicKey
|
|
74
|
+
- secret = PrivateKey
|
|
75
|
+
- modulus = DH_PRIME
|
|
76
|
+
"""
|
|
77
|
+
# calculate the shared secret value based upon the remote's PublicKey, our PrivateKey, and the shared Modulus.
|
|
78
|
+
|
|
79
|
+
# another way to express this formula is:
|
|
80
|
+
# SharedSecret = g**a mod p
|
|
81
|
+
|
|
82
|
+
# another way to express this formula is:
|
|
83
|
+
# SharedSecret = base ** secret % modulus
|
|
84
|
+
|
|
85
|
+
# - base or `g` = remotePublicKey
|
|
86
|
+
# - secret or `a` = PrivateKey
|
|
87
|
+
# - modulus or `p` = DH_PRIME
|
|
88
|
+
return pow(remotePublicKey, self._PrivateKey, CryptoDiffieHellman.DH_PRIME_MODULUS)
|
|
89
|
+
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from base64 import b64decode, b64encode
|
|
2
|
+
import binascii
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
def write_int(i: int, out: bytearray):
|
|
@@ -14,17 +15,43 @@ def write_bytes(b: bytes, out: bytearray):
|
|
|
14
15
|
out.extend(b)
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
def byte_list_to_int(byte_list):
|
|
18
|
+
def byte_list_to_int(byte_list) -> int:
|
|
19
|
+
"""
|
|
20
|
+
Converts a list of bytes to a big integer.
|
|
21
|
+
"""
|
|
18
22
|
return int.from_bytes(bytes(byte_list), 'big')
|
|
19
23
|
|
|
20
24
|
|
|
21
|
-
def int_to_bytes(
|
|
22
|
-
|
|
25
|
+
def int_to_bytes(value: int) -> bytes:
|
|
26
|
+
"""
|
|
27
|
+
Converts a big integer to bytes.
|
|
28
|
+
"""
|
|
29
|
+
return value.to_bytes((value.bit_length() + 7) // 8, byteorder='big') or b'\0'
|
|
23
30
|
|
|
24
31
|
|
|
25
|
-
def int_to_b64str(
|
|
26
|
-
|
|
32
|
+
def int_to_b64str(value: int) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Converts a big integer to a base64-encoded string value.
|
|
35
|
+
"""
|
|
36
|
+
return b64encode(int_to_bytes(value)).decode('ascii')
|
|
27
37
|
|
|
28
38
|
|
|
29
|
-
def b64_to_int(
|
|
30
|
-
|
|
39
|
+
def b64_to_int(value: str):
|
|
40
|
+
"""
|
|
41
|
+
Converts a base64-encoded string or byte array to a big integer.
|
|
42
|
+
"""
|
|
43
|
+
return int.from_bytes(b64decode(value), 'big')
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def string_to_int(value):
|
|
47
|
+
"""
|
|
48
|
+
Converts a string to a big integer.
|
|
49
|
+
"""
|
|
50
|
+
return int(binascii.hexlify(value), 16)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def int_of_string(value):
|
|
54
|
+
"""
|
|
55
|
+
Converts a string to a big integer.
|
|
56
|
+
"""
|
|
57
|
+
return int(binascii.hexlify(value), 16)
|
|
@@ -6,7 +6,7 @@ import time
|
|
|
6
6
|
|
|
7
7
|
# our package imports.
|
|
8
8
|
from .blobbuilder import BlobBuilder
|
|
9
|
-
from .credentials import Credentials
|
|
9
|
+
from .credentials import Credentials, AuthenticationTypes
|
|
10
10
|
from .helpers import int_to_b64str
|
|
11
11
|
from .spotifyzeroconfapierror import SpotifyZeroconfApiError
|
|
12
12
|
from .zeroconfresponse import ZeroconfResponse
|
|
@@ -42,6 +42,12 @@ class ZeroconfConnect:
|
|
|
42
42
|
GitHub repository.
|
|
43
43
|
"""
|
|
44
44
|
|
|
45
|
+
SPOTIFY_CONNECT_CLIENT_VERSION_DEFAULT:str = '2.7.1'
|
|
46
|
+
"""
|
|
47
|
+
Spotify Connect default version string ('2.7.1').
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
|
|
45
51
|
def __init__(self,
|
|
46
52
|
hostIpAddress:str,
|
|
47
53
|
hostIpPort:int,
|
|
@@ -72,6 +78,8 @@ class ZeroconfConnect:
|
|
|
72
78
|
# validations.
|
|
73
79
|
if (useSSL is None) or (not isinstance(useSSL,bool)):
|
|
74
80
|
useSSL = False
|
|
81
|
+
if (version is None) or (not isinstance(version,str)):
|
|
82
|
+
version = ZeroconfConnect.SPOTIFY_CONNECT_CLIENT_VERSION_DEFAULT
|
|
75
83
|
|
|
76
84
|
# initialize storage.
|
|
77
85
|
self._CPath:str = cpath
|
|
@@ -208,10 +216,15 @@ class ZeroconfConnect:
|
|
|
208
216
|
responseUTF8 = response.content.decode('utf-8')
|
|
209
217
|
_logsi.LogText(SILevel.Error, "ZeroconfConnect http response [%s-%s]: '%s' (utf-8)" % (response.status_code, response.reason, endpoint), responseUTF8)
|
|
210
218
|
|
|
211
|
-
# at this point we don't know what Spotify Web Api returned
|
|
212
|
-
#
|
|
213
|
-
raise
|
|
214
|
-
|
|
219
|
+
# at this point we don't know what Spotify Web Api returned!
|
|
220
|
+
# it's been found that some devices (Sonos, etc) do not return a proper JSON response.
|
|
221
|
+
# raise a new exception with the non-JSON response data if the http status is not 200 (OK).
|
|
222
|
+
if (response.status_code != 200):
|
|
223
|
+
raise SpotifyZeroconfApiError(response.status_code, responseUTF8, methodName, response.reason, _logsi)
|
|
224
|
+
|
|
225
|
+
# trace.
|
|
226
|
+
_logsi.LogVerbose("Spotify Connect Device (ip=%s) returned an invalid JSON response, but the http status code was 200 so we will ignore it" % (self._HostIpAddress))
|
|
227
|
+
|
|
215
228
|
# at this point we know that the response contains JSON data, as
|
|
216
229
|
# we would have raised an exception before this if we could not
|
|
217
230
|
# convert the response to JSON!
|
|
@@ -335,14 +348,6 @@ class ZeroconfConnect:
|
|
|
335
348
|
# get the current device id from the device via Spotify ZeroConf API `getInfo` endpoint.
|
|
336
349
|
info:ZeroconfGetInfo = self.GetInformation()
|
|
337
350
|
|
|
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
|
|
343
|
-
elif info.TokenType == 'authorization_code':
|
|
344
|
-
includeOriginDeviceInfo = True
|
|
345
|
-
|
|
346
351
|
# execute the Spotify Zeroconf API addUser request.
|
|
347
352
|
responseData:dict = self._ConnectAddUser(
|
|
348
353
|
info,
|
|
@@ -350,7 +355,6 @@ class ZeroconfConnect:
|
|
|
350
355
|
password,
|
|
351
356
|
loginId,
|
|
352
357
|
apiMethodName,
|
|
353
|
-
includeOriginDeviceInfo=includeOriginDeviceInfo
|
|
354
358
|
)
|
|
355
359
|
|
|
356
360
|
# process results.
|
|
@@ -402,7 +406,6 @@ class ZeroconfConnect:
|
|
|
402
406
|
password,
|
|
403
407
|
loginId,
|
|
404
408
|
apiMethodName,
|
|
405
|
-
includeOriginDeviceInfo=includeOriginDeviceInfo
|
|
406
409
|
)
|
|
407
410
|
|
|
408
411
|
# process results.
|
|
@@ -447,7 +450,6 @@ class ZeroconfConnect:
|
|
|
447
450
|
password:str,
|
|
448
451
|
loginId:str,
|
|
449
452
|
apiMethodName:str,
|
|
450
|
-
includeOriginDeviceInfo:bool=False,
|
|
451
453
|
) -> dict:
|
|
452
454
|
"""
|
|
453
455
|
Execute the `addUser` request.
|
|
@@ -459,17 +461,14 @@ class ZeroconfConnect:
|
|
|
459
461
|
|
|
460
462
|
# trace.
|
|
461
463
|
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
462
|
-
apiMethodParms.AppendKeyValue("includeOriginDeviceInfo", includeOriginDeviceInfo)
|
|
463
464
|
apiMethodParms.AppendKeyValue("info.Availability", info.Availability)
|
|
464
465
|
apiMethodParms.AppendKeyValue("info.PublicKey", info.PublicKey)
|
|
466
|
+
apiMethodParms.AppendKeyValue("info.DeviceId", info.DeviceId)
|
|
467
|
+
apiMethodParms.AppendKeyValue("info.RemoteName", info.RemoteName)
|
|
465
468
|
_logsi.LogMethodParmList(SILevel.Verbose, "Issuing Spotify Connect Zeroconf addUser request (ip=%s)" % self._HostIpAddress, apiMethodParms)
|
|
466
469
|
|
|
467
|
-
# validations.
|
|
468
|
-
if (includeOriginDeviceInfo is None):
|
|
469
|
-
includeOriginDeviceInfo = False
|
|
470
|
-
|
|
471
470
|
# formulate the blob.
|
|
472
|
-
credentials:Credentials = Credentials(username, password)
|
|
471
|
+
credentials:Credentials = Credentials(username, password, AuthenticationTypes.USER_PASS)
|
|
473
472
|
builder = BlobBuilder(credentials, info.DeviceId, info.PublicKey)
|
|
474
473
|
blob = builder.build()
|
|
475
474
|
|
|
@@ -482,29 +481,41 @@ class ZeroconfConnect:
|
|
|
482
481
|
'Connection': 'close'
|
|
483
482
|
}
|
|
484
483
|
_logsi.LogDictionary(SILevel.Debug, "ZeroconfConnect http request: '%s' (headers)" % (endpoint), reqHeaders)
|
|
485
|
-
|
|
486
|
-
# set
|
|
487
|
-
#
|
|
488
|
-
#
|
|
484
|
+
|
|
485
|
+
# set defaults used by most manufacturers - we will tailor these in the
|
|
486
|
+
# logic below for specific manufacturers / devices as required:
|
|
487
|
+
# - clientKey should be the getInfo response PublicKey (base64 encoded).
|
|
488
|
+
# - exclude origin device information from the addUser request.
|
|
489
|
+
# - tokenType should be set to 'default'.
|
|
490
|
+
clientKey:str = builder.dh_keys.PublicKeyBase64String
|
|
491
|
+
includeOriginDeviceInfo:bool = False
|
|
489
492
|
tokenType:str = 'default'
|
|
490
|
-
if info.TokenType == 'authorization_code':
|
|
491
|
-
tokenType = info.TokenType
|
|
492
493
|
|
|
493
|
-
#
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
494
|
+
# include the origin device information if PublicKey="INVALID" (base64 encoded) and availability="NOT-LOADED".
|
|
495
|
+
if (info.PublicKey == 'SU5WQUxJRA==') and (info.Availability == 'NOT-LOADED'):
|
|
496
|
+
includeOriginDeviceInfo = True
|
|
497
|
+
|
|
498
|
+
# special processing for tokenType "authorization_code":
|
|
499
|
+
# - include origin device information in the addUser request.
|
|
500
|
+
# - set the tokenType to 'authorization_code'.
|
|
501
|
+
# - set the clientKey value to null, as it is contained in the token.
|
|
502
|
+
if info.TokenType == 'authorization_code':
|
|
498
503
|
clientKey = ''
|
|
499
|
-
|
|
504
|
+
includeOriginDeviceInfo = True
|
|
505
|
+
tokenType = info.TokenType
|
|
506
|
+
|
|
507
|
+
# TODO - not sure of the blob credentials layout for AuthenticationTypes.SPOTIFY_TOKEN requests.
|
|
508
|
+
# get a Spotify WebServices API auth token that the Sonos device can use.
|
|
509
|
+
#credentials:Credentials = Credentials(username, authToken, AuthenticationTypes.SPOTIFY_TOKEN) # authToken is the password
|
|
510
|
+
#credentials:Credentials = Credentials('', authToken, AuthenticationTypes.SPOTIFY_TOKEN) # authToken is the password
|
|
511
|
+
#builder = BlobBuilder(credentials, info.DeviceId, info.PublicKey)
|
|
512
|
+
#blob = builder.build()
|
|
513
|
+
|
|
500
514
|
# set request parameters.
|
|
501
515
|
reqData={
|
|
502
516
|
'action': 'addUser',
|
|
503
517
|
'version': info.Version,
|
|
504
518
|
'tokenType': tokenType,
|
|
505
|
-
#'version': self.Version,
|
|
506
|
-
#'tokenType': 'default', # NOTE - not the same as info.TokenType!
|
|
507
|
-
#'clientKey': int_to_b64str(builder.dh_keys.public_key),
|
|
508
519
|
'clientKey': clientKey,
|
|
509
520
|
'loginId': loginId or '', # canonical login id (e.g. "31l77fd87g8h9j00k89f07jf87ge")
|
|
510
521
|
'userName': credentials.username.decode('ascii'), # user name (e.g. "youremail@mail.com")
|
|
@@ -643,9 +654,32 @@ class ZeroconfConnect:
|
|
|
643
654
|
# trace.
|
|
644
655
|
_logsi.LogObject(SILevel.Verbose, '%s result (%s)' % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
|
|
645
656
|
|
|
646
|
-
#
|
|
657
|
+
# was a status value returned?
|
|
658
|
+
# it's been found that some devices (Sonos, etc) do not return a proper JSON response.
|
|
659
|
+
# for these cases, we will rely on the http status code for the response code.
|
|
660
|
+
if (result.Status is None):
|
|
661
|
+
|
|
662
|
+
# was a good http status code returned?
|
|
663
|
+
if response.status_code == 200:
|
|
664
|
+
|
|
665
|
+
# yes - default status results if they are not set.
|
|
666
|
+
result.Status = 101
|
|
667
|
+
if result.StatusString is None:
|
|
668
|
+
result.StatusString = "OK"
|
|
669
|
+
if result.SpotifyError is None:
|
|
670
|
+
result.SpotifyError = 0
|
|
671
|
+
|
|
672
|
+
# are we processing result status?
|
|
647
673
|
if (not ignoreStatusResult):
|
|
648
|
-
|
|
674
|
+
|
|
675
|
+
if (result.Status is None) and (response.status_code != 200):
|
|
676
|
+
# if status value was not returned and http status is not 200, then
|
|
677
|
+
# raise an exception with http status details.
|
|
678
|
+
raise SpotifyZeroconfApiError(response.status_code, responseData, apiMethodName, response.reason, _logsi)
|
|
679
|
+
|
|
680
|
+
elif (result.Status != 101):
|
|
681
|
+
# if status was returned and it's not ok, then raise an exception
|
|
682
|
+
# with JSON response details.
|
|
649
683
|
raise SpotifyZeroconfApiError(result.Status, result.ToString(), apiMethodName, result.StatusString, _logsi)
|
|
650
684
|
|
|
651
685
|
# give spotify zeroconf api time to process the change.
|
|
@@ -724,6 +758,9 @@ class ZeroconfConnect:
|
|
|
724
758
|
Raises:
|
|
725
759
|
SpotifyWebApiError:
|
|
726
760
|
If the Spotify Zeroconf API request response contains error information.
|
|
761
|
+
|
|
762
|
+
The request will timeout after 5 seconds and an exception raised if the device cannot be reached
|
|
763
|
+
or does not respond within that time frame.
|
|
727
764
|
|
|
728
765
|
<details>
|
|
729
766
|
<summary>Sample Code</summary>
|
|
@@ -768,7 +805,7 @@ class ZeroconfConnect:
|
|
|
768
805
|
# execute spotify zeroconf api request.
|
|
769
806
|
response = requests.get(
|
|
770
807
|
self._Uri,
|
|
771
|
-
timeout=
|
|
808
|
+
timeout=5,
|
|
772
809
|
headers=reqHeaders,
|
|
773
810
|
params=reqParams
|
|
774
811
|
)
|
|
@@ -133,7 +133,7 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
133
133
|
|
|
134
134
|
Device aliases will show up as separate devices in the Spotify app.
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
A maximum of 8 aliases are supported (SP_MAX_DEVICE_ALIASES).
|
|
137
137
|
|
|
138
138
|
Please refer to the `RemoteName` property for more information.
|
|
139
139
|
"""
|
|
@@ -146,9 +146,12 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
146
146
|
The SpZeroConfVars availability field returned by SpZeroConfGetVars.
|
|
147
147
|
|
|
148
148
|
The following are values that I have encountered thus far:
|
|
149
|
-
- ""
|
|
149
|
+
- "" - Device is available and ready for use.
|
|
150
150
|
- "UNAVAILABLE" - Device is unavailable, and should probably be rebooted.
|
|
151
|
-
- "NOT-LOADED"
|
|
151
|
+
- "NOT-LOADED" - Spotify SDK / API is not loaded.
|
|
152
|
+
|
|
153
|
+
The maximum length of the availability string (SP_MAX_AVAILABILITY_LENGTH)
|
|
154
|
+
is 15 characters (not counting terminating NULL).
|
|
152
155
|
"""
|
|
153
156
|
return self._Availability
|
|
154
157
|
|
|
@@ -157,6 +160,9 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
157
160
|
def BrandDisplayName(self) -> str:
|
|
158
161
|
"""
|
|
159
162
|
A UTF-8-encoded brand name of the hardware device, for hardware integrations (e.g. "Bose", "Onkyo", etc).
|
|
163
|
+
|
|
164
|
+
The maximum length of the brand display name (SP_MAX_BRAND_NAME_LENGTH)
|
|
165
|
+
is 32 characters (not counting terminating NULL).
|
|
160
166
|
"""
|
|
161
167
|
return self._BrandDisplayName
|
|
162
168
|
|
|
@@ -165,6 +171,9 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
165
171
|
def ClientId(self) -> str:
|
|
166
172
|
"""
|
|
167
173
|
Client id of the application (e.g. "79ebcb219e8e4e123456789000123456").
|
|
174
|
+
|
|
175
|
+
The maximum length of the client ID value (SP_MAX_CLIENT_ID_LENGTH)
|
|
176
|
+
is 32 characters (not counting terminating NULL).
|
|
168
177
|
"""
|
|
169
178
|
return self._ClientId
|
|
170
179
|
|
|
@@ -173,6 +182,9 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
173
182
|
def DeviceId(self) -> str:
|
|
174
183
|
"""
|
|
175
184
|
Unique device ID used for ZeroConf logins (e.g. "30fbc80e35598f3c242f2120413c943dfd9715fe").
|
|
185
|
+
|
|
186
|
+
The maximum length of the device ID value used for ZeroConf logins (SP_MAX_DEVICE_ID_LENGTH)
|
|
187
|
+
is 64 characters (not counting terminating NULL).
|
|
176
188
|
"""
|
|
177
189
|
return self._DeviceId
|
|
178
190
|
|
|
@@ -191,20 +203,23 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
191
203
|
Type of the device (e.g. "SPEAKER", "AVR", etc).
|
|
192
204
|
|
|
193
205
|
Can be any of the following `SpDeviceType` devices:
|
|
194
|
-
-
|
|
195
|
-
-
|
|
196
|
-
-
|
|
197
|
-
-
|
|
198
|
-
-
|
|
199
|
-
-
|
|
200
|
-
-
|
|
201
|
-
-
|
|
202
|
-
-
|
|
203
|
-
-
|
|
204
|
-
-
|
|
205
|
-
-
|
|
206
|
-
-
|
|
207
|
-
-
|
|
206
|
+
- Computer Laptop or desktop computer device.
|
|
207
|
+
- Tablet Tablet PC device.
|
|
208
|
+
- Smartphone Smartphone device.
|
|
209
|
+
- Speaker Speaker device.
|
|
210
|
+
- TV Television device.
|
|
211
|
+
- AVR Audio/Video receiver device.
|
|
212
|
+
- STB Set-Top Box device.
|
|
213
|
+
- AudioDongle Audio dongle device.
|
|
214
|
+
- GameConsole Game console device.
|
|
215
|
+
- CastVideo Chromecast Video.
|
|
216
|
+
- CastAudio Chromecast Audio.
|
|
217
|
+
- Automobile Automobile.
|
|
218
|
+
- Smartwatch Smartwatch.
|
|
219
|
+
- Chromebook Chromebook.
|
|
220
|
+
|
|
221
|
+
The maximum length of the device type string (SP_MAX_DEVICE_TYPE_LENGTH)
|
|
222
|
+
is 15 characters (not counting terminating NULL).
|
|
208
223
|
"""
|
|
209
224
|
return self._DeviceType
|
|
210
225
|
|
|
@@ -221,6 +236,9 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
221
236
|
def GroupStatus(self) -> str:
|
|
222
237
|
"""
|
|
223
238
|
The SpZeroConfVars group_status field returned by SpZeroConfGetVars (e.g. "NONE").
|
|
239
|
+
|
|
240
|
+
The maximum length of the group status string (SP_MAX_GROUP_STATUS_LENGTH)
|
|
241
|
+
is 15 characters (not counting terminating NULL).
|
|
224
242
|
"""
|
|
225
243
|
return self._GroupStatus
|
|
226
244
|
|
|
@@ -255,6 +273,19 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
255
273
|
return False
|
|
256
274
|
|
|
257
275
|
|
|
276
|
+
@property
|
|
277
|
+
def IsBrandSonos(self) -> bool:
|
|
278
|
+
"""
|
|
279
|
+
Returns True if the device is a 'Sonos' branded device; otherwise, False.
|
|
280
|
+
|
|
281
|
+
Determination is made based upon the `BrandDisplayName` property value.
|
|
282
|
+
"""
|
|
283
|
+
result:bool = False
|
|
284
|
+
if (isinstance(self._BrandDisplayName, str)) and (self._BrandDisplayName.lower().strip() == 'sonos'):
|
|
285
|
+
result = True
|
|
286
|
+
return result
|
|
287
|
+
|
|
288
|
+
|
|
258
289
|
@property
|
|
259
290
|
def LibraryVersion(self) -> str:
|
|
260
291
|
"""
|
|
@@ -267,6 +298,9 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
267
298
|
def ModelDisplayName(self) -> str:
|
|
268
299
|
"""
|
|
269
300
|
A UTF-8-encoded model name of the hardware device, for hardware integrations (e.g. "Soundtouch").
|
|
301
|
+
|
|
302
|
+
The maximum length of the model display name (SP_MAX_MODEL_NAME_LENGTH)
|
|
303
|
+
is 30 characters (not counting terminating NULL).
|
|
270
304
|
"""
|
|
271
305
|
return self._ModelDisplayName
|
|
272
306
|
|
|
@@ -283,6 +317,9 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
283
317
|
def PublicKey(self) -> str:
|
|
284
318
|
"""
|
|
285
319
|
Public key used in ZeroConf logins (e.g. "G+ZM4irhc...").
|
|
320
|
+
|
|
321
|
+
The maximum length of the public key used in ZeroConf logins (SP_MAX_PUBLIC_KEY_LENGTH)
|
|
322
|
+
is 149 characters (not counting terminating NULL).
|
|
286
323
|
"""
|
|
287
324
|
return self._PublicKey
|
|
288
325
|
|
|
@@ -328,6 +365,9 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
328
365
|
def Scope(self) -> str:
|
|
329
366
|
"""
|
|
330
367
|
OAuth scope requested when authenticating with the Spotify backend (e.g. "streaming").
|
|
368
|
+
|
|
369
|
+
The maximum length of the token type string (SP_MAX_SCOPE_LENGTH)
|
|
370
|
+
is 64 characters (not counting terminating NULL).
|
|
331
371
|
"""
|
|
332
372
|
return self._Scope
|
|
333
373
|
|
|
@@ -344,6 +384,8 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
344
384
|
def SupportedDrmMediaFormats(self) -> list:
|
|
345
385
|
"""
|
|
346
386
|
The SpZeroConfVars supported_drm_media_formats field returned by SpZeroConfGetVars (e.g. []).
|
|
387
|
+
|
|
388
|
+
A maximum of 8 formats are supported (SP_MAX_SUPPORTED_FORMATS).
|
|
347
389
|
"""
|
|
348
390
|
return self._SupportedDrmMediaFormats
|
|
349
391
|
|
|
@@ -351,7 +393,13 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
351
393
|
@property
|
|
352
394
|
def TokenType(self) -> str:
|
|
353
395
|
"""
|
|
354
|
-
Token type provided by the client
|
|
396
|
+
Token type provided by the client:
|
|
397
|
+
- "accesstoken" Access token.
|
|
398
|
+
- "authorization_code" OAuth Authorization Code token.
|
|
399
|
+
- "default" Default access token.
|
|
400
|
+
|
|
401
|
+
The maximum length of the token type string (SP_MAX_TOKEN_TYPE_LENGTH)
|
|
402
|
+
is 30 characters (not counting terminating NULL).
|
|
355
403
|
"""
|
|
356
404
|
return self._TokenType
|
|
357
405
|
|
|
@@ -360,6 +408,9 @@ class ZeroconfGetInfo(ZeroconfResponse):
|
|
|
360
408
|
def Version(self) -> str:
|
|
361
409
|
"""
|
|
362
410
|
ZeroConf API version number (e.g. "2.10.0").
|
|
411
|
+
|
|
412
|
+
The maximum length of the library version string (SP_MAX_VERSION_LENGTH)
|
|
413
|
+
is 30 characters (not counting terminating NULL).
|
|
363
414
|
"""
|
|
364
415
|
return self._Version
|
|
365
416
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|