spotifywebapiPython 1.0.132__py3-none-any.whl → 1.0.134__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 (23) hide show
  1. {spotifywebapiPython-1.0.132.dist-info → spotifywebapiPython-1.0.134.dist-info}/METADATA +1 -1
  2. {spotifywebapiPython-1.0.132.dist-info → spotifywebapiPython-1.0.134.dist-info}/RECORD +23 -23
  3. spotifywebapipython/const.py +1 -1
  4. spotifywebapipython/docs/__init__.html +14 -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 +2 -2
  14. spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +40 -2
  15. spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +2 -2
  16. spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +2 -2
  17. spotifywebapipython/models/zeroconfdiscoveryresult.py +21 -0
  18. spotifywebapipython/spotifyclient.py +36 -13
  19. spotifywebapipython/spotifydiscovery.py +207 -45
  20. spotifywebapipython/zeroconfapi/zeroconfconnect.py +3 -3
  21. {spotifywebapiPython-1.0.132.dist-info → spotifywebapiPython-1.0.134.dist-info}/LICENSE +0 -0
  22. {spotifywebapiPython-1.0.132.dist-info → spotifywebapiPython-1.0.134.dist-info}/WHEEL +0 -0
  23. {spotifywebapiPython-1.0.132.dist-info → spotifywebapiPython-1.0.134.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,7 @@ try:
3
3
  from queue import Queue, Empty
4
4
  except ImportError:
5
5
  from Queue import Queue, Empty
6
+ import threading
6
7
  import time
7
8
  from zeroconf import Zeroconf, ServiceBrowser, ServiceInfo, ServiceStateChange, IPVersion
8
9
 
@@ -66,6 +67,7 @@ class SpotifyDiscovery:
66
67
  self._DiscoveryResults:list[ZeroconfDiscoveryResult] = []
67
68
  self._PrintToConsole:bool = printToConsole
68
69
  self._ZeroconfClient = zeroconfClient
70
+ self._Zeroconf_Lock = threading.Lock()
69
71
 
70
72
 
71
73
  def __getitem__(self, key):
@@ -89,6 +91,17 @@ class SpotifyDiscovery:
89
91
  return self.ToString()
90
92
 
91
93
 
94
+ def ContainsId(self, value:str) -> bool:
95
+ """
96
+ Returns True if the `DiscoveryResults` collection contains the specified id value;
97
+ otherwise, False.
98
+ """
99
+ item:ZeroconfDiscoveryResult = self.GetResultById(value)
100
+ if item is not None:
101
+ return True
102
+ return False
103
+
104
+
92
105
  @property
93
106
  def DiscoveredDeviceNames(self) -> dict:
94
107
  """
@@ -113,6 +126,27 @@ class SpotifyDiscovery:
113
126
  return self._DiscoveryResults
114
127
 
115
128
 
129
+ def GetResultById(self, value:str) -> bool:
130
+ """
131
+ Returns a `ZeroconfDiscoveryResult` instance if the `DiscoveryResults` collection contains
132
+ the specified device id value; otherwise, None.
133
+ """
134
+ result:ZeroconfDiscoveryResult = None
135
+ if value is None:
136
+ return result
137
+
138
+ # convert case for comparison.
139
+ value = value.lower()
140
+
141
+ # process all discovered devices.
142
+ item:ZeroconfDiscoveryResult
143
+ for item in self._DiscoveryResults:
144
+ if (item.Id.lower() == value):
145
+ result = item
146
+ break
147
+ return result
148
+
149
+
116
150
  @property
117
151
  def ZeroconfClient(self) -> Zeroconf:
118
152
  """
@@ -121,38 +155,41 @@ class SpotifyDiscovery:
121
155
  return self._ZeroconfClient
122
156
 
123
157
 
124
- def _OnServiceStateChange(self,
125
- zeroconf:Zeroconf,
126
- service_type:str,
127
- name:str,
128
- state_change:ServiceStateChange
129
- ) -> None:
158
+ def _GetZeroconfDiscoveryResult(
159
+ self,
160
+ zeroconf:Zeroconf,
161
+ serviceType:str,
162
+ serviceName:str,
163
+ serviceStateChange:ServiceStateChange,
164
+ serviceStateChangeDesc:str
165
+ ) -> ZeroconfDiscoveryResult:
130
166
  """
131
- Called by the zeroconf ServiceBrowser when a service state has changed (e.g. added,
132
- removed, or updated). In our case, we only care about devices that were added.
133
-
134
- IMPORTANT - This method is executed on a different thread, so be careful about
135
- multi-threaded operations!
167
+ Builds a `ZeroconfDiscoveryResult` instance from Zeroconf Service Information data.
136
168
  """
137
- serviceType:str = service_type
138
- serviceName:str = name
139
- serviceStateChange:ServiceStateChange = state_change
140
-
141
- # process by the state change value.
142
- if (serviceStateChange is ServiceStateChange.Added):
143
- _logsi.LogVerbose("Discovered Spotify Connect device service: '%s' (%s:%s)" % (serviceName, serviceType, serviceStateChange))
169
+ result:ZeroconfDiscoveryResult = None
170
+
171
+ try:
172
+
173
+ # trace.
174
+ _logsi.EnterMethod(SILevel.Debug)
144
175
 
176
+ # get device service information instance; if none, then we are done.
145
177
  deviceName:str = (serviceName.split(".")[0])
146
- serviceInfo:ServiceInfo = zeroconf.get_service_info(service_type, serviceName)
178
+ serviceInfo:ServiceInfo = zeroconf.get_service_info(serviceType, serviceName)
147
179
  if serviceInfo is None:
148
- return
149
-
150
- # get list of displayable (parsed) ipv4 addresses that were detected; if none, then we are done.
180
+ _logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification ignored: \"%s\" (%s:%s) - no serviceinfo data" % (serviceName, serviceType, serviceStateChange))
181
+ return result
182
+
183
+ # get list of displayable (parsed) ipv4 addresses; if none, then we are done.
151
184
  ipAddressList = serviceInfo.parsed_addresses(IPVersion.V4Only)
152
185
  if (ipAddressList is None):
153
- return
186
+ _logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification ignored: \"%s\" (%s:%s) - no IP address list" % (serviceName, serviceType, serviceStateChange))
187
+ return result
154
188
 
155
- # create new result instance.
189
+ # trace.
190
+ _logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf service details: \"%s\" %s (serviceinfo object)" % (deviceName, ipAddressList), serviceInfo)
191
+
192
+ # create new discovery result instance.
156
193
  result:ZeroconfDiscoveryResult = ZeroconfDiscoveryResult()
157
194
  result.DeviceName = deviceName
158
195
  result.Domain = '.local'
@@ -169,14 +206,8 @@ class SpotifyDiscovery:
169
206
  result.ServiceInfo = serviceInfo
170
207
  result.ServiceType = serviceType
171
208
  result.Weight = serviceInfo.weight
172
-
173
- # formulate a unique key for this device.
174
- deviceKey:str = "'%s' (%s:%i)" % (deviceName, result.HostIpAddress, result.HostIpPort)
175
- _logsi.LogVerbose("Discovered Spotify Connect device: %s %s" % (deviceKey, result.HostIpAddresses))
176
- if (self._PrintToConsole == True):
177
- print("Discovered Spotify Connect device: %s [%s]" % (deviceKey, result.HostIpAddresses))
178
- _logsi.LogObject(SILevel.Verbose, "Discovered Spotify Connect device ServiceInfo: %s" % (deviceKey), serviceInfo)
179
-
209
+ result.Id = "\"%s\" (%s:%s)" % (result.DeviceName, result.HostIpAddress, result.HostIpPort)
210
+
180
211
  # process service info propertys.
181
212
  # note that the property keys and values must first be decoded to utf-8 encoding.
182
213
  if serviceInfo.properties is not None:
@@ -191,25 +222,156 @@ class SpotifyDiscovery:
191
222
  result.SpotifyConnectCPath = valueStr
192
223
  elif keyStr.lower() == 'version':
193
224
  result.SpotifyConnectVersion = valueStr
194
- _logsi.LogDictionary(SILevel.Verbose, "Discovered Spotify Connect device ServiceInfo.Properties: %s" % (deviceKey), dspProperties)
225
+ _logsi.LogDictionary(SILevel.Verbose, "Spotify Connect Zeroconf service details: \"%s\" %s (serviceinfo properties)" % (deviceName, ipAddressList), dspProperties)
195
226
 
196
- # add the device name to the list (if not already added).
197
- if deviceKey not in self._DiscoveredDeviceNames.keys():
198
- self._DiscoveredDeviceNames[deviceKey] = deviceName
227
+ # trace.
228
+ _logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf Discovery Result: %s (object)" % (result.Id), result, excludeNonPublic=True)
199
229
 
200
- # add the device result to the list.
201
- self._DiscoveryResults.append(result)
230
+ # return discovery result.
231
+ return result
202
232
 
233
+ except Exception as ex:
234
+
203
235
  # trace.
204
- _logsi.LogObject(SILevel.Verbose, "Discovered Spotify Connect device result: %s" % (deviceKey), result, excludeNonPublic=True)
236
+ _logsi.LogException("Could not load ZeroconfDiscoveryResult instance from Zeroconf serviceinfo object.", ex, logToSystemLogger=False)
237
+
238
+ # ignore exception
239
+ return None
240
+
241
+ finally:
242
+
243
+ # trace.
244
+ _logsi.LeaveMethod(SILevel.Debug)
205
245
 
206
- elif (serviceStateChange is ServiceStateChange.Removed):
207
- _logsi.LogVerbose("Discovered Spotify Connect device removal (ignored): '%s' (%s:%s)", serviceName, serviceType, serviceStateChange)
208
- pass
209
246
 
210
- elif (serviceStateChange is ServiceStateChange.Updated):
211
- _logsi.LogVerbose("Discovered Spotify Connect device update (ignored): '%s' (%s:%s)", serviceName, serviceType, serviceStateChange)
212
- pass
247
+ def _OnServiceStateChange(
248
+ self,
249
+ zeroconf:Zeroconf,
250
+ service_type:str,
251
+ name:str,
252
+ state_change:ServiceStateChange
253
+ ) -> None:
254
+ """
255
+ Called by the zeroconf ServiceBrowser when a service state has changed (e.g. added,
256
+ removed, or updated). In our case, we only care about devices that were added.
257
+
258
+ IMPORTANT - This method is executed on a different thread, so be careful about
259
+ multi-threaded operations!
260
+ """
261
+
262
+ # this method can be tested by issuing the following commands from an MS-DOS
263
+ # command prompt to simulate Zeroconf registration updates.
264
+
265
+ # - add a new Spotify Connect instance registration:
266
+ # > dns-sd -R "MyTestSC1" _spotify-connect._tcp. local 8080 cpath=/zcXXX version=1.0
267
+
268
+ # - update a new Spotify Connect instance registration;
269
+ # - if you change the name, service type, or port number, a NEW registration is created!
270
+ # - for updates, the only thing you can really update is the TXT record properties.
271
+ # > dns-sd -R "MyTestSC1" _spotify-connect._tcp. local 8080 cpath=/zcYYY version=1.0
272
+
273
+ # - remove an existing Spotify Connect instance registration;
274
+ # > the only way to remove an entry is to kill the process the entry was created under!
275
+
276
+ # enter the following section one thread at a time (threadsafe).
277
+ with self._Zeroconf_Lock:
278
+
279
+ # copy passed parameters to thread-safe storage.
280
+ serviceType:str = service_type
281
+ serviceName:str = name
282
+ serviceStateChange:ServiceStateChange = state_change
283
+ serviceStateChangeDesc:str = None
284
+
285
+ try:
286
+
287
+ # trace.
288
+ _logsi.EnterMethod(SILevel.Debug)
289
+
290
+ # process by the state change value.
291
+ if (serviceStateChange is ServiceStateChange.Added) \
292
+ or (serviceStateChange is ServiceStateChange.Updated):
293
+
294
+ # set service state change description.
295
+ serviceStateChangeDesc = "Added"
296
+ if (serviceStateChange is ServiceStateChange.Updated):
297
+ serviceStateChangeDesc = "Updated"
298
+
299
+ # trace.
300
+ _logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification: \"%s\" (%s)" % (serviceName, serviceStateChangeDesc))
301
+
302
+ # build discovery result instance from service state information.
303
+ # if nothing returned, then don't bother!
304
+ result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(zeroconf, serviceType, serviceName, serviceStateChange, serviceStateChangeDesc)
305
+ if (result is None):
306
+ return
307
+
308
+ # add device name to list of registered names.
309
+ self._DiscoveredDeviceNames[result.Id] = result.DeviceName
310
+
311
+ # add / update discovered results by a unique result ID value, which is comprised
312
+ # of the name, ip address, and ip port of the service. we use the unique ID value
313
+ # since some manufacturers use random ip port numbers, which will ensure that we
314
+ # remove (or update) the unique service entry. in other words, we can't just rely
315
+ # on the name since a manufacturer could re-register the same name using a different
316
+ # ip port value.
317
+ item:ZeroconfDiscoveryResult = self.GetResultById(result.Id)
318
+ if (item is None):
319
+ self._DiscoveryResults.append(result)
320
+ else:
321
+ item = result
322
+
323
+ # trace.
324
+ _logsi.LogVerbose("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
325
+ if (self._PrintToConsole == True):
326
+ print("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
327
+
328
+ elif (serviceStateChange is ServiceStateChange.Removed):
329
+
330
+ # set service state change description.
331
+ serviceStateChangeDesc = "Removed"
332
+
333
+ # trace.
334
+ _logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification: \"%s\" (%s)" % (serviceName, serviceStateChangeDesc))
335
+
336
+ # build discovery result instance from service state information.
337
+ # if nothing returned, then don't bother!
338
+ result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(zeroconf, serviceType, serviceName, serviceStateChange, serviceStateChangeDesc)
339
+ if (result is None):
340
+ return
341
+
342
+ # remove device name from list of registered names.
343
+ self._DiscoveredDeviceNames.pop(result.Id, None)
344
+
345
+ # remove device from results collection;
346
+ # the reversed() function creates an iterator that traverses the list in reverse order.
347
+ # This ensures that removing an element doesn't affect the indices of the subsequent
348
+ # elements we're going to iterate over.
349
+ resultCompare:str = result.Id.lower()
350
+ for i in reversed(range(len(self._DiscoveryResults))):
351
+ if (self._DiscoveryResults[i].Id.lower() == resultCompare):
352
+
353
+ # remove discovery result.
354
+ result = self._DiscoveryResults.pop(i)
355
+
356
+ # trace.
357
+ _logsi.LogVerbose("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
358
+ if (self._PrintToConsole == True):
359
+ print("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
360
+
361
+ break
362
+
363
+ except Exception as ex:
364
+
365
+ # trace.
366
+ _logsi.LogException("Unhandled exception occured while processing Spotify Connect Zeroconf discovery service notification (%s)" % (serviceStateChange), ex, logToSystemLogger=False)
367
+
368
+ # ignore exception
369
+ return None
370
+
371
+ finally:
372
+
373
+ # trace.
374
+ _logsi.LeaveMethod(SILevel.Debug)
213
375
 
214
376
 
215
377
  def DiscoverDevices(self, timeout:float=2) -> dict:
@@ -405,7 +405,7 @@ class ZeroconfConnect:
405
405
  apiMethodParms.AppendKeyValue("username", username)
406
406
  apiMethodParms.AppendKeyValue("password (with mask)", passwordMaskString(password))
407
407
  apiMethodParms.AppendKeyValue("delay", delay)
408
- _logsi.LogMethodParmList(SILevel.Verbose, "Connecting device to Spotify Connect (ip=%s)" % self._HostIpAddress, apiMethodParms)
408
+ _logsi.LogMethodParmList(SILevel.Verbose, "Connecting device to Spotify Connect (ip=%s:%s)" % (self._HostIpAddress, self._HostIpPort), apiMethodParms)
409
409
 
410
410
  # validations.
411
411
  delay = validateDelay(delay, 0.50, 10)
@@ -536,7 +536,7 @@ class ZeroconfConnect:
536
536
  apiMethodParms.AppendKeyValue("info.TokenType", info.TokenType)
537
537
  apiMethodParms.AppendKeyValue("info.BrandDisplayName", info.BrandDisplayName)
538
538
  apiMethodParms.AppendKeyValue("info.ModelDisplayName", info.ModelDisplayName)
539
- _logsi.LogMethodParmList(SILevel.Verbose, "Issuing Spotify Connect Zeroconf addUser request (ip=%s)" % self._HostIpAddress, apiMethodParms)
539
+ _logsi.LogMethodParmList(SILevel.Verbose, "Issuing Spotify Connect Zeroconf addUser request (ip=%s:%s)" % (self._HostIpAddress, self._HostIpPort), apiMethodParms)
540
540
 
541
541
  # set request endpoint.
542
542
  endpoint:str = self.GetEndpoint('addUser')
@@ -1022,7 +1022,7 @@ class ZeroconfConnect:
1022
1022
  apiMethodParms.AppendKeyValue("Uri", self._Uri)
1023
1023
  apiMethodParms.AppendKeyValue("delay", delay)
1024
1024
  apiMethodParms.AppendKeyValue("ignoreStatusResult", ignoreStatusResult)
1025
- _logsi.LogMethodParmList(SILevel.Verbose, "Disconnecting device from Spotify Connect (ip=%s)" % self._HostIpAddress, apiMethodParms)
1025
+ _logsi.LogMethodParmList(SILevel.Verbose, "Disconnecting device from Spotify Connect (ip=%s:%s)" % (self._HostIpAddress, self._HostIpPort), apiMethodParms)
1026
1026
 
1027
1027
  # validations.
1028
1028
  delay = validateDelay(delay, 0.50, 10)