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.

Files changed (30) hide show
  1. {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/METADATA +1 -1
  2. {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/RECORD +30 -29
  3. spotifywebapipython/const.py +1 -1
  4. spotifywebapipython/docs/__init__.html +20 -2
  5. spotifywebapipython/docs/index.html +2 -2
  6. spotifywebapipython/docs/search.js +1 -1
  7. spotifywebapipython/docs/spotifywebapiPython/const.html +3 -3
  8. spotifywebapipython/docs/spotifywebapiPython/saappmessages.html +2 -2
  9. spotifywebapipython/docs/spotifywebapiPython/sautils.html +2 -2
  10. spotifywebapipython/docs/spotifywebapiPython/spotifyapierror.html +2 -2
  11. spotifywebapipython/docs/spotifywebapiPython/spotifyapimessage.html +2 -2
  12. spotifywebapipython/docs/spotifywebapiPython/spotifyauthtoken.html +2 -2
  13. spotifywebapipython/docs/spotifywebapiPython/spotifyclient.html +8 -2
  14. spotifywebapipython/docs/spotifywebapiPython/spotifydiscovery.html +2 -2
  15. spotifywebapipython/docs/spotifywebapiPython/spotifywebapiauthenticationerror.html +2 -2
  16. spotifywebapipython/docs/spotifywebapiPython/spotifywebapierror.html +2 -2
  17. spotifywebapipython/models/spotifyconnectdevices.py +53 -27
  18. spotifywebapipython/models/zeroconfdiscoveryresult.py +13 -0
  19. spotifywebapipython/spotifyclient.py +68 -20
  20. spotifywebapipython/zeroconfapi/__init__.py +2 -0
  21. spotifywebapipython/zeroconfapi/authenticationtypes.py +38 -0
  22. spotifywebapipython/zeroconfapi/blobbuilder.py +333 -66
  23. spotifywebapipython/zeroconfapi/credentials.py +18 -2
  24. spotifywebapipython/zeroconfapi/cryptodiffiehellman.py +71 -6
  25. spotifywebapipython/zeroconfapi/helpers.py +34 -7
  26. spotifywebapipython/zeroconfapi/zeroconfconnect.py +77 -40
  27. spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +69 -18
  28. {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/LICENSE +0 -0
  29. {spotifywebapiPython-1.0.72.dist-info → spotifywebapiPython-1.0.74.dist-info}/WHEEL +0 -0
  30. {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
- DH_PRIME = byte_list_to_int([
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
- self._private_key = secrets.randbits(95 * 8)
20
- self.public_key = pow(CryptoDiffieHellman.DH_GENERATOR, self._private_key, CryptoDiffieHellman.DH_PRIME)
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 shared_secret(self, remote_key):
24
- return pow(remote_key, self._private_key, CryptoDiffieHellman.DH_PRIME)
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(i: int):
22
- return i.to_bytes((i.bit_length() + 7) // 8, byteorder='big')
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(i: int):
26
- return b64encode(int_to_bytes(i)).decode('ascii')
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(b64: str):
30
- return int.from_bytes(b64decode(b64), 'big')
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, so let's
212
- # just raise a new exception with the non-JSON response data.
213
- raise SpotifyZeroconfApiError(response.status_code, responseUTF8, methodName, response.reason, _logsi)
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 tokenType based on getInfo token type.
487
- # if TokenType is 'athorization_code', then use it as-is;
488
- # otherwise, use 'default'.
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
- # set clientKey based on getInfo token type.
494
- # if TokenType is 'athorization_code', then clientKey should be null.
495
- # otherwise, clientKey should be the PublicKey (base64 encoded).
496
- clientKey:str = int_to_b64str(builder.dh_keys.public_key)
497
- if info.TokenType == 'authorization_code':
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
- # if result status is not ok, then raise an exception.
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
- if (result.Status != 101):
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=10,
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
- Note
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
- - "" = Device is available and ready for use.
149
+ - "" - Device is available and ready for use.
150
150
  - "UNAVAILABLE" - Device is unavailable, and should probably be rebooted.
151
- - "NOT-LOADED" - Spotify SDK / API is 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
- - kSpDeviceTypeComputer Laptop or desktop computer device.
195
- - kSpDeviceTypeTablet Tablet PC device.
196
- - kSpDeviceTypeSmartphone Smartphone device.
197
- - kSpDeviceTypeSpeaker Speaker device.
198
- - kSpDeviceTypeTV Television device.
199
- - kSpDeviceTypeAVR Audio/Video receiver device.
200
- - kSpDeviceTypeSTB Set-Top Box device.
201
- - kSpDeviceTypeAudioDongle Audio dongle device.
202
- - kSpDeviceTypeGameConsole Game console device.
203
- - kSpDeviceTypeCastVideo Chromecast Video.
204
- - kSpDeviceTypeCastAudio Chromecast Audio.
205
- - kSpDeviceTypeAutomobile Automobile.
206
- - kSpDeviceTypeSmartwatch Smartwatch.
207
- - kSpDeviceTypeChromebook Chromebook.
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 (e.g. "accesstoken", "authorization_code", etc).
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