spotifywebapiPython 1.0.147__py3-none-any.whl → 1.0.149__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.147.dist-info → spotifywebapiPython-1.0.149.dist-info}/METADATA +5 -9
  2. {spotifywebapiPython-1.0.147.dist-info → spotifywebapiPython-1.0.149.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 +0 -41
  22. spotifywebapipython/sautils.py +0 -1
  23. spotifywebapipython/spotifyclient.py +1384 -1835
  24. spotifywebapipython/spotifyconnect/__init__.py +19 -0
  25. spotifywebapipython/{spotifyconnectdeviceeventargs.py → spotifyconnect/spotifyconnectdeviceeventargs.py} +1 -1
  26. spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +1967 -0
  27. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +336 -0
  28. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastcontroller.py +609 -0
  29. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +316 -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.147.dist-info → spotifywebapiPython-1.0.149.dist-info}/LICENSE +0 -0
  37. {spotifywebapiPython-1.0.147.dist-info → spotifywebapiPython-1.0.149.dist-info}/WHEEL +0 -0
  38. {spotifywebapiPython-1.0.147.dist-info → spotifywebapiPython-1.0.149.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,344 @@
1
+ # external package imports.
2
+ from datetime import datetime, timedelta, timezone
3
+ import json
4
+ import os.path
5
+ import platformdirs
6
+ import requests
7
+ import time
8
+ from tokenize import TokenError
9
+
10
+ # our package imports.
11
+ from .const import (
12
+ SPOTIFY_WEBUI_URL_BASE,
13
+ SPOTIFYWEBAPIPYTHON_TOKEN_CACHE_FILE,
14
+ )
15
+
16
+ # get smartinspect logger reference; create a new session for this module name.
17
+ from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext
18
+ import logging
19
+ _logsi:SISession = SIAuto.Si.GetSession(__name__)
20
+ if (_logsi == None):
21
+ _logsi = SIAuto.Si.AddSession(__name__, True)
22
+ _logsi.SystemLogger = logging.getLogger(__name__)
23
+
24
+
25
+ SPOTIFY_WEBUI_URL_GET_ACCESS_TOKEN = SPOTIFY_WEBUI_URL_BASE + "/get_access_token"
26
+ """ Url used to get access token. """
27
+
28
+
29
+ class SpotifyWebPlayerToken:
30
+ """
31
+ Represents a Spotify web player token for an account.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ clientId:str=None,
37
+ profileId:str=None,
38
+ tokenProviderId:str=None,
39
+ tokenStorageDir:str=None,
40
+ tokenStorageFile:str=None,
41
+ ) -> None:
42
+ """
43
+ Initializes a new instance of the class.
44
+
45
+ Args:
46
+ clientId (str):
47
+ The unique identifier of the application.
48
+ A null value will default to `Shared`.
49
+ Default: `Shared`
50
+ profileId (str):
51
+ Profile identifier used when loading / storing the token to disk.
52
+ A null value will default to `Shared`.
53
+ Default: `Shared`
54
+ tokenProviderId (str):
55
+ Provider identifier used when storing the token to disk.
56
+ A null value will default to `Shared`.
57
+ Default: `Shared`
58
+ tokenStorageDir (str):
59
+ The directory path that will contain the Token Cache file.
60
+ A null value will default to the platform specific storage location:
61
+ Example for Windows OS = `C:\\ProgramData\\SpotifyWebApiPython`
62
+ tokenStorageFile (str):
63
+ The filename and extension of the Token Cache file.
64
+ Default is `tokens.json`.
65
+ """
66
+ # validations.
67
+ if clientId is None:
68
+ clientId = "Shared"
69
+ if profileId is None:
70
+ profileId = 'Shared'
71
+
72
+ # verify token storage directory exists.
73
+ if tokenStorageDir is None:
74
+ tokenStorageDir = platformdirs.site_config_dir('SpotifyWebApiPython', ensure_exists=True, appauthor=False)
75
+ os.makedirs(tokenStorageDir, exist_ok=True) # succeeds even if directory exists.
76
+
77
+ # verify token storage filename was specified.
78
+ if tokenStorageFile is None:
79
+ tokenStorageFile = SPOTIFYWEBAPIPYTHON_TOKEN_CACHE_FILE
80
+
81
+ # if token providerId not set, then default to shared.
82
+ if tokenProviderId is None:
83
+ tokenProviderId = 'SpotifyWebPlayerCookieCredentials'
84
+
85
+ # initializa storage.
86
+ self._AccessToken = None
87
+ self._ClientId:str = clientId
88
+ self._ExpiresAt:int = 0
89
+ self._ExpireDateTimeUtc:datetime = None
90
+ self._ExpiresIn:int = 0
91
+ self._IsAnonymous:bool = False
92
+ self._ProfileId:str = profileId
93
+ self._TokenProviderId:str = tokenProviderId
94
+ self._TokenStorageDir:str = tokenStorageDir
95
+ self._TokenStorageFile:str = tokenStorageFile
96
+ self._TokenStoragePath:str = os.path.join(tokenStorageDir, tokenStorageFile)
97
+
98
+ # load cookie credentials from the token storage file.
99
+ token:dict = self._LoadCookieCredentials()
100
+
101
+ # get Spotify Web Player access token from stored Spotify Web Player cookie credentials.
102
+ self.GetAccessTokenFromCookieCredentials()
103
+
104
+
105
+ @property
106
+ def AccessToken(self) -> str:
107
+ """
108
+ An access token that can be provided to a Spotify Web Player application.
109
+ """
110
+ return self._AccessToken
111
+
112
+
113
+ @property
114
+ def ClientId(self) -> str:
115
+ """
116
+ The unique identifier of the application.
117
+ """
118
+ return self._ClientId
119
+
120
+
121
+ @property
122
+ def ExpiresAt(self) -> int:
123
+ """
124
+ DateTime (in epoch seconds) that the authorization token will expire.
125
+ """
126
+ return self._ExpiresAt
127
+
128
+
129
+ @property
130
+ def ExpireDateTimeUtc(self) -> datetime:
131
+ """
132
+ DateTime (in UTC format) that the authorization token will expire.
133
+ """
134
+ return self._ExpireDateTimeUtc
135
+
136
+
137
+ @property
138
+ def ExpiresIn(self) -> int:
139
+ """
140
+ The time period (in seconds) for which the access token is valid.
141
+ """
142
+ return self._ExpiresIn
143
+
144
+
145
+ @property
146
+ def IsExpired(self) -> bool:
147
+ """
148
+ Returns true if the token has expired; otherwise, False if not expired.
149
+ """
150
+ if self._ExpiresAt is not None:
151
+ nowsecs:int = int(time.time())
152
+ # subtract 30 seconds in case we are right at the edge of expiring.
153
+ if (nowsecs + 30) > self._ExpiresAt:
154
+ return True
155
+ return False
156
+
157
+
158
+ @property
159
+ def ProfileId(self) -> str:
160
+ """
161
+ Profile identifier used when loading / storing the token to disk.
162
+ """
163
+ return self._ProfileId
164
+
165
+
166
+ def _LoadCookieCredentials(self) -> dict:
167
+ """
168
+ Loads a token from the token storage file for the ProviderId / ClientId key.
169
+
170
+ Returns:
171
+ A token dictionary, if one was found in the token storage file for the
172
+ specified ProviderId and ClientId key; otherwise, null.
173
+ """
174
+ apiMethodName:str = '_LoadCookieCredentials'
175
+ apiMethodParms:SIMethodParmListContext = None
176
+
177
+ try:
178
+
179
+ # trace.
180
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
181
+ _logsi.LogMethodParmList(SILevel.Verbose, "Loading Spotify Web Player cookie credentials from token storage", apiMethodParms)
182
+
183
+ # formulate token storage key.
184
+ tokenKey:str = f'{self._TokenProviderId}/{self._ClientId}/{self._ProfileId}'
185
+ _logsi.LogVerbose("Cookie credentials storage key: \"%s\"" % (tokenKey))
186
+ _logsi.LogVerbose("Cookie credentials storage file path: \"%s\"" % (self._TokenStoragePath))
187
+
188
+ # does the token storage file exist?
189
+ if os.path.exists(self._TokenStoragePath):
190
+
191
+ # open the token storage file, and load it's contents.
192
+ _logsi.LogVerbose("Opening cookie credentials token storage file")
193
+ with open(self._TokenStoragePath, 'r') as f:
194
+
195
+ tokens = json.load(f)
196
+
197
+ # if token key exists then load the token.
198
+ if tokenKey in tokens:
199
+ _logsi.LogDictionary(SILevel.Verbose, "Cookie credentials were loaded from token storage file for provider: \"%s\"" % (tokenKey), tokens[tokenKey], prettyPrint=True)
200
+
201
+ # parse token for cookie data parameters.
202
+ token:dict = tokens[tokenKey]
203
+ self._sp_dc:str = token.get("sp_dc", None)
204
+ self._sp_key:str = token.get("sp_key", None)
205
+
206
+ # validation.
207
+ if (self._sp_dc is None):
208
+ raise TokenError("Cookie credentials storage key \"%s\" did not contain an \"sp_dc\" key value" % (tokenKey))
209
+ if (self._sp_key is None):
210
+ raise TokenError("Cookie credentials storage key \"%s\" did not contain an \"sp_key\" key value" % (tokenKey))
211
+
212
+ # return token to caller.
213
+ return tokens[tokenKey]
214
+
215
+ # if we make it here, then it denotes that a token was not found
216
+ # for the specified key.
217
+ raise TokenError("Cookie credentials storage key not found: \"%s\"" % (tokenKey))
218
+
219
+ except TokenError as ex:
220
+
221
+ # trace.
222
+ _logsi.LogException(str(ex), ex, logToSystemLogger=False)
223
+ raise
224
+
225
+ except Exception as ex:
226
+
227
+ # trace.
228
+ _logsi.LogException("Could not load Spotify Web Player cookie credentials", ex, logToSystemLogger=False)
229
+ raise TokenError(str(ex))
230
+
231
+ finally:
232
+
233
+ # trace.
234
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
235
+
236
+
237
+ def GetAccessTokenFromCookieCredentials(self) -> None:
238
+ """
239
+ Get Spotify Web Player access token from stored Spotify Web Player cookie
240
+ credentials.
241
+
242
+ This will create a new session to the "https://open.spotify.com" url, passing
243
+ it the stored cookie credentials.
244
+ """
245
+ apiMethodName:str = 'GetAccessTokenFromCookieCredentials'
246
+ apiMethodParms:SIMethodParmListContext = None
247
+ tracePrefix:str = 'SpotifyWebPlayerAccessToken exchange'
248
+
249
+ try:
250
+
251
+ # trace.
252
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
253
+ apiMethodParms.AppendKeyValue("sp_dc", self._sp_dc)
254
+ apiMethodParms.AppendKeyValue("sp_key", self._sp_key)
255
+ _logsi.LogMethodParmList(SILevel.Verbose, "Exchange Spotify Web Player cookie credentials for OAuth2 authorization access token", apiMethodParms)
256
+
257
+ # create a session, using the specified header and generated cookie data.
258
+ session = requests.Session()
259
+ reqHeaders = {'user-agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"}
260
+ reqCookies = {'sp_dc': self._sp_dc, 'sp_key': self._sp_key}
261
+ reqParms={
262
+ "reason": "transport",
263
+ "productType": "web_player",
264
+ }
265
+
266
+ # trace.
267
+ _logsi.LogVerbose("Exchanging Spotify Web Player cookie credentials for OAuth2 authorization access token")
268
+ _logsi.LogDictionary(SILevel.Verbose, "%s http request: \"%s\" (cookies)" % (tracePrefix, SPOTIFY_WEBUI_URL_GET_ACCESS_TOKEN), reqCookies)
269
+ _logsi.LogDictionary(SILevel.Debug, "%s http request: \"%s\" (headers)" % (tracePrefix, SPOTIFY_WEBUI_URL_GET_ACCESS_TOKEN), reqHeaders)
270
+ _logsi.LogDictionary(SILevel.Verbose, "%s http request: \"%s\" (parms)" % (tracePrefix, SPOTIFY_WEBUI_URL_GET_ACCESS_TOKEN), reqParms)
271
+
272
+ # convert the spotify web player cookie credentials to an access token.
273
+ response = session.get(
274
+ SPOTIFY_WEBUI_URL_GET_ACCESS_TOKEN,
275
+ cookies=reqCookies,
276
+ headers=reqHeaders,
277
+ params=reqParms,
278
+ allow_redirects=False,
279
+ )
280
+
281
+ # trace.
282
+ if _logsi.IsOn(SILevel.Debug):
283
+ _logsi.LogObject(SILevel.Debug, "%s http response object - type=\"%s\", module=\"%s\"" % (tracePrefix, type(response).__name__, type(response).__module__), response)
284
+
285
+ # trace.
286
+ if _logsi.IsOn(SILevel.Debug):
287
+ _logsi.LogObject(SILevel.Debug, "%s http response [%s-%s] (response)" % (tracePrefix, response.status_code, response.reason), response)
288
+ if (response.headers):
289
+ _logsi.LogCollection(SILevel.Debug, "%s http response [%s-%s] (headers)" % (tracePrefix, response.status_code, response.reason), response.headers.items())
290
+
291
+ # raise exception if we could not get the access token, or if a redirect occured.
292
+ # the redirect usually indicates the sp_dc and sp_key are expired, and it's redirecting to a login page.
293
+ if (response.status_code == 302 and response.headers["Location"] == "/get_access_token?reason=transport&productType=web_player&_authfailed=1"):
294
+ _logsi.LogError("Unsuccessful token request, received code 302 and Location header %s. sp_dc and sp_key could be expired. Please update values in token storage file.", response.headers["Location"])
295
+ raise TokenError("Expired sp_dc, sp_key; please update values in token storage file")
296
+ if response.status_code != 200:
297
+ _logsi.LogVerbose("Unsuccessful token request, received code %i", response.status_code)
298
+ raise TokenError("Token exchange request failed: %s - %s" % (response.status_code, response.reason))
299
+
300
+ # load request response.
301
+ data = response.content.decode('utf-8')
302
+ responseData = json.loads(data)
303
+
304
+ # trace.
305
+ if _logsi.IsOn(SILevel.Verbose):
306
+ if isinstance(responseData, dict):
307
+ _logsi.LogDictionary(SILevel.Verbose, "%s http response [%s-%s] (json dict)" % (tracePrefix, response.status_code, response.reason), responseData, prettyPrint=True)
308
+ elif isinstance(responseData, list):
309
+ _logsi.LogArray(SILevel.Verbose, "%s http response [%s-%s] (json array)" % (tracePrefix, response.status_code, response.reason), responseData)
310
+ else:
311
+ _logsi.LogObject(SILevel.Verbose, "%s http response [%s-%s] (json object)" % (tracePrefix, response.status_code, response.reason), responseData)
312
+
313
+ # process the access token information.
314
+ self._AccessToken = responseData.get('accessToken', None)
315
+ self._ExpiresAt = int(responseData.get('accessTokenExpirationTimestampMs', 0)) // 1000
316
+ self._ExpiresIn = self._ExpiresAt - int(time.time())
317
+ self._IsAnonymous = bool(responseData.get('isAnonymous', False))
318
+
319
+ # set non-JSON response properties.
320
+ # calculate expire time based on ExpiresAt (epoch) seconds.
321
+ self._ExpireDateTimeUtc = datetime.utcfromtimestamp(self._ExpiresAt)
322
+
323
+ # the following can be used if the `datetime.utcfromtimestamp` is ever removed from python:
324
+ # unix_epoch = datetime(1970, 1, 1)
325
+ # dtUtcNow:datetime = datetime.now(timezone.utc)
326
+ # self._ExpireDateTimeUtc = dtUtcNow + timedelta(seconds=self._ExpiresIn)
327
+
328
+ # trace.
329
+ _logsi.LogObject(SILevel.Verbose, "Spotify Web Player access token object", self, excludeNonPublic=True)
330
+
331
+ except requests.TooManyRedirects as ex:
332
+
333
+ _logsi.LogException("Could not get Spotify Web Player access token; sp_dc and sp_key could be expired; Please update values in token storage file", ex, logToSystemLogger=False)
334
+ raise TokenError("Expired sp_dc, sp_key")
335
+
336
+ except Exception as ex:
337
+
338
+ _logsi.LogException("Could not get Spotify Web Player access token", ex, logToSystemLogger=False)
339
+ raise TokenError("Could not get Spotify Web Player access token")
340
+
341
+ finally:
342
+
343
+ # trace.
344
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
@@ -31,7 +31,7 @@ class ZeroconfGetInfo(ZeroconfResponse):
31
31
  self._Availability:str = ""
32
32
  self._BrandDisplayName:str = None
33
33
  self._ClientId:str = None
34
- self._DeviceId:str = None
34
+ self._DeviceId:str = ""
35
35
  self._DeviceType:str = None
36
36
  self._GroupStatus:str = None
37
37
  self._LibraryVersion:str = None
@@ -47,10 +47,6 @@ class ZeroconfGetInfo(ZeroconfResponse):
47
47
  self._Version:str = None
48
48
  self._VoiceSupport:str = None
49
49
 
50
- # non-Spotify properties.
51
- self._IsActiveDevice:bool = None
52
- self._IsInDeviceList:bool = None
53
-
54
50
  if (root is None):
55
51
 
56
52
  pass
@@ -62,7 +58,7 @@ class ZeroconfGetInfo(ZeroconfResponse):
62
58
  self._Availability = root.get('availability', "")
63
59
  self._BrandDisplayName = root.get('brandDisplayName', None)
64
60
  self._ClientId = root.get('clientID', None)
65
- self._DeviceId = root.get('deviceID', None)
61
+ self._DeviceId = root.get('deviceID', "")
66
62
  self._DeviceType = root.get('deviceType', None)
67
63
  self._GroupStatus = root.get('groupStatus', None)
68
64
  self._LibraryVersion = root.get('libraryVersion', None)
@@ -121,7 +117,7 @@ class ZeroconfGetInfo(ZeroconfResponse):
121
117
  Sets the ActiveUser property value.
122
118
  """
123
119
  if (isinstance(value, str)) or (value is None):
124
- self._ActiveUser = value
120
+ self._ActiveUser = value or ""
125
121
 
126
122
 
127
123
  @property
@@ -203,7 +199,7 @@ class ZeroconfGetInfo(ZeroconfResponse):
203
199
  Sets the DeviceId property value.
204
200
  """
205
201
  if (isinstance(value, str)) or (value is None):
206
- self._DeviceId = value
202
+ self._DeviceId = value or ""
207
203
 
208
204
 
209
205
  @property
@@ -448,44 +444,6 @@ class ZeroconfGetInfo(ZeroconfResponse):
448
444
  return self._VoiceSupport
449
445
 
450
446
 
451
- # non-Spotify properties.
452
-
453
- @property
454
- def IsActiveDevice(self) -> bool:
455
- """
456
- Returns True if this device is the currently active device;
457
- otherwise, False.
458
- """
459
- return self._IsActiveDevice
460
-
461
- @IsActiveDevice.setter
462
- def IsActiveDevice(self, value:bool):
463
- """
464
- Sets the IsActiveDevice property value.
465
- """
466
- if isinstance(value, bool):
467
- self._IsActiveDevice = value
468
-
469
-
470
- @property
471
- def IsInDeviceList(self) -> bool:
472
- """
473
- Returns the status of active device list verification:
474
- * None - device list verification has not been performed for this device.
475
- * True - device is a member of the active device list.
476
- * False - device is NOT a member of the active device list.
477
- """
478
- return self._IsInDeviceList
479
-
480
- @IsInDeviceList.setter
481
- def IsInDeviceList(self, value:bool):
482
- """
483
- Sets the IsInDeviceList property value.
484
- """
485
- if isinstance(value, bool):
486
- self._IsInDeviceList = value
487
-
488
-
489
447
  def Equals(self, obj) -> bool:
490
448
  """
491
449
  Returns true if the specified object instance contains the same argument
@@ -522,8 +480,6 @@ class ZeroconfGetInfo(ZeroconfResponse):
522
480
 
523
481
  # don't compare the following:
524
482
  # self._Aliases:list[ZeroconfGetInfoAlias] = []
525
- # self._IsActiveDevice:bool = None
526
- # self._IsInDeviceList:bool = None
527
483
  # self._SupportedDrmMediaFormats:list[ZeroconfGetInfoDrmMediaFormat] = []
528
484
 
529
485
  # objects attributes are equal.
@@ -566,8 +522,6 @@ class ZeroconfGetInfo(ZeroconfResponse):
566
522
  'HasAliases': self.HasAliases,
567
523
  'IsAvailable': self.IsAvailable,
568
524
  'IsBrandSonos': self.IsBrandSonos,
569
- 'IsActiveDevice': self._IsActiveDevice,
570
- 'IsInDeviceList': self._IsInDeviceList,
571
525
  }
572
526
 
573
527
  # combine base class results with these results.
@@ -614,6 +568,4 @@ class ZeroconfGetInfo(ZeroconfResponse):
614
568
  if self._TokenType is not None: msg = '%s\n TokenType="%s"' % (msg, str(self._TokenType))
615
569
  if self._Version is not None: msg = '%s\n Version="%s"' % (msg, str(self._Version))
616
570
  if self._VoiceSupport is not None: msg = '%s\n VoiceSupport="%s"' % (msg, str(self._VoiceSupport))
617
- if self._IsActiveDevice is not None: msg = '%s\n IsActiveDevice="%s"' % (msg, str(self._IsActiveDevice))
618
- if self._IsInDeviceList is not None: msg = '%s\n IsInDeviceList="%s"' % (msg, str(self._IsInDeviceList))
619
571
  return msg
@@ -53,6 +53,14 @@ class ZeroconfResponse:
53
53
  """
54
54
  return self._ResponseSource
55
55
 
56
+ @ResponseSource.setter
57
+ def ResponseSource(self, value:str):
58
+ """
59
+ Sets the ResponseSource property value.
60
+ """
61
+ if (isinstance(value, str)) or (value is None):
62
+ self._ResponseSource = value or ""
63
+
56
64
 
57
65
  @property
58
66
  def SpotifyError(self) -> int: