spotifywebapiPython 1.0.36__py3-none-any.whl → 1.0.147__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 (91) hide show
  1. {spotifywebapiPython-1.0.36.dist-info → spotifywebapiPython-1.0.147.dist-info}/LICENSE +1 -1
  2. {spotifywebapiPython-1.0.36.dist-info → spotifywebapiPython-1.0.147.dist-info}/METADATA +7 -6
  3. spotifywebapiPython-1.0.147.dist-info/RECORD +117 -0
  4. spotifywebapipython/__init__.py +2 -0
  5. spotifywebapipython/const.py +19 -1
  6. spotifywebapipython/docs/__init__.html +790 -3
  7. spotifywebapipython/docs/index.html +21 -3
  8. spotifywebapipython/docs/search.js +1 -1
  9. spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/const.html +72 -4
  10. spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/saappmessages.html +77 -3
  11. spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/sautils.html +63 -3
  12. spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifyapierror.html +3 -3
  13. spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifyapimessage.html +20 -3
  14. spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifyauthtoken.html +39 -3
  15. spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifyclient.html +2553 -364
  16. spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifydiscovery.html +85 -52
  17. spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifywebapiauthenticationerror.html +3 -3
  18. spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifywebapierror.html +3 -3
  19. spotifywebapipython/models/__init__.py +8 -0
  20. spotifywebapipython/models/albumpagesaved.py +2 -1
  21. spotifywebapipython/models/albumpagesimplified.py +2 -1
  22. spotifywebapipython/models/albumsimplified.py +4 -5
  23. spotifywebapipython/models/artist.py +4 -5
  24. spotifywebapipython/models/artistinfo.py +282 -0
  25. spotifywebapipython/models/artistinfotourevent.py +127 -0
  26. spotifywebapipython/models/artistpage.py +2 -1
  27. spotifywebapipython/models/artistsimplified.py +4 -0
  28. spotifywebapipython/models/audiobookpagesimplified.py +2 -1
  29. spotifywebapipython/models/audiobooksimplified.py +4 -5
  30. spotifywebapipython/models/category.py +16 -0
  31. spotifywebapipython/models/categorypage.py +2 -1
  32. spotifywebapipython/models/chapterpagesimplified.py +2 -1
  33. spotifywebapipython/models/chaptersimplified.py +6 -7
  34. spotifywebapipython/models/context.py +1 -3
  35. spotifywebapipython/models/device.py +85 -2
  36. spotifywebapipython/models/episodepagesaved.py +2 -1
  37. spotifywebapipython/models/episodepagesimplified.py +2 -1
  38. spotifywebapipython/models/episodesaved.py +2 -2
  39. spotifywebapipython/models/episodesimplified.py +5 -6
  40. spotifywebapipython/models/imageobject.py +98 -0
  41. spotifywebapipython/models/pageobject.py +29 -4
  42. spotifywebapipython/models/playeractions.py +33 -11
  43. spotifywebapipython/models/playerplaystate.py +128 -13
  44. spotifywebapipython/models/playerqueueinfo.py +61 -4
  45. spotifywebapipython/models/playhistorypage.py +2 -1
  46. spotifywebapipython/models/playlist.py +6 -3
  47. spotifywebapipython/models/playlistpage.py +2 -1
  48. spotifywebapipython/models/playlistpagesimplified.py +2 -1
  49. spotifywebapipython/models/playlistsimplified.py +49 -7
  50. spotifywebapipython/models/playlisttrack.py +1 -0
  51. spotifywebapipython/models/playlisttracksummary.py +3 -1
  52. spotifywebapipython/models/searchresponse.py +1 -1
  53. spotifywebapipython/models/showpagesaved.py +2 -1
  54. spotifywebapipython/models/showpagesimplified.py +2 -1
  55. spotifywebapipython/models/showsaved.py +2 -2
  56. spotifywebapipython/models/showsimplified.py +8 -5
  57. spotifywebapipython/models/spotifyconnectdevice.py +188 -0
  58. spotifywebapipython/models/spotifyconnectdevices.py +628 -0
  59. spotifywebapipython/models/track.py +2 -1
  60. spotifywebapipython/models/trackpage.py +42 -1
  61. spotifywebapipython/models/trackpagesaved.py +2 -1
  62. spotifywebapipython/models/trackpagesimplified.py +2 -1
  63. spotifywebapipython/models/trackrecommendations.py +5 -5
  64. spotifywebapipython/models/tracksimplified.py +10 -3
  65. spotifywebapipython/models/userprofile.py +25 -1
  66. spotifywebapipython/models/userprofilesimplified.py +4 -5
  67. spotifywebapipython/models/zeroconfdiscoveryresult.py +599 -0
  68. spotifywebapipython/models/zeroconfproperty.py +93 -0
  69. spotifywebapipython/oauthcli/authclient.py +119 -19
  70. spotifywebapipython/saappmessages.py +21 -1
  71. spotifywebapipython/sautils.py +88 -1
  72. spotifywebapipython/spotifyapimessage.py +18 -1
  73. spotifywebapipython/spotifyauthtoken.py +26 -0
  74. spotifywebapipython/spotifyclient.py +4302 -516
  75. spotifywebapipython/spotifyconnectdeviceeventargs.py +67 -0
  76. spotifywebapipython/spotifydiscovery.py +326 -136
  77. spotifywebapipython/zeroconfapi/__init__.py +23 -0
  78. spotifywebapipython/zeroconfapi/authenticationtypes.py +38 -0
  79. spotifywebapipython/zeroconfapi/blobbuilder.py +370 -0
  80. spotifywebapipython/zeroconfapi/credentials.py +37 -0
  81. spotifywebapipython/zeroconfapi/cryptodiffiehellman.py +89 -0
  82. spotifywebapipython/zeroconfapi/helpers.py +64 -0
  83. spotifywebapipython/zeroconfapi/spotifyzeroconfapierror.py +108 -0
  84. spotifywebapipython/zeroconfapi/zeroconfconnect.py +1315 -0
  85. spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +619 -0
  86. spotifywebapipython/zeroconfapi/zeroconfgetinfoalias.py +105 -0
  87. spotifywebapipython/zeroconfapi/zeroconfgetinfodrmmediaformat.py +90 -0
  88. spotifywebapipython/zeroconfapi/zeroconfresponse.py +141 -0
  89. spotifywebapiPython-1.0.36.dist-info/RECORD +0 -98
  90. {spotifywebapiPython-1.0.36.dist-info → spotifywebapiPython-1.0.147.dist-info}/WHEEL +0 -0
  91. {spotifywebapiPython-1.0.36.dist-info → spotifywebapiPython-1.0.147.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,67 @@
1
+ # our package imports.
2
+ from .models import SpotifyConnectDevice
3
+
4
+
5
+ class SpotifyConnectDeviceEventArgs:
6
+ """
7
+ Class used by various methods that inform interested parties that a Spotify
8
+ Connect device update has occured.
9
+
10
+ Threadsafety:
11
+ This class is fully thread-safe.
12
+ """
13
+ def __init__(self, device:SpotifyConnectDevice) -> None:
14
+ """
15
+ Initializes a new instance of the class.
16
+
17
+ Args:
18
+ device (SpotifyConnectDevice):
19
+ Spotify Connect device object that was affected.
20
+ """
21
+
22
+ # initialize instance.
23
+ self._DeviceObject:SpotifyConnectDevice = device
24
+
25
+
26
+ @property
27
+ def DeviceObject(self) -> SpotifyConnectDevice:
28
+ """
29
+ Spotify Connect device object that was affected.
30
+ """
31
+ return self._DeviceObject
32
+
33
+ @DeviceObject.setter
34
+ def DeviceObject(self, value:str) -> None:
35
+ """
36
+ Sets the DeviceObject property value.
37
+ """
38
+ if (isinstance(value, SpotifyConnectDevice)):
39
+ self._DeviceObject = value
40
+
41
+
42
+ def __str__(self) -> str:
43
+ """
44
+ Returns a string representation of the object.
45
+
46
+ Returns:
47
+ A string in the form of "SpotifyConnectDeviceEventArgs: {status}".
48
+ """
49
+ return str.format("SpotifyConnectDeviceEventArgs: {0}", self._DeviceObject or "")
50
+
51
+
52
+ class SpotifyConnectDeviceEventHandler:
53
+ """
54
+ Event handler type definition for an event.
55
+ """
56
+
57
+ def __init__(self, sender:object, e:SpotifyConnectDeviceEventArgs) -> None:
58
+ """
59
+ Initializes a new instance of the class.
60
+
61
+ Args:
62
+ sender (object):
63
+ The object which fired the event.
64
+ e (SpotifyConnectDeviceEventArgs):
65
+ Arguments that contain detailed information related to the event,
66
+ and canceling of its processing.
67
+ """
@@ -3,13 +3,16 @@ try:
3
3
  from queue import Queue, Empty
4
4
  except ImportError:
5
5
  from Queue import Queue, Empty
6
+ import threading
7
+ import time
6
8
  from zeroconf import Zeroconf, ServiceBrowser, ServiceInfo, ServiceStateChange, IPVersion
7
9
 
8
10
  # our package imports.
9
11
  from .saappmessages import SAAppMessages
12
+ from .spotifyapierror import SpotifyApiError
10
13
  from .sautils import export
11
14
  from .models.device import Device
12
- from .spotifyclient import SpotifyClient, SpotifyApiError
15
+ from .models import ZeroconfDiscoveryResult, ZeroconfProperty
13
16
 
14
17
  # get smartinspect logger reference; create a new session for this module name.
15
18
  from smartinspectpython.siauto import SIAuto, SILevel, SISession
@@ -30,42 +33,41 @@ class SpotifyDiscovery:
30
33
  Click the **Sample Code** links in the individual methods for sample code examples.
31
34
  """
32
35
 
33
- def __init__(self, spotifyClient:SpotifyClient=None, areDevicesVerified:bool=False, printToConsole:bool=False) -> None:
36
+ def __init__(
37
+ self,
38
+ zeroconfClient:Zeroconf=None,
39
+ printToConsole:bool=False
40
+ ) -> None:
34
41
  """
35
42
  Initializes a new instance of the class.
36
43
 
37
44
  Args:
38
- spotifyClient (SpotifyClient):
39
- A SpotifyClient instance that will be used to verify discovered devices.
40
- This can be null if the areDevicesVerified argument is False.
41
- This argument is required if the areDevicesVerified argument is True.
42
- areDevicesVerified (bool):
43
- True to create a `SpotifyClient` instance for discovered devices, which
44
- verifies that the device can be accessed and basic information obtained
45
- about its capabilities; otherwise, False to just identify the IPV4 Address,
46
- Port, and Device Name.
47
- Default is False.
45
+ zeroconfClient (Zeroconf)
46
+ A Zeroconf client instance that will be used to discover Spotify Connect devices,
47
+ or null to create a new instance of Zeroconf.
48
+ Default is null.
48
49
  printToConsole (bool):
49
50
  True to print discovered device information to the console as the devices
50
51
  are discovered; otherwise, False to not print anything to the console.
51
52
  Default is False.
52
-
53
- Specify False for the `areDevicesVerified` argument if you want to speed up
54
- device discovery, as it takes extra time to verify device connections as they
55
- are discovered.
56
53
  """
57
54
  # validations.
58
- if spotifyClient is not None and (not isinstance(spotifyClient, SpotifyClient)):
59
- raise SpotifyApiError(SAAppMessages.ARGUMENT_TYPE_ERROR % ("__init__", 'spotifyClient', 'SpotifyClient', type(spotifyClient).__name__), logsi=_logsi)
60
- if spotifyClient is None and areDevicesVerified == True:
61
- raise SpotifyApiError("The spotifyClient argument is required if devices are to be verified (areDevicesVerified argument = True)", logsi=_logsi)
55
+ if zeroconfClient is not None and (not isinstance(zeroconfClient, Zeroconf)):
56
+ raise SpotifyApiError(SAAppMessages.ARGUMENT_TYPE_ERROR % ("__init__", 'zeroconfClient', 'Zeroconf', type(zeroconfClient).__name__), logsi=_logsi)
57
+
58
+ # create the zeroconf client if one was not specified.
59
+ if zeroconfClient is None:
60
+ _logsi.LogVerbose("Creating new Zeroconf instance for discovery")
61
+ zeroconfClient = Zeroconf()
62
+ else:
63
+ _logsi.LogObject(SILevel.Verbose, "Using existing Zeroconf instance for discovery", zeroconfClient)
62
64
 
63
65
  # initialize instance properties.
64
- self._AreDevicesVerified:bool = areDevicesVerified
65
66
  self._DiscoveredDeviceNames:dict = {}
67
+ self._DiscoveryResults:list[ZeroconfDiscoveryResult] = []
66
68
  self._PrintToConsole:bool = printToConsole
67
- self._SpotifyClient:SpotifyClient = spotifyClient
68
- self._VerifiedDevices:dict = {}
69
+ self._ZeroconfClient = zeroconfClient
70
+ self._Zeroconf_Lock = threading.Lock()
69
71
 
70
72
 
71
73
  def __getitem__(self, key):
@@ -89,29 +91,25 @@ class SpotifyDiscovery:
89
91
  return self.ToString()
90
92
 
91
93
 
92
- @property
93
- def AreDevicesVerified(self) -> bool:
94
+ def ContainsId(self, value:str) -> bool:
95
+ """
96
+ Returns True if the `DiscoveryResults` collection contains the specified id value;
97
+ otherwise, False.
94
98
  """
95
- Determines if a `Device` object is created for devices that are
96
- discovered. This property is set by what is passed to the class constructor.
97
-
98
- If False, then the `VerifiedDevices` property will be empty;
99
-
100
- If True, then the `VerifiedDevices` property will contain a `Device`
101
- instance for each device that was detected as part of the discovery process
102
- and was found in the Spotify User's player devices list.
103
- """
104
- return self._AreDevicesVerified
105
-
99
+ item:ZeroconfDiscoveryResult = self.GetResultById(value)
100
+ if item is not None:
101
+ return True
102
+ return False
103
+
106
104
 
107
105
  @property
108
106
  def DiscoveredDeviceNames(self) -> dict:
109
107
  """
110
108
  A dictionary of discovered device names that were detected by the discovery process.
111
109
 
112
- Dictionary keys will be in the form of "address:port", where "address" is the device
113
- ipv4 address and the "port" is the ipv4 port number the Spotify Connect device
114
- is listening on.
110
+ Dictionary keys will be in the form of "'name' (address:port)", where "name" is the
111
+ device name, "address" is the device server address and the "port" is the ipv4 port
112
+ number the Spotify Connect device is listening on.
115
113
 
116
114
  Dictionary values will be the device names (e.g. "Web Player (Chrome)", etc.). This SHOULD
117
115
  match the name of the device as displayed in the Spotify App, but is not guaranteed.
@@ -120,28 +118,159 @@ class SpotifyDiscovery:
120
118
 
121
119
 
122
120
  @property
123
- def VerifiedDevices(self) -> dict:
121
+ def DiscoveryResults(self) -> list[ZeroconfDiscoveryResult]:
122
+ """
123
+ An array of `ZeroconfDiscoveryResult` items that contain discovery details for
124
+ each service that was discovered.
125
+ """
126
+ return self._DiscoveryResults
127
+
128
+
129
+ def GetIndexByKey(self, value:str) -> int:
130
+ """
131
+ Returns the index of the `DiscoveryResults` collection that contains
132
+ the specified device key value; otherwise, -1.
124
133
  """
125
- A dictionary of discovered `Device` instances that were detected on the network.
134
+ result:int = -1
135
+ if value is None:
136
+ return result
126
137
 
127
- This property is only populated if the `AreDevicesVerified` property is True.
138
+ # convert case for comparison.
139
+ value = value.lower()
140
+
141
+ # process all discovered devices.
142
+ for i in range(len(self._DiscoveryResults)):
143
+ if (self._DiscoveryResults[i].Key.lower() == value):
144
+ result = i
145
+ break
146
+ return result
147
+
148
+
149
+ def GetResultById(self, value:str) -> bool:
150
+ """
151
+ Returns a `ZeroconfDiscoveryResult` instance if the `DiscoveryResults` collection contains
152
+ the specified device id value; otherwise, None.
153
+ """
154
+ result:ZeroconfDiscoveryResult = None
155
+ if value is None:
156
+ return result
128
157
 
129
- Dictionary keys will be in the form of "address:port", where "address" is the device
130
- ipv4 address and the "port" is the ipv4 port number the Spotify Connect device
131
- is listening on.
158
+ # convert case for comparison.
159
+ value = value.lower()
132
160
 
133
- Dictionary values will be `Device` instances that represent the discovered
134
- device.
161
+ # process all discovered devices.
162
+ item:ZeroconfDiscoveryResult
163
+ for item in self._DiscoveryResults:
164
+ if (item.Id.lower() == value):
165
+ result = item
166
+ break
167
+ return result
168
+
169
+
170
+ @property
171
+ def ZeroconfClient(self) -> Zeroconf:
172
+ """
173
+ Zeroconf client instance that will be used to discover Spotify Connect devices.
135
174
  """
136
- return self._VerifiedDevices
175
+ return self._ZeroconfClient
176
+
177
+
178
+ def _GetZeroconfDiscoveryResult(
179
+ self,
180
+ zeroconf:Zeroconf,
181
+ serviceType:str,
182
+ serviceName:str,
183
+ serviceStateChange:ServiceStateChange,
184
+ serviceStateChangeDesc:str
185
+ ) -> ZeroconfDiscoveryResult:
186
+ """
187
+ Builds a `ZeroconfDiscoveryResult` instance from Zeroconf Service Information data.
188
+ """
189
+ result:ZeroconfDiscoveryResult = None
137
190
 
191
+ try:
138
192
 
139
- def _OnServiceStateChange(self,
140
- zeroconf:Zeroconf,
141
- service_type:str,
142
- name:str,
143
- state_change:ServiceStateChange
144
- ) -> None:
193
+ # trace.
194
+ _logsi.EnterMethod(SILevel.Debug)
195
+
196
+ # get device service information instance; if none, then we are done.
197
+ deviceName:str = (serviceName.split(".")[0])
198
+ serviceInfo:ServiceInfo = zeroconf.get_service_info(serviceType, serviceName)
199
+ if serviceInfo is None:
200
+ _logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification ignored: \"%s\" (%s:%s) - no serviceinfo data" % (serviceName, serviceType, serviceStateChange))
201
+ return result
202
+
203
+ # get list of displayable (parsed) ipv4 addresses; if none, then we are done.
204
+ ipAddressList = serviceInfo.parsed_addresses(IPVersion.V4Only)
205
+ if (ipAddressList is None):
206
+ _logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification ignored: \"%s\" (%s:%s) - no IP address list" % (serviceName, serviceType, serviceStateChange))
207
+ return result
208
+
209
+ # trace.
210
+ _logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf service details: \"%s\" %s (serviceinfo object)" % (deviceName, ipAddressList), serviceInfo)
211
+
212
+ # create new discovery result instance.
213
+ result:ZeroconfDiscoveryResult = ZeroconfDiscoveryResult()
214
+ result.DeviceName = deviceName
215
+ result.Domain = '.local'
216
+ for item in ipAddressList:
217
+ result.HostIpAddresses.append(item)
218
+ result.HostIpPort = serviceInfo.port
219
+ result.HostTTL = serviceInfo.host_ttl
220
+ result.Key = serviceInfo.key
221
+ result.Name = serviceInfo.name
222
+ result.OtherTTL = serviceInfo.other_ttl
223
+ result.Priority = serviceInfo.priority
224
+ result.Server = serviceInfo.server
225
+ result.ServerKey = serviceInfo.server_key
226
+ result.ServiceInfo = serviceInfo
227
+ result.ServiceType = serviceType
228
+ result.Weight = serviceInfo.weight
229
+ result.Id = "\"%s\" (%s:%s)" % (result.DeviceName, result.HostIpAddress, result.HostIpPort)
230
+
231
+ # process service info propertys.
232
+ # note that the property keys and values must first be decoded to utf-8 encoding.
233
+ if serviceInfo.properties is not None:
234
+ dspProperties:dict = {}
235
+ for key, value in serviceInfo.properties.items():
236
+ keyStr = key.decode('utf-8')
237
+ valueStr = value.decode('utf-8')
238
+ result.Properties.append(ZeroconfProperty(keyStr, valueStr))
239
+ dspProperties[keyStr] = valueStr
240
+ # check for Spotify Connect common properties.
241
+ if keyStr.lower() == 'cpath':
242
+ result.SpotifyConnectCPath = valueStr
243
+ elif keyStr.lower() == 'version':
244
+ result.SpotifyConnectVersion = valueStr
245
+ _logsi.LogDictionary(SILevel.Verbose, "Spotify Connect Zeroconf service details: \"%s\" %s (serviceinfo properties)" % (deviceName, ipAddressList), dspProperties)
246
+
247
+ # trace.
248
+ _logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf Discovery Result: %s (object)" % (result.Id), result, excludeNonPublic=True)
249
+
250
+ # return discovery result.
251
+ return result
252
+
253
+ except Exception as ex:
254
+
255
+ # trace.
256
+ _logsi.LogException("Could not load ZeroconfDiscoveryResult instance from Zeroconf serviceinfo object.", ex, logToSystemLogger=False)
257
+
258
+ # ignore exception
259
+ return None
260
+
261
+ finally:
262
+
263
+ # trace.
264
+ _logsi.LeaveMethod(SILevel.Debug)
265
+
266
+
267
+ def _OnServiceStateChange(
268
+ self,
269
+ zeroconf:Zeroconf,
270
+ service_type:str,
271
+ name:str,
272
+ state_change:ServiceStateChange
273
+ ) -> None:
145
274
  """
146
275
  Called by the zeroconf ServiceBrowser when a service state has changed (e.g. added,
147
276
  removed, or updated). In our case, we only care about devices that were added.
@@ -149,84 +278,134 @@ class SpotifyDiscovery:
149
278
  IMPORTANT - This method is executed on a different thread, so be careful about
150
279
  multi-threaded operations!
151
280
  """
152
- serviceType:str = service_type
153
- serviceName:str = name
154
- serviceStateChange:ServiceStateChange = state_change
281
+
282
+ # this method can be tested by issuing the following commands from an MS-DOS
283
+ # command prompt to simulate Zeroconf registration updates.
284
+
285
+ # - add a new Spotify Connect instance registration:
286
+ # > dns-sd -R "MyTestSC1" _spotify-connect._tcp. local 8080 cpath=/zcXXX version=1.0
155
287
 
156
- # process by the state change value.
157
- if (serviceStateChange is ServiceStateChange.Added):
158
- _logsi.LogVerbose("Discovered Spotify Connect device service: '%s' (%s:%s)" % (serviceName, serviceType, serviceStateChange))
288
+ # - update a new Spotify Connect instance registration;
289
+ # - if you change the name, service type, or port number, a NEW registration is created!
290
+ # - for updates, the only thing you can really update is the TXT record properties.
291
+ # > dns-sd -R "MyTestSC1" _spotify-connect._tcp. local 8080 cpath=/zcYYY version=1.0
292
+
293
+ # - remove an existing Spotify Connect instance registration;
294
+ # > the only way to remove an entry is to kill the process the entry was created under!
295
+
296
+ # enter the following section one thread at a time (threadsafe).
297
+ with self._Zeroconf_Lock:
298
+
299
+ # copy passed parameters to thread-safe storage.
300
+ serviceType:str = service_type
301
+ serviceName:str = name
302
+ serviceStateChange:ServiceStateChange = state_change
303
+ serviceStateChangeDesc:str = None
304
+
305
+ try:
306
+
307
+ # trace.
308
+ _logsi.EnterMethod(SILevel.Debug)
309
+
310
+ # process by the state change value.
311
+ if (serviceStateChange is ServiceStateChange.Added) \
312
+ or (serviceStateChange is ServiceStateChange.Updated):
313
+
314
+ # set service state change description.
315
+ serviceStateChangeDesc = "Added"
316
+ if (serviceStateChange is ServiceStateChange.Updated):
317
+ serviceStateChangeDesc = "Updated"
318
+
319
+ # trace.
320
+ _logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification: \"%s\" (%s)" % (serviceName, serviceStateChangeDesc))
321
+
322
+ # build discovery result instance from service state information.
323
+ # if nothing returned, then don't bother!
324
+ result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(zeroconf, serviceType, serviceName, serviceStateChange, serviceStateChangeDesc)
325
+ if (result is None):
326
+ return
327
+
328
+ # add device name to list of registered names.
329
+ self._DiscoveredDeviceNames[result.Id] = result.DeviceName
330
+
331
+ # add / update discovered results by serviceinfo key value.
332
+ # we use the serviceinfo KEY value (not SERVERKEY) since some manufacturers use random ip port
333
+ # numbers, and the port number could have been updated. in this case, the key SHOULD remain the same.
334
+ # note that a different serviceinfo key SHOULD be created for speaker groups; at least they are for BOSE devices.
335
+ idx:int = self.GetIndexByKey(result.Key)
336
+ if (idx == -1):
337
+ self._DiscoveryResults.append(result) # add new result
338
+ _logsi.LogVerbose("Spotify Connect Zeroconf service discovery result was added: %s (%s)" % (result.Id, serviceStateChangeDesc))
339
+ else:
340
+ self._DiscoveryResults[idx] = result # update existing result
341
+ _logsi.LogVerbose("Spotify Connect Zeroconf service discovery result was updated: %s (%s)" % (result.Id, serviceStateChangeDesc))
342
+
343
+ # trace.
344
+ if (self._PrintToConsole == True):
345
+ print("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
346
+
347
+ elif (serviceStateChange is ServiceStateChange.Removed):
348
+
349
+ # set service state change description.
350
+ serviceStateChangeDesc = "Removed"
351
+
352
+ # trace.
353
+ _logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification: \"%s\" (%s)" % (serviceName, serviceStateChangeDesc))
354
+
355
+ # build discovery result instance from service state information.
356
+ # if nothing returned, then don't bother!
357
+ result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(zeroconf, serviceType, serviceName, serviceStateChange, serviceStateChangeDesc)
358
+ if (result is None):
359
+ return
360
+
361
+ # remove device name from list of registered names.
362
+ self._DiscoveredDeviceNames.pop(result.Id, None)
363
+
364
+ # remove device from results collection;
365
+ # the reversed() function creates an iterator that traverses the list in reverse order.
366
+ # This ensures that removing an element doesn't affect the indices of the subsequent
367
+ # elements we're going to iterate over.
368
+ resultCompare:str = result.Id.lower()
369
+ for i in reversed(range(len(self._DiscoveryResults))):
370
+ if (self._DiscoveryResults[i].Id.lower() == resultCompare):
371
+
372
+ # remove discovery result.
373
+ result = self._DiscoveryResults.pop(i)
374
+
375
+ # trace.
376
+ _logsi.LogVerbose("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
377
+ if (self._PrintToConsole == True):
378
+ print("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
379
+
380
+ break
381
+
382
+ except Exception as ex:
159
383
 
160
- deviceName:str = (serviceName.split(".")[0])
161
- serviceInfo:ServiceInfo = zeroconf.get_service_info(service_type, serviceName)
162
- if serviceInfo is None:
163
- return
384
+ # trace.
385
+ _logsi.LogException("Unhandled exception occured while processing Spotify Connect Zeroconf discovery service notification (%s)" % (serviceStateChange), ex, logToSystemLogger=False)
164
386
 
165
- # get list of displayable (parsed) ipv4 addresses that were detected; if none, then we are done.
166
- ipAddressList = serviceInfo.parsed_addresses(IPVersion.V4Only)
167
- if (ipAddressList is None):
168
- return
387
+ # ignore exception
388
+ return None
169
389
 
170
- # create device instances for each ipv4 address found.
171
- for deviceIpAddress in ipAddressList:
172
-
173
- devicePort:int = serviceInfo.port
174
- deviceKey:str = '%s:%i' % (deviceIpAddress, devicePort)
175
- _logsi.LogVerbose("Discovered Spotify Connect device: %s - %s" % (deviceKey, deviceName))
176
- if (self._PrintToConsole == True):
177
- print("Discovered Spotify Connect device: %s - %s" % (deviceKey, deviceName))
178
- _logsi.LogObject(SILevel.Verbose, "Discovered Spotify Connect device ServiceInfo: %s - %s" % (deviceKey, deviceName), serviceInfo)
179
-
180
- # # trace service info propertys if desired.
181
- # # note that the property keys and values must first be decoded to utf-8 encoding.
182
- # if _logsi.IsOn(SILevel.Verbose):
183
- # if serviceInfo.properties is not None:
184
- # dspProperties:dict = {}
185
- # for key, value in serviceInfo.properties.items():
186
- # dspProperties[key.decode('utf-8')] = value.decode('utf-8')
187
- # _logsi.LogDictionary(SILevel.Verbose, "Discovered Spotify Connect device ServiceInfo.Properties: '%s' (%s:%i)" % (deviceName, deviceIpAddress, devicePort), dspProperties)
188
-
189
- # add the device name to the list (if not already added) using its
190
- # ipv4 address and port number as the key.
191
- if deviceKey not in self._DiscoveredDeviceNames.keys():
192
- self._DiscoveredDeviceNames[deviceKey] = deviceName
193
-
194
- # are we verifying devices connections? if so, then create a SpotifyClient
195
- # object, which will verify the connection and gather basic capabilities of the device.
196
- # we will also add the SpotifyClient instance using the same key as the device name list.
197
- if self._AreDevicesVerified == True:
198
-
199
- if deviceKey not in self._VerifiedDevices.keys():
200
-
201
- #device:SpotifyClient = SpotifyClient(host=deviceIpAddress, port=devicePort)
202
-
203
- # get Spotify Connect player device by it's Name value.
204
- device:Device = self._SpotifyClient.GetPlayerDevice(deviceName)
205
- if device is not None:
206
- self._VerifiedDevices[deviceKey] = device
207
-
208
- elif (serviceStateChange is ServiceStateChange.Removed):
209
- _logsi.LogVerbose("Discovered Spotify Connect device removal (ignored): '%s' (%s:%s)", serviceName, serviceType, serviceStateChange)
210
- pass
211
-
212
- elif (serviceStateChange is ServiceStateChange.Updated):
213
- _logsi.LogVerbose("Discovered Spotify Connect device update (ignored): '%s' (%s:%s)", serviceName, serviceType, serviceStateChange)
214
- pass
215
-
216
-
217
- def DiscoverDevices(self, timeout:int=5) -> dict:
390
+ finally:
391
+
392
+ # trace.
393
+ _logsi.LeaveMethod(SILevel.Debug)
394
+
395
+
396
+ def DiscoverDevices(self, timeout:float=2) -> dict:
218
397
  """
219
398
  Discover Spotify Connect devices on the local network via the
220
399
  ZeroConf (aka MDNS) service.
221
400
 
222
401
  Args:
223
- timeout (int):
402
+ timeout (float):
224
403
  Maximum amount of time to wait (in seconds) for the
225
404
  discovery to complete.
226
- Default is 5 seconds.
405
+ Default is 2 seconds.
227
406
 
228
407
  Returns:
229
- A dictionary of discovered `SpotifyClient` objects.
408
+ A dictionary of `ZeroconfDiscoveryResult` objects.
230
409
 
231
410
  <details>
232
411
  <summary>Sample Code</summary>
@@ -235,26 +414,43 @@ class SpotifyDiscovery:
235
414
  ```
236
415
  </details>
237
416
  """
417
+ browser:ServiceBrowser = None
418
+
238
419
  # using Queue as a timer (timeout functionality).
239
420
  discoveryQueueTimer = Queue()
240
-
241
- # create the zeroconf service and our listener callback.
242
- zeroconf:Zeroconf = Zeroconf()
243
-
244
- # create the zeroconf service browser that will start device discovery.
245
- _logsi.LogVerbose("Discovery of Spotify Connect devices via Zeroconf is starting ...")
246
- ServiceBrowser(zeroconf, "_spotify-connect._tcp.local.", handlers=[self._OnServiceStateChange])
247
-
421
+
248
422
  try:
249
- # give the ServiceBrowser time to discover
423
+
424
+ # create the zeroconf service browser that will start device discovery.
425
+ _logsi.LogVerbose("Discovery of Spotify Connect devices via Zeroconf is starting")
426
+ browser = ServiceBrowser(self._ZeroconfClient, "_spotify-connect._tcp.local.", handlers=[self._OnServiceStateChange])
427
+
428
+ # trace.
429
+ _logsi.LogObject(SILevel.Verbose, "ZeroconfClient object after ServiceBrowser creation", self._ZeroconfClient)
430
+
431
+ # give the ServiceBrowser time to discover.
250
432
  discoveryQueueTimer.get(timeout=timeout)
251
433
 
252
434
  except Empty:
253
435
 
254
436
  # this is not really an exception, but more of an indicator that
255
437
  # the timeout has been reached.
256
- _logsi.LogVerbose("Discovery of Spotify Connect devices via Zeroconf has ended.")
438
+ _logsi.LogVerbose("Discovery of Spotify Connect devices via Zeroconf has reached its timeout of %f seconds" % timeout)
439
+
440
+ finally:
441
+
442
+ if (browser is not None):
443
+
444
+ # remove the zeroconf service browser with associated listener and free resources.
445
+ _logsi.LogVerbose("Releasing Zeroconf ServiceBrowser instance")
446
+ browser.cancel()
447
+ del browser
448
+ _logsi.LogObject(SILevel.Verbose, "ZeroconfClient object after ServiceBrowser resources released", self._ZeroconfClient)
449
+
450
+ # trace.
451
+ _logsi.LogVerbose("Discovery of Spotify Connect devices via Zeroconf has ended")
257
452
 
453
+ # return result to caller.
258
454
  return self._DiscoveredDeviceNames
259
455
 
260
456
 
@@ -273,12 +469,6 @@ class SpotifyDiscovery:
273
469
  if includeItems == True:
274
470
 
275
471
  for key, deviceName in self._DiscoveredDeviceNames.items():
276
-
277
- device:Device
278
- verifiedStatus:str = ''
279
- device = self._VerifiedDevices.get(key, None)
280
- if device is not None:
281
- verifiedStatus = ' (verified - ID=%s)' % device.Id
282
- msg = "%s\n- %s - %s %s" % (msg, key, deviceName, verifiedStatus)
472
+ msg = "%s\n- %s" % (msg, key)
283
473
 
284
474
  return msg
@@ -0,0 +1,23 @@
1
+ # import all classes from the namespace.
2
+ from .authenticationtypes import AuthenticationTypes
3
+ from .blobbuilder import BlobBuilder
4
+ from .credentials import Credentials
5
+ from .spotifyzeroconfapierror import SpotifyZeroconfApiError
6
+ from .zeroconfconnect import ZeroconfConnect
7
+ from .zeroconfresponse import ZeroconfResponse
8
+ from .zeroconfgetinfo import ZeroconfGetInfo
9
+ from .zeroconfgetinfoalias import ZeroconfGetInfoAlias
10
+ from .zeroconfgetinfodrmmediaformat import ZeroconfGetInfoDrmMediaFormat
11
+
12
+ # all classes to import when "import *" is specified.
13
+ __all__ = [
14
+ 'AuthenticationTypes',
15
+ 'BlobBuilder',
16
+ 'Credentials',
17
+ 'SpotifyZeroconfApiError',
18
+ 'ZeroconfConnect',
19
+ 'ZeroconfGetInfo',
20
+ 'ZeroconfGetInfoAlias',
21
+ 'ZeroconfGetInfoDrmMediaFormat',
22
+ 'ZeroconfResponse',
23
+ ]