spotifywebapiPython 1.0.242__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.
Files changed (138) hide show
  1. spotifywebapipython/__init__.py +35 -0
  2. spotifywebapipython/const.py +114 -0
  3. spotifywebapipython/docs/__init__.html +2156 -0
  4. spotifywebapipython/docs/codecopy.js +70 -0
  5. spotifywebapipython/docs/index.html +514 -0
  6. spotifywebapipython/docs/search.js +46 -0
  7. spotifywebapipython/docs/spotifywebapipython/const.html +636 -0
  8. spotifywebapipython/docs/spotifywebapipython/saappmessages.html +540 -0
  9. spotifywebapipython/docs/spotifywebapipython/sautils.html +620 -0
  10. spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +341 -0
  11. spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +598 -0
  12. spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +612 -0
  13. spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +16557 -0
  14. spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +518 -0
  15. spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +421 -0
  16. spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +400 -0
  17. spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +635 -0
  18. spotifywebapipython/docs/spotifywebapipython.ico +0 -0
  19. spotifywebapipython/docs/spotifywebapipython_OLD.ico +0 -0
  20. spotifywebapipython/models/__init__.py +121 -0
  21. spotifywebapipython/models/album.py +170 -0
  22. spotifywebapipython/models/albumpagesaved.py +85 -0
  23. spotifywebapipython/models/albumpagesimplified.py +88 -0
  24. spotifywebapipython/models/albumsaved.py +102 -0
  25. spotifywebapipython/models/albumsimplified.py +308 -0
  26. spotifywebapipython/models/artist.py +146 -0
  27. spotifywebapipython/models/artistinfo.py +282 -0
  28. spotifywebapipython/models/artistinfotourevent.py +127 -0
  29. spotifywebapipython/models/artistpage.py +70 -0
  30. spotifywebapipython/models/artistsimplified.py +163 -0
  31. spotifywebapipython/models/audiobook.py +87 -0
  32. spotifywebapipython/models/audiobookpagesimplified.py +88 -0
  33. spotifywebapipython/models/audiobooksimplified.py +362 -0
  34. spotifywebapipython/models/audiofeatures.py +333 -0
  35. spotifywebapipython/models/author.py +67 -0
  36. spotifywebapipython/models/category.py +168 -0
  37. spotifywebapipython/models/categorypage.py +70 -0
  38. spotifywebapipython/models/chapter.py +84 -0
  39. spotifywebapipython/models/chapterpagesimplified.py +88 -0
  40. spotifywebapipython/models/chaptersimplified.py +387 -0
  41. spotifywebapipython/models/context.py +113 -0
  42. spotifywebapipython/models/copyright.py +79 -0
  43. spotifywebapipython/models/device.py +301 -0
  44. spotifywebapipython/models/episode.py +84 -0
  45. spotifywebapipython/models/episodepagesaved.py +85 -0
  46. spotifywebapipython/models/episodepagesimplified.py +88 -0
  47. spotifywebapipython/models/episodesaved.py +99 -0
  48. spotifywebapipython/models/episodesimplified.py +370 -0
  49. spotifywebapipython/models/explicitcontent.py +80 -0
  50. spotifywebapipython/models/externalids.py +91 -0
  51. spotifywebapipython/models/externalurls.py +104 -0
  52. spotifywebapipython/models/followers.py +81 -0
  53. spotifywebapipython/models/imageobject.py +193 -0
  54. spotifywebapipython/models/imagevibrantcolors.py +182 -0
  55. spotifywebapipython/models/linkedfrom.py +165 -0
  56. spotifywebapipython/models/narrator.py +67 -0
  57. spotifywebapipython/models/owner.py +162 -0
  58. spotifywebapipython/models/pageobject.py +343 -0
  59. spotifywebapipython/models/playeractions.py +196 -0
  60. spotifywebapipython/models/playerlastplayedinfo.py +334 -0
  61. spotifywebapipython/models/playerplaystate.py +467 -0
  62. spotifywebapipython/models/playerqueueinfo.py +207 -0
  63. spotifywebapipython/models/playhistory.py +136 -0
  64. spotifywebapipython/models/playhistorypage.py +85 -0
  65. spotifywebapipython/models/playlist.py +125 -0
  66. spotifywebapipython/models/playlistpage.py +85 -0
  67. spotifywebapipython/models/playlistpagesimplified.py +107 -0
  68. spotifywebapipython/models/playlistsimplified.py +334 -0
  69. spotifywebapipython/models/playlisttrack.py +143 -0
  70. spotifywebapipython/models/playlisttracksummary.py +80 -0
  71. spotifywebapipython/models/recommendationseed.py +135 -0
  72. spotifywebapipython/models/restrictions.py +86 -0
  73. spotifywebapipython/models/resumepoint.py +79 -0
  74. spotifywebapipython/models/searchresponse.py +391 -0
  75. spotifywebapipython/models/searchresultbase.py +299 -0
  76. spotifywebapipython/models/show.py +87 -0
  77. spotifywebapipython/models/showpagesaved.py +85 -0
  78. spotifywebapipython/models/showpagesimplified.py +88 -0
  79. spotifywebapipython/models/showsaved.py +99 -0
  80. spotifywebapipython/models/showsimplified.py +320 -0
  81. spotifywebapipython/models/spotifyconnectdevice.py +321 -0
  82. spotifywebapipython/models/spotifyconnectdevices.py +441 -0
  83. spotifywebapipython/models/track.py +142 -0
  84. spotifywebapipython/models/trackpage.py +110 -0
  85. spotifywebapipython/models/trackpagesaved.py +85 -0
  86. spotifywebapipython/models/trackpagesimplified.py +88 -0
  87. spotifywebapipython/models/trackrecommendations.py +88 -0
  88. spotifywebapipython/models/tracksaved.py +99 -0
  89. spotifywebapipython/models/tracksimplified.py +398 -0
  90. spotifywebapipython/models/userprofile.py +184 -0
  91. spotifywebapipython/models/userprofilesimplified.py +206 -0
  92. spotifywebapipython/models/zeroconfdiscoveryresult.py +592 -0
  93. spotifywebapipython/models/zeroconfproperty.py +93 -0
  94. spotifywebapipython/oauthcli/__init__.py +7 -0
  95. spotifywebapipython/oauthcli/authclient.py +1229 -0
  96. spotifywebapipython/saappmessages.py +86 -0
  97. spotifywebapipython/sautils.py +285 -0
  98. spotifywebapipython/spotifyapierror.py +66 -0
  99. spotifywebapipython/spotifyapimessage.py +269 -0
  100. spotifywebapipython/spotifyauthtoken.py +264 -0
  101. spotifywebapipython/spotifyclient.py +17885 -0
  102. spotifywebapipython/spotifyconnect/__init__.py +19 -0
  103. spotifywebapipython/spotifyconnect/spotifyconnectdeviceeventargs.py +67 -0
  104. spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +2564 -0
  105. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +467 -0
  106. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastcontroller.py +701 -0
  107. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +359 -0
  108. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastmultizonelistener.py +212 -0
  109. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfexceptions.py +20 -0
  110. spotifywebapipython/spotifyconnect/spotifyconnectzeroconflistener.py +335 -0
  111. spotifywebapipython/spotifydiscovery.py +473 -0
  112. spotifywebapipython/spotifymediatypes.py +53 -0
  113. spotifywebapipython/spotifywebapiauthenticationerror.py +122 -0
  114. spotifywebapipython/spotifywebapierror.py +106 -0
  115. spotifywebapipython/spotifywebplayertoken.py +743 -0
  116. spotifywebapipython/vibrant/__init__.py +17 -0
  117. spotifywebapipython/vibrant/generator.py +261 -0
  118. spotifywebapipython/vibrant/image.py +100 -0
  119. spotifywebapipython/vibrant/main.py +34 -0
  120. spotifywebapipython/vibrant/models.py +93 -0
  121. spotifywebapipython/vibrant/utils.py +56 -0
  122. spotifywebapipython/zeroconfapi/__init__.py +23 -0
  123. spotifywebapipython/zeroconfapi/authenticationtypes.py +38 -0
  124. spotifywebapipython/zeroconfapi/blobbuilder.py +370 -0
  125. spotifywebapipython/zeroconfapi/credentials.py +37 -0
  126. spotifywebapipython/zeroconfapi/cryptodiffiehellman.py +89 -0
  127. spotifywebapipython/zeroconfapi/helpers.py +64 -0
  128. spotifywebapipython/zeroconfapi/spotifyzeroconfapierror.py +108 -0
  129. spotifywebapipython/zeroconfapi/zeroconfconnect.py +1327 -0
  130. spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +572 -0
  131. spotifywebapipython/zeroconfapi/zeroconfgetinfoalias.py +109 -0
  132. spotifywebapipython/zeroconfapi/zeroconfgetinfodrmmediaformat.py +90 -0
  133. spotifywebapipython/zeroconfapi/zeroconfresponse.py +195 -0
  134. spotifywebapipython-1.0.242.dist-info/LICENSE +22 -0
  135. spotifywebapipython-1.0.242.dist-info/METADATA +219 -0
  136. spotifywebapipython-1.0.242.dist-info/RECORD +138 -0
  137. spotifywebapipython-1.0.242.dist-info/WHEEL +5 -0
  138. spotifywebapipython-1.0.242.dist-info/top_level.txt +1 -0
@@ -0,0 +1,473 @@
1
+ # external package imports.
2
+ try:
3
+ from queue import Queue, Empty
4
+ except ImportError:
5
+ from Queue import Queue, Empty
6
+ import threading
7
+ import time
8
+ from zeroconf import Zeroconf, ServiceBrowser, ServiceInfo, ServiceStateChange, IPVersion
9
+
10
+ # our package imports.
11
+ from .saappmessages import SAAppMessages
12
+ from .spotifyapierror import SpotifyApiError
13
+ from .sautils import export
14
+ from .models.device import Device
15
+ from .models import ZeroconfDiscoveryResult, ZeroconfProperty
16
+
17
+ # get smartinspect logger reference; create a new session for this module name.
18
+ from smartinspectpython.siauto import SIAuto, SILevel, SISession
19
+ import logging
20
+ _logsi:SISession = SIAuto.Si.GetSession(__name__)
21
+ if (_logsi == None):
22
+ _logsi = SIAuto.Si.AddSession(__name__, True)
23
+ _logsi.SystemLogger = logging.getLogger(__name__)
24
+
25
+
26
+ @export
27
+ class SpotifyDiscovery:
28
+ """
29
+ This class contains methods used to dicover Spotify Connect devices on a local
30
+ network. The ZeroConf (aka MDNS, etc) service is used to detect devices,
31
+ and adds them to a device list as they are discovered.
32
+
33
+ Click the **Sample Code** links in the individual methods for sample code examples.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ zeroconfClient:Zeroconf=None,
39
+ printToConsole:bool=False
40
+ ) -> None:
41
+ """
42
+ Initializes a new instance of the class.
43
+
44
+ Args:
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.
49
+ printToConsole (bool):
50
+ True to print discovered device information to the console as the devices
51
+ are discovered; otherwise, False to not print anything to the console.
52
+ Default is False.
53
+ """
54
+ # validations.
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)
64
+
65
+ # initialize instance properties.
66
+ self._DiscoveredDeviceNames:dict = {}
67
+ self._DiscoveryResults:list[ZeroconfDiscoveryResult] = []
68
+ self._PrintToConsole:bool = printToConsole
69
+ self._ZeroconfClient = zeroconfClient
70
+ self._Zeroconf_RLock = threading.RLock()
71
+
72
+
73
+ def __getitem__(self, key):
74
+ if repr(key) in self._DiscoveredDeviceNames:
75
+ return self._DiscoveredDeviceNames[repr(key)]
76
+
77
+
78
+ def __iter__(self):
79
+ return iter(self._DiscoveredDeviceNames)
80
+
81
+
82
+ def __len__(self) -> int:
83
+ return len(self._DiscoveredDeviceNames)
84
+
85
+
86
+ def __repr__(self) -> str:
87
+ return self.ToString()
88
+
89
+
90
+ def __str__(self) -> str:
91
+ return self.ToString()
92
+
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
+
105
+ @property
106
+ def DiscoveredDeviceNames(self) -> dict:
107
+ """
108
+ A dictionary of discovered device names that were detected by the discovery process.
109
+
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.
113
+
114
+ Dictionary values will be the device names (e.g. "Web Player (Chrome)", etc.). This SHOULD
115
+ match the name of the device as displayed in the Spotify App, but is not guaranteed.
116
+ """
117
+ return self._DiscoveredDeviceNames
118
+
119
+
120
+ @property
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.
133
+ """
134
+ result:int = -1
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
+ 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
157
+
158
+ # convert case for comparison.
159
+ value = value.lower()
160
+
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.
174
+ """
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
190
+
191
+ try:
192
+
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.ServiceType = serviceType
227
+ result.Weight = serviceInfo.weight
228
+ result.Id = "\"%s\" (%s:%s)" % (result.DeviceName, result.HostIpAddress, result.HostIpPort)
229
+
230
+ # process service info propertys.
231
+ # note that the property keys and values must first be decoded to utf-8 encoding.
232
+ if serviceInfo.properties is not None:
233
+ dspProperties:dict = {}
234
+ for key, value in serviceInfo.properties.items():
235
+ keyStr = key.decode('utf-8')
236
+ valueStr = value.decode('utf-8')
237
+ result.Properties.append(ZeroconfProperty(keyStr, valueStr))
238
+ dspProperties[keyStr] = valueStr
239
+ # check for Spotify Connect common properties.
240
+ if keyStr.lower() == 'cpath':
241
+ result.SpotifyConnectCPath = valueStr
242
+ elif keyStr.lower() == 'version':
243
+ result.SpotifyConnectVersion = valueStr
244
+ _logsi.LogDictionary(SILevel.Verbose, "Spotify Connect Zeroconf service details: \"%s\" %s (serviceinfo properties)" % (deviceName, ipAddressList), dspProperties)
245
+
246
+ # trace.
247
+ _logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf Discovery Result: %s (object)" % (result.Id), result, excludeNonPublic=True)
248
+
249
+ # return discovery result.
250
+ return result
251
+
252
+ except Exception as ex:
253
+
254
+ # trace.
255
+ _logsi.LogException("Could not load ZeroconfDiscoveryResult instance from Zeroconf serviceinfo object.", ex, logToSystemLogger=False)
256
+
257
+ # ignore exception
258
+ return None
259
+
260
+ finally:
261
+
262
+ # trace.
263
+ _logsi.LeaveMethod(SILevel.Debug)
264
+
265
+
266
+ def _OnServiceStateChange(
267
+ self,
268
+ zeroconf:Zeroconf,
269
+ service_type:str,
270
+ name:str,
271
+ state_change:ServiceStateChange
272
+ ) -> None:
273
+ """
274
+ Called by the zeroconf ServiceBrowser when a service state has changed (e.g. added,
275
+ removed, or updated). In our case, we only care about devices that were added.
276
+
277
+ IMPORTANT - This method is executed on a different thread, so be careful about
278
+ multi-threaded operations!
279
+ """
280
+
281
+ # this method can be tested by issuing the following commands from an MS-DOS
282
+ # command prompt to simulate Zeroconf registration updates.
283
+
284
+ # - add a new Spotify Connect instance registration:
285
+ # > dns-sd -R "MyTestSC1" _spotify-connect._tcp. local 8080 cpath=/zcXXX version=1.0
286
+
287
+ # - update a new Spotify Connect instance registration;
288
+ # - if you change the name, service type, or port number, a NEW registration is created!
289
+ # - for updates, the only thing you can really update is the TXT record properties.
290
+ # > dns-sd -R "MyTestSC1" _spotify-connect._tcp. local 8080 cpath=/zcYYY version=1.0
291
+
292
+ # - remove an existing Spotify Connect instance registration;
293
+ # > the only way to remove an entry is to kill the process the entry was created under!
294
+
295
+ # enter the following section one thread at a time (threadsafe).
296
+ with self._Zeroconf_RLock:
297
+
298
+ # copy passed parameters to thread-safe storage.
299
+ serviceType:str = service_type
300
+ serviceName:str = name
301
+ serviceStateChange:ServiceStateChange = state_change
302
+ serviceStateChangeDesc:str = None
303
+
304
+ try:
305
+
306
+ # trace.
307
+ _logsi.EnterMethod(SILevel.Debug)
308
+
309
+ # process by the state change value.
310
+ if (serviceStateChange is ServiceStateChange.Added) \
311
+ or (serviceStateChange is ServiceStateChange.Updated):
312
+
313
+ # set service state change description.
314
+ serviceStateChangeDesc = "Added"
315
+ if (serviceStateChange is ServiceStateChange.Updated):
316
+ serviceStateChangeDesc = "Updated"
317
+
318
+ # trace.
319
+ _logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification: \"%s\" (%s)" % (serviceName, serviceStateChangeDesc))
320
+
321
+ # build discovery result instance from service state information.
322
+ # if nothing returned, then don't bother!
323
+ result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(zeroconf, serviceType, serviceName, serviceStateChange, serviceStateChangeDesc)
324
+ if (result is None):
325
+ return
326
+
327
+ # add device name to list of registered names.
328
+ self._DiscoveredDeviceNames[result.Id] = result.DeviceName
329
+
330
+ # add / update discovered results by serviceinfo key value.
331
+ # we use the serviceinfo KEY value (not SERVERKEY) since some manufacturers use random ip port
332
+ # numbers, and the port number could have been updated. in this case, the key SHOULD remain the same.
333
+ # note that a different serviceinfo key SHOULD be created for speaker groups; at least they are for BOSE devices.
334
+ idx:int = self.GetIndexByKey(result.Key)
335
+ if (idx == -1):
336
+ self._DiscoveryResults.append(result) # add new result
337
+ _logsi.LogVerbose("Spotify Connect Zeroconf service discovery result was added: %s (%s)" % (result.Id, serviceStateChangeDesc))
338
+ else:
339
+ self._DiscoveryResults[idx] = result # update existing result
340
+ _logsi.LogVerbose("Spotify Connect Zeroconf service discovery result was updated: %s (%s)" % (result.Id, serviceStateChangeDesc))
341
+
342
+ # trace.
343
+ if (self._PrintToConsole == True):
344
+ print("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
345
+
346
+ elif (serviceStateChange is ServiceStateChange.Removed):
347
+
348
+ # set service state change description.
349
+ serviceStateChangeDesc = "Removed"
350
+
351
+ # trace.
352
+ _logsi.LogVerbose("Spotify Connect Zeroconf discovery service notification: \"%s\" (%s)" % (serviceName, serviceStateChangeDesc))
353
+
354
+ # build discovery result instance from service state information.
355
+ # if nothing returned, then don't bother!
356
+ result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(zeroconf, serviceType, serviceName, serviceStateChange, serviceStateChangeDesc)
357
+ if (result is None):
358
+ return
359
+
360
+ # remove device name from list of registered names.
361
+ self._DiscoveredDeviceNames.pop(result.Id, None)
362
+
363
+ # remove device from results collection;
364
+ # the reversed() function creates an iterator that traverses the list in reverse order.
365
+ # This ensures that removing an element doesn't affect the indices of the subsequent
366
+ # elements we're going to iterate over.
367
+ resultCompare:str = result.Id.lower()
368
+ for i in reversed(range(len(self._DiscoveryResults))):
369
+ if (self._DiscoveryResults[i].Id.lower() == resultCompare):
370
+
371
+ # remove discovery result.
372
+ result = self._DiscoveryResults.pop(i)
373
+
374
+ # trace.
375
+ _logsi.LogVerbose("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
376
+ if (self._PrintToConsole == True):
377
+ print("Spotify Connect Zeroconf service %s: %s" % (serviceStateChangeDesc, result.Id))
378
+
379
+ break
380
+
381
+ except Exception as ex:
382
+
383
+ # trace.
384
+ _logsi.LogException("Unhandled exception occured while processing Spotify Connect Zeroconf discovery service notification (%s)" % (serviceStateChange), ex, logToSystemLogger=False)
385
+
386
+ # ignore exception
387
+ return None
388
+
389
+ finally:
390
+
391
+ # trace.
392
+ _logsi.LeaveMethod(SILevel.Debug)
393
+
394
+
395
+ def DiscoverDevices(self, timeout:float=2) -> dict:
396
+ """
397
+ Discover Spotify Connect devices on the local network via the
398
+ ZeroConf (aka MDNS) service.
399
+
400
+ Args:
401
+ timeout (float):
402
+ Maximum amount of time to wait (in seconds) for the
403
+ discovery to complete.
404
+ Default is 2 seconds.
405
+
406
+ Returns:
407
+ A dictionary of `ZeroconfDiscoveryResult` objects.
408
+
409
+ <details>
410
+ <summary>Sample Code</summary>
411
+ ```python
412
+ .. include:: ../docs/include/samplecode/SpotifyDiscovery/DiscoverDevices.py
413
+ ```
414
+ </details>
415
+ """
416
+ browser:ServiceBrowser = None
417
+
418
+ # using Queue as a timer (timeout functionality).
419
+ discoveryQueueTimer = Queue()
420
+
421
+ try:
422
+
423
+ # create the zeroconf service browser that will start device discovery.
424
+ _logsi.LogVerbose("Discovery of Spotify Connect devices via Zeroconf is starting")
425
+ browser = ServiceBrowser(self._ZeroconfClient, "_spotify-connect._tcp.local.", handlers=[self._OnServiceStateChange])
426
+
427
+ # trace.
428
+ _logsi.LogObject(SILevel.Verbose, "ZeroconfClient object after ServiceBrowser creation", self._ZeroconfClient)
429
+
430
+ # give the ServiceBrowser time to discover.
431
+ discoveryQueueTimer.get(timeout=timeout)
432
+
433
+ except Empty:
434
+
435
+ # this is not really an exception, but more of an indicator that
436
+ # the timeout has been reached.
437
+ _logsi.LogVerbose("Discovery of Spotify Connect devices via Zeroconf has reached its timeout of %f seconds" % timeout)
438
+
439
+ finally:
440
+
441
+ if (browser is not None):
442
+
443
+ # remove the zeroconf service browser with associated listener and free resources.
444
+ _logsi.LogVerbose("Releasing Zeroconf ServiceBrowser instance")
445
+ browser.cancel()
446
+ del browser
447
+ _logsi.LogObject(SILevel.Verbose, "ZeroconfClient object after ServiceBrowser resources released", self._ZeroconfClient)
448
+
449
+ # trace.
450
+ _logsi.LogVerbose("Discovery of Spotify Connect devices via Zeroconf has ended")
451
+
452
+ # return result to caller.
453
+ return self._DiscoveredDeviceNames
454
+
455
+
456
+ def ToString(self, includeItems:bool=False) -> str:
457
+ """
458
+ Returns a displayable string representation of the class.
459
+
460
+ Args:
461
+ includeItems (bool):
462
+ True to include all items in the list; otherwise False to only
463
+ include the base list.
464
+ """
465
+ msg:str = 'SpotifyDiscovery:'
466
+ msg = "%s (%d items)" % (msg, len(self._DiscoveredDeviceNames))
467
+
468
+ if includeItems == True:
469
+
470
+ for key, deviceName in self._DiscoveredDeviceNames.items():
471
+ msg = "%s\n- %s" % (msg, key)
472
+
473
+ return msg
@@ -0,0 +1,53 @@
1
+ from enum import StrEnum
2
+
3
+ class SpotifyMediaTypes(StrEnum):
4
+ """
5
+ Spotify media types enumeration.
6
+ """
7
+
8
+ ALBUM = "album"
9
+ """
10
+ Album media type.
11
+ """
12
+
13
+ ARTIST = "artist"
14
+ """
15
+ Artist media type.
16
+ """
17
+
18
+ AUDIOBOOK = "audiobook"
19
+ """
20
+ Audiobook media type.
21
+ """
22
+
23
+ EPISODE = "episode"
24
+ """
25
+ Episode media type.
26
+ """
27
+
28
+ PLAYLIST = "playlist"
29
+ """
30
+ Playlist media type.
31
+ """
32
+
33
+ SHOW = "show"
34
+ """
35
+ Show media type.
36
+ """
37
+
38
+ TRACK = "track"
39
+ """
40
+ Track media type.
41
+ """
42
+
43
+
44
+ def __eq__(self, other):
45
+ """
46
+ Override __eq__ so that we can compare enum members directly to their raw values
47
+ without explicitly accessing the ".value".
48
+ """
49
+ if isinstance(other, self.__class__):
50
+ return self is other
51
+ elif isinstance(other, type(self.value)):
52
+ return self.value == other
53
+ return False
@@ -0,0 +1,122 @@
1
+ # external package imports.
2
+ from smartinspectpython.sisession import SISession
3
+
4
+ # our package imports.
5
+ from .sautils import export
6
+ from .saappmessages import SAAppMessages
7
+
8
+
9
+ @export
10
+ class SpotifyWebApiAuthenticationError(Exception):
11
+ """
12
+ Exception thrown when a Spotify Web API Authentication Error occurs.
13
+
14
+ Whenever the application makes requests related to authentication or authorization to Web API,
15
+ such as retrieving an access token or refreshing an access token, the error response follows
16
+ RFC 6749 on the OAuth 2.0 Authorization Framework.
17
+ """
18
+
19
+ def __init__(self, error:str, errorDescription:str, methodName:str, httpStatus:int, httpReason:str, logsi:SISession=None) -> None:
20
+ """
21
+ Initializes a new instance of the class.
22
+
23
+ Args:
24
+ error (str):
25
+ A high level description of the error as specified in RFC 6749 Section 5.2.
26
+ errorDescription (str):
27
+ A more detailed description of the error as specified in RFC 6749 Section 4.1.2.1.
28
+ methodName (str):
29
+ Name of the client method that executed the request.
30
+ httpStatus (str):
31
+ HTTP status code for the error.
32
+ httpReason (str):
33
+ HTTP reason code for the error.
34
+ logsi (SISession):
35
+ Trace session object that this exception will be logged to, or null to bypass trace logging.
36
+ Default is None.
37
+ """
38
+
39
+ # initialize base class.
40
+ super().__init__(errorDescription)
41
+
42
+ # initialize class instance.
43
+ self._Error:str = error
44
+ self._ErrorDescription:str = errorDescription
45
+ self._MethodName:str = methodName
46
+ self._HttpReason:int = httpReason
47
+ self._HttpStatus:str = httpStatus
48
+
49
+ # trace.
50
+ if logsi is not None:
51
+ if (isinstance(logsi, SISession)):
52
+ logsi.LogException(str(self), self, logToSystemLogger=False)
53
+
54
+
55
+ def __repr__(self) -> str:
56
+ return self.ToString()
57
+
58
+
59
+ def __str__(self) -> str:
60
+ return self.ToString()
61
+
62
+
63
+ @property
64
+ def Error(self) -> str:
65
+ """
66
+ A high level description of the error as specified in RFC 6749 Section 5.2.
67
+
68
+ Example: `invalid_client`.
69
+ """
70
+ return self._Error
71
+
72
+
73
+ @property
74
+ def ErrorDescription(self) -> str:
75
+ """
76
+ Message text, as reported by the element text of the error xml response.
77
+
78
+ Example: `Invalid client secret`.
79
+ """
80
+ return self._ErrorDescription
81
+
82
+
83
+ @property
84
+ def HttpReason(self) -> str:
85
+ """
86
+ HTTP reason code for the error.
87
+
88
+ Example: `Bad Request`.
89
+ """
90
+ return self._HttpReason
91
+
92
+
93
+ @property
94
+ def HttpStatus(self) -> int:
95
+ """
96
+ HTTP status code for the error.
97
+
98
+ Example: `400`
99
+ """
100
+ return self._HttpStatus
101
+
102
+
103
+ @property
104
+ def MethodName(self) -> str:
105
+ """
106
+ The method being executed when the error occured.
107
+
108
+ Example: `GetArtist`.
109
+ """
110
+ return self._MethodName
111
+
112
+
113
+ def ToString(self) -> str:
114
+ """
115
+ Returns a UI-friendly string representation of the class.
116
+ """
117
+ msg:str = SAAppMessages.MSG_SPOTIFY_WEB_API_AUTH_ERROR.format(methodname=self._MethodName,
118
+ error=self._Error,
119
+ httpstatus=self._HttpStatus,
120
+ httpreason=self._HttpReason,
121
+ errordescription=self._ErrorDescription)
122
+ return msg