spotifywebapiPython 1.0.35__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.35.dist-info → spotifywebapiPython-1.0.147.dist-info}/LICENSE +1 -1
  2. {spotifywebapiPython-1.0.35.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 +799 -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 +4311 -525
  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.35.dist-info/RECORD +0 -98
  90. {spotifywebapiPython-1.0.35.dist-info → spotifywebapiPython-1.0.147.dist-info}/WHEEL +0 -0
  91. {spotifywebapiPython-1.0.35.dist-info → spotifywebapiPython-1.0.147.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,628 @@
1
+ # external package imports.
2
+ from datetime import datetime
3
+ from typing import Iterator
4
+
5
+ # our package imports.
6
+ from ..sautils import export
7
+ from .device import Device
8
+ from .spotifyconnectdevice import SpotifyConnectDevice
9
+ from .playerplaystate import PlayerPlayState
10
+ from .zeroconfdiscoveryresult import ZeroconfDiscoveryResult
11
+ from spotifywebapipython.zeroconfapi import ZeroconfGetInfo, ZeroconfGetInfoAlias
12
+
13
+ # get smartinspect logger reference; create a new session for this module name.
14
+ from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SIColors
15
+ import logging
16
+
17
+ _logsi:SISession = SIAuto.Si.GetSession(__name__)
18
+ if (_logsi == None):
19
+ _logsi = SIAuto.Si.AddSession(__name__, True)
20
+ _logsi.SystemLogger = logging.getLogger(__name__)
21
+
22
+
23
+ @export
24
+ class SpotifyConnectDevices():
25
+ """
26
+ Spotify Connect Devices collection.
27
+ """
28
+
29
+ def __init__(self) -> None:
30
+ """
31
+ Initializes a new instance of the class.
32
+ """
33
+ self._DateLastRefreshed:int = 0
34
+ self._Items:list[SpotifyConnectDevice] = []
35
+
36
+
37
+ def __getitem__(self, key) -> SpotifyConnectDevice:
38
+ return self._Items[key]
39
+
40
+
41
+ def __iter__(self) -> Iterator:
42
+ return iter(self._Items)
43
+
44
+
45
+ def __len__(self) -> int:
46
+ return len(self._Items)
47
+
48
+
49
+ def __repr__(self) -> str:
50
+ return self.ToString()
51
+
52
+
53
+ def __str__(self) -> str:
54
+ return self.ToString()
55
+
56
+
57
+ @property
58
+ def AgeLastRefreshed(self) -> float:
59
+ """
60
+ Number of seconds between the current date time and the `DateLastRefreshed` property value.
61
+ """
62
+ return (datetime.utcnow().timestamp() - self._DateLastRefreshed)
63
+
64
+
65
+ @property
66
+ def DateLastRefreshed(self) -> float:
67
+ """
68
+ Date and time the device list was last refreshed, in unix epoch format (e.g. 1669123919.331225).
69
+ """
70
+ return self._DateLastRefreshed
71
+
72
+ @DateLastRefreshed.setter
73
+ def DateLastRefreshed(self, value:float):
74
+ """
75
+ Sets the DateLastRefreshed property value.
76
+ """
77
+ if isinstance(value, float):
78
+ self._DateLastRefreshed = value
79
+
80
+
81
+ @property
82
+ def Items(self) -> list[SpotifyConnectDevice]:
83
+ """
84
+ Array of `SpotifyConnectDevice` objects.
85
+ """
86
+ return self._Items
87
+
88
+
89
+ @property
90
+ def ItemsCount(self) -> int:
91
+ """
92
+ Number of objects in the `Items` property array.
93
+ """
94
+ if self._Items is not None:
95
+ return len(self._Items)
96
+ return 0
97
+
98
+
99
+ def AddDynamicDevice(
100
+ self,
101
+ device:Device,
102
+ activeUser:str
103
+ ) -> None:
104
+ """
105
+ Adds a new dynamic device entry to the `Items` collection.
106
+
107
+ Args:
108
+ device (Device):
109
+ Device information to add.
110
+ activeUser (str):
111
+ Currently active user id.
112
+
113
+ Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
114
+ process, but still exist in the player device list. These are usually Spotify Player
115
+ client devices (e.g. mobile / web / desktop players) that utilize temporary device id's.
116
+ """
117
+ # validations.
118
+ if (device is None) or (not isinstance(device, Device)):
119
+ return
120
+
121
+ # build zeroconf discovery result with what we can.
122
+ discoverResult:ZeroconfDiscoveryResult = ZeroconfDiscoveryResult()
123
+ discoverResult.DeviceName = device.Name
124
+ discoverResult.HostIpAddresses.append('127.0.0.1')
125
+ discoverResult.Server = '127.0.0.1'
126
+ discoverResult.HostIpPort = 0
127
+ discoverResult.Name = device.Name
128
+ discoverResult.SpotifyConnectCPath = '/zc'
129
+ discoverResult.SpotifyConnectVersion = '1.0'
130
+
131
+ # build zeroconf getinfo result with what we can.
132
+ info:ZeroconfGetInfo = ZeroconfGetInfo()
133
+ info.ActiveUser = activeUser
134
+ info.DeviceId = device.Id
135
+ info.DeviceType = device.Type
136
+ info.RemoteName = device.Name
137
+ info.SpotifyError = 0
138
+ info.Status = 101
139
+ info.StatusString = 'OK'
140
+
141
+ # populate brand info for popular devices.
142
+ if (device.Name == "Web Player (Chrome)"):
143
+ info.BrandDisplayName = 'Google'
144
+ info.ModelDisplayName = 'Chrome'
145
+ info.ProductId = 'Web Player'
146
+ elif (device.Name == "Web Player (Microsoft Edge)"):
147
+ info.BrandDisplayName = 'Microsoft'
148
+ info.ModelDisplayName = 'Edge'
149
+ info.ProductId = 'Web Player'
150
+ else:
151
+ info.BrandDisplayName = 'unknown'
152
+ info.ModelDisplayName = 'unknown'
153
+ info.ProductId = 'unknown'
154
+
155
+ # add the device.
156
+ scDevice:SpotifyConnectDevice = SpotifyConnectDevice()
157
+ scDevice.Id = info.DeviceId
158
+ scDevice.Name = info.RemoteName
159
+ scDevice.DeviceInfo = info
160
+ scDevice.DiscoveryResult = discoverResult
161
+ self._Items.append(scDevice)
162
+
163
+ # trace.
164
+ _logsi.LogVerbose("Added SpotifyConnectDevices collection entry: \"%s\" (dynamic device=%s)" % (scDevice.Title, discoverResult.IsDynamicDevice), colorValue=SIColors.ForestGreen)
165
+
166
+
167
+ def GetActiveDevice(
168
+ self,
169
+ ) -> SpotifyConnectDevice:
170
+ """
171
+ Returns a Spotify Connect device if one is currently marked as the active device;
172
+ otherwise, null if there is no active device.
173
+
174
+ Note that this does not perform a real-time Spotify Web API query for the active device.
175
+ It simply checks the devices collection for an entry that was marked as the active device
176
+ by the `UpdatePlayerDevices` method.
177
+ """
178
+ # find and return the active device if one exists; otherwise, null.
179
+ result:SpotifyConnectDevice = None
180
+ scDevice:SpotifyConnectDevice
181
+ for scDevice in self._Items:
182
+ if (scDevice.DeviceInfo is not None):
183
+ if (scDevice.DeviceInfo.IsActiveDevice):
184
+ result = scDevice
185
+ break
186
+ return result
187
+
188
+
189
+ def RemoveDevice(
190
+ self,
191
+ deviceId:str,
192
+ dynamicDeviceOnly:bool=False
193
+ ) -> None:
194
+ """
195
+ Removes existing device entry(s) from the `Items` collection by device id value.
196
+
197
+ Args:
198
+ deviceId (str):
199
+ Device id to remove.
200
+ dynamicDeviceOnly (str):
201
+ True to only remove the device if it's a dynamic device entry;
202
+ otherwise, False to remove the device id regardless.
203
+ Default is False.
204
+
205
+ It's possible to have multiple devices with the same id (e.g. both a dynamic and a
206
+ zeroconf registered device).
207
+
208
+ Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
209
+ process, but still exist in the player device list. These are usually Spotify Player
210
+ client devices (e.g. mobile / web / desktop players) that utilize temporary device id's.
211
+ """
212
+ # validations.
213
+ if (not isinstance(dynamicDeviceOnly,bool)):
214
+ dynamicDeviceOnly = False
215
+
216
+ # remove device from devices collection;
217
+ # the reversed() function creates an iterator that traverses the list in reverse order.
218
+ # this ensures that removing an element doesn't affect the indices of the subsequent
219
+ # elements we're going to iterate over.
220
+ deviceIdCompare:str = "" + deviceId.lower()
221
+ scDevice:SpotifyConnectDevice
222
+ for idx in reversed(range(len(self._Items))):
223
+
224
+ scDevice = self._Items[idx]
225
+
226
+ # did we find a match by device id?
227
+ if (deviceIdCompare == scDevice.Id.lower()):
228
+
229
+ isFound:bool = True
230
+
231
+ # get dynamic device status.
232
+ isDynamic:bool = False
233
+ if (scDevice.DiscoveryResult is not None):
234
+ isDynamic = scDevice.DiscoveryResult.IsDynamicDevice
235
+
236
+ # are we checking for dynamic device? if so, and it's NOT dynamic, then don't delete it!
237
+ if (dynamicDeviceOnly) and (not isDynamic):
238
+ isFound = False
239
+
240
+ # are we deleting this device?
241
+ if (isFound):
242
+ _logsi.LogVerbose("Removing SpotifyConnectDevices collection entry: \"%s\" - %s" % (scDevice.Title, scDevice.DiscoveryResult.Description), colorValue=SIColors.Orange)
243
+ self._Items.pop(idx)
244
+
245
+
246
+ def UpdatePlayerDevices(
247
+ self,
248
+ playerDevices:list[Device],
249
+ activeUser:str,
250
+ playerState:PlayerPlayState=None
251
+ ) -> SpotifyConnectDevice:
252
+ """
253
+ Adds a list of dynamic player device entries to the `Items` collection, removes any
254
+ existing dynamic devices from the collection that are not in the `playerDevices` list,
255
+ and updates the currently active device based on playerState.
256
+
257
+ Args:
258
+ playerDevices (list[Device]):
259
+ List of current player devices obtained via a call to `GetPlayerDevices` method.
260
+ activeUser (str):
261
+ Currently active user id.
262
+ playerState (PlayerPlayState):
263
+ Current player state obtained via a call to `GetPlayerPlaybackState` method
264
+ which will be used to set the active device in the `Items` collection;
265
+ otherwise, null to bypass active device set.
266
+
267
+ Returns:
268
+ null if the `playerState` argument was not specified;
269
+ otherwise, the currently active Spotify Player device instance as determined by
270
+ the Spotify Player PlayState.
271
+
272
+ Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
273
+ process, but still exist in the player device list. These are usually Spotify Player
274
+ client devices (e.g. mobile / web / desktop players) that utilize temporary device id's.
275
+ """
276
+ result:SpotifyConnectDevice = None
277
+
278
+ # validations
279
+ if (not isinstance(playerDevices, list)):
280
+ return result
281
+
282
+ # add dynamic devices.
283
+ scDevice:SpotifyConnectDevice
284
+ device:Device
285
+ for device in playerDevices:
286
+
287
+ # is the player device present in the collection? if not, then add it.
288
+ if (not self.ContainsDeviceId(device.Id)):
289
+ self.AddDynamicDevice(device, activeUser)
290
+
291
+ # is the device in the available device list for this user context?
292
+ # if not, then set an indicator so we can re-activate it later if need be.
293
+ for scDevice in self._Items:
294
+ if (scDevice.DeviceInfo.DeviceId == device.Id):
295
+ scDevice.DeviceInfo.IsInDeviceList = True
296
+ break
297
+
298
+ # remove stale dynamic devices from results collection;
299
+ # the reversed() function creates an iterator that traverses the list in reverse order.
300
+ # this ensures that removing an element doesn't affect the indices of the subsequent
301
+ # elements we're going to iterate over.
302
+ for idx in reversed(range(len(self._Items))):
303
+
304
+ # only process dynamic devices.
305
+ scDevice = self._Items[idx]
306
+ if (scDevice.DiscoveryResult.IsDynamicDevice):
307
+
308
+ # process all player devices.
309
+ wasFound:bool = False
310
+ playerDevice:Device
311
+ for playerDevice in playerDevices:
312
+ if (scDevice.Id == playerDevice.Id):
313
+ wasFound = True
314
+ break
315
+
316
+ # if collection entry was not found in the player device list then remove it.
317
+ if (not wasFound):
318
+ _logsi.LogVerbose("Removed dynamic device \"%s\" (%s) from Spotify Connect Devices collection" % (scDevice.Name, scDevice.Id))
319
+ self._Items.pop(idx)
320
+
321
+ # update active device entry.
322
+ # returns null if the `playerState` argument was not specified;
323
+ # otherwise, the currently active Spotify Player device instance as determined by
324
+ # the Spotify Player PlayState (could be null if no active device).
325
+ result = self.UpdateActiveDevice(playerState)
326
+ return result
327
+
328
+
329
+ def UpdateActiveDevice(
330
+ self,
331
+ playerState:PlayerPlayState=None
332
+ ) -> SpotifyConnectDevice:
333
+ """
334
+ Updates the currently active device based on playerState.
335
+
336
+ Args:
337
+ playerState (PlayerPlayState):
338
+ Current player state obtained via a call to `GetPlayerPlaybackState` method
339
+ which will be used to set the active device in the `Items` collection;
340
+ otherwise, null to bypass active device set.
341
+
342
+ Returns:
343
+ The currently active Spotify Player device instance as determined by
344
+ the Spotify Player PlayState; could be null if no active device was found.
345
+ """
346
+ result:SpotifyConnectDevice = None
347
+
348
+ if (isinstance(playerState, PlayerPlayState)):
349
+
350
+ # we do this in case the active device is a "restricted" device; if it is, then it may
351
+ # not show up as the active device (common issue with Sonos devices).
352
+ # in this case, it WILL show up in the player state device property as the active device.
353
+ # if it IS active, then we will set the active flag.
354
+ # note that the PlayerPlayState device is a name and not a device id.
355
+ if playerState.Device is not None:
356
+
357
+ device = playerState.Device
358
+ for scDevice in self._Items:
359
+ scDevice.DeviceInfo.IsActiveDevice = False
360
+ if (device.Name == scDevice.Name):
361
+ _logsi.LogVerbose("Spotify Connect active device detected: \"%s\" (%s)" % (device.Name, device.Id))
362
+ scDevice.DeviceInfo.IsActiveDevice = True
363
+ result = scDevice
364
+
365
+ # returns null if the `playerState` argument was not specified;
366
+ # otherwise, the currently active Spotify Player device instance as determined by
367
+ # the Spotify Player PlayState (could be null if no active device).
368
+ return result
369
+
370
+
371
+ def ContainsDeviceId(self, value:str) -> bool:
372
+ """
373
+ Returns True if the `Items` collection contains the specified device id value;
374
+ otherwise, False.
375
+
376
+ Alias entries (if any) are also compared.
377
+ """
378
+ scDevice:SpotifyConnectDevice = self.GetDeviceById(value)
379
+ if scDevice is not None:
380
+ return True
381
+ return False
382
+
383
+
384
+ def ContainsDeviceName(self, value:str) -> bool:
385
+ """
386
+ Returns True if the `Items` collection contains the specified device name value;
387
+ otherwise, False.
388
+
389
+ Alias entries (if any) are also compared.
390
+ """
391
+ scDevice:SpotifyConnectDevice = self.GetDeviceByName(value)
392
+ if scDevice is not None:
393
+ return True
394
+ return False
395
+
396
+
397
+ def ContainsZeroconfEndpointGetInformation(self, value:str) -> bool:
398
+ """
399
+ Returns True if the `Items` collection contains the specified Zeroconf getInfo Endpoint url value;
400
+ otherwise, False.
401
+ """
402
+ result:bool = False
403
+ if value is None:
404
+ return result
405
+
406
+ # convert case for comparison.
407
+ value = value.lower()
408
+
409
+ # process all discovered devices.
410
+ scDevice:SpotifyConnectDevice
411
+ for scDevice in self._Items:
412
+ if (scDevice.DiscoveryResult.ZeroconfApiEndpointGetInformation.lower() == value):
413
+ result = True
414
+ break
415
+ return result
416
+
417
+
418
+ def GetDeviceByDiscoveryKey(self, value:str) -> SpotifyConnectDevice:
419
+ """
420
+ Returns a `SpotifyConnectDevice` instance if the `Items` collection contains the specified
421
+ device zeroconf discovery results key value; otherwise, None.
422
+ """
423
+ result:SpotifyConnectDevice = None
424
+ if value is None:
425
+ return result
426
+
427
+ # convert case for comparison.
428
+ value = value.lower()
429
+
430
+ # process all devices, comparing discovery keys.
431
+ for i in range(len(self._Items)):
432
+ if (self._Items[i].DiscoveryResult is not None):
433
+ if (self._Items[i].DiscoveryResult.Key is not None):
434
+ if (self._Items[i].DiscoveryResult.Key.lower() == value):
435
+ result = self._Items[i]
436
+ break
437
+ return result
438
+
439
+
440
+ def GetDeviceById(self, value:str) -> SpotifyConnectDevice:
441
+ """
442
+ Returns a `SpotifyConnectDevice` instance if the `Items` collection contains the specified
443
+ device id value; otherwise, None.
444
+
445
+ Alias entries (if any) are also compared.
446
+ """
447
+ result:SpotifyConnectDevice = None
448
+ if value is None:
449
+ return result
450
+
451
+ # convert case for comparison.
452
+ value = value.lower()
453
+
454
+ # process all discovered devices.
455
+ scDevice:SpotifyConnectDevice
456
+ for scDevice in self._Items:
457
+ if (scDevice.DeviceInfo.HasAliases):
458
+ scAlias:ZeroconfGetInfoAlias
459
+ for scAlias in scDevice.DeviceInfo.Aliases:
460
+ if (scAlias.Id.lower() == value):
461
+ result = scDevice
462
+ break
463
+ if result:
464
+ break
465
+ else:
466
+ if (scDevice.DeviceInfo.DeviceId.lower() == value):
467
+ result = scDevice
468
+ break
469
+ return result
470
+
471
+
472
+ def GetDeviceByName(self, value:str) -> SpotifyConnectDevice:
473
+ """
474
+ Returns a `SpotifyConnectDevice` instance if the `Items` collection contains the specified
475
+ device name value; otherwise, None.
476
+
477
+ Alias entries (if any) are also compared.
478
+ """
479
+ result:SpotifyConnectDevice = None
480
+ if value is None:
481
+ return result
482
+
483
+ # convert case for comparison.
484
+ value = value.lower()
485
+
486
+ # process all discovered devices.
487
+ scDevice:SpotifyConnectDevice
488
+ for scDevice in self._Items:
489
+ if (scDevice.DeviceInfo.HasAliases):
490
+ scAlias:ZeroconfGetInfoAlias
491
+ for scAlias in scDevice.DeviceInfo.Aliases:
492
+ if (scAlias.Name.lower() == value):
493
+ result = scDevice
494
+ break
495
+ if result is not None:
496
+ break
497
+ else:
498
+ if (scDevice.DeviceInfo.RemoteName.lower() == value):
499
+ result = scDevice
500
+ break
501
+ return result
502
+
503
+
504
+ def GetDeviceList(self) -> list[Device]:
505
+ """
506
+ Returns a list of `Device` objects that can be used to build a selection list
507
+ of available devices.
508
+
509
+ Note that the `Device` object has the following properties populated:
510
+ Id, Name, IsActive, Type.
511
+ """
512
+ result:list[Device] = []
513
+
514
+ # process all discovered devices.
515
+ scDevice:SpotifyConnectDevice
516
+ for scDevice in self._Items:
517
+
518
+ # map device information details.
519
+ info:ZeroconfGetInfo = scDevice.DeviceInfo
520
+
521
+ # create new mock device.
522
+ device = Device()
523
+ device.Type = info.DeviceType
524
+
525
+ # are aliases being used (RemoteName is null if so)?
526
+ if info.RemoteName is None:
527
+
528
+ # if aliases are defined, then use the alias details.
529
+ infoAlias:ZeroconfGetInfoAlias
530
+ for infoAlias in info.Aliases:
531
+ device.Id = infoAlias.Id
532
+ device.Name = infoAlias.Name
533
+
534
+ else:
535
+
536
+ # if no aliases then use the remote name and id.
537
+ device.Id = info.DeviceId
538
+ device.Name = info.RemoteName
539
+
540
+ # append device to results.
541
+ result.append(device)
542
+
543
+ # sort items on Name property, ascending order.
544
+ if len(result) > 0:
545
+ result.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
546
+
547
+ return result
548
+
549
+
550
+ def GetDeviceIndexByDiscoveryKey(self, value:str) -> int:
551
+ """
552
+ Returns the index of the `Items` collection entry that contains
553
+ the specified device zeroconf discovery results key value if found;
554
+ otherwise, -1.
555
+ """
556
+ result:int = -1
557
+ if value is None:
558
+ return result
559
+
560
+ # convert case for comparison.
561
+ value = value.lower()
562
+
563
+ # process all devices, comparing discovery keys.
564
+ for i in range(len(self._Items)):
565
+ if (self._Items[i].DiscoveryResult is not None):
566
+ if (self._Items[i].DiscoveryResult.Key is not None):
567
+ if (self._Items[i].DiscoveryResult.Key.lower() == value):
568
+ result = i
569
+ break
570
+ return result
571
+
572
+
573
+ def GetDeviceIndexByDiscoveryName(self, value:str) -> int:
574
+ """
575
+ Returns the index of the `Items` collection entry that contains
576
+ the specified device zeroconf discovery results name value if found;
577
+ otherwise, -1.
578
+ """
579
+ result:int = -1
580
+ if value is None:
581
+ return result
582
+
583
+ # convert case for comparison.
584
+ value = value.lower()
585
+
586
+ # process all devices, comparing discovery keys.
587
+ for i in range(len(self._Items)):
588
+ if (self._Items[i].DiscoveryResult is not None):
589
+ if (self._Items[i].DiscoveryResult.Name is not None):
590
+ if (self._Items[i].DiscoveryResult.Name.lower() == value):
591
+ result = i
592
+ break
593
+ return result
594
+
595
+
596
+ def ToDictionary(self) -> dict:
597
+ """
598
+ Returns a dictionary representation of the class.
599
+ """
600
+ result:dict = \
601
+ {
602
+ 'AgeLastRefreshed': self.AgeLastRefreshed,
603
+ 'DateLastRefreshed': self._DateLastRefreshed,
604
+ 'ItemsCount': self.ItemsCount,
605
+ 'Items': [ item.ToDictionary() for item in self._Items ],
606
+ }
607
+
608
+ return result
609
+
610
+
611
+ def ToString(self, includeItems:bool=False) -> str:
612
+ """
613
+ Returns a displayable string representation of the class.
614
+
615
+ Args:
616
+ includeItems (bool):
617
+ True to include the Items collection of objects; otherwise, False
618
+ to only return base properties.
619
+ """
620
+ msg:str = 'Items Count=%s' % (str(self.ItemsCount))
621
+ msg:str = '%s, DateLastRefreshed=%s' % (msg, str(self._DateLastRefreshed))
622
+
623
+ if (includeItems):
624
+ item:SpotifyConnectDevice
625
+ for item in self._Items:
626
+ msg = "%s\n- %s" % (msg, item.ToString(True))
627
+
628
+ return msg
@@ -120,7 +120,8 @@ class Track(TrackSimplified):
120
120
  {
121
121
  'album': album,
122
122
  'external_ids': externalIds,
123
- 'popularity': self._Popularity
123
+ 'popularity': self._Popularity,
124
+ 'image_url': self.ImageUrl,
124
125
  }
125
126
 
126
127
  # combine base class results with these results.
@@ -38,7 +38,8 @@ class TrackPage(PageObject):
38
38
  items:list = root.get('items',None)
39
39
  if items is not None:
40
40
  for item in items:
41
- self._Items.append(Track(root=item))
41
+ if item is not None:
42
+ self._Items.append(Track(root=item))
42
43
 
43
44
 
44
45
  def __repr__(self) -> str:
@@ -57,6 +58,46 @@ class TrackPage(PageObject):
57
58
  return self._Items
58
59
 
59
60
 
61
+ def GetUris(self, afterUri:str=None) -> str:
62
+ """
63
+ Gets a comma-delimited list of uri's contained in the underlying `Items` list.
64
+
65
+ args:
66
+ afterUri (str):
67
+ If specified, the process will look for this uri before adding subsequent
68
+ uri's to the returned results. This allows you to skip uri's that have
69
+ recently been played.
70
+
71
+ This is a convenience method so one does not have to loop through the `Items`
72
+ array of PlayHistory objects to get the list of uri's.
73
+ """
74
+ result:str = ""
75
+ addUri:bool = False
76
+ item:Track
77
+
78
+ # add uri's from the beginning if no `afterUri` argument was passed.
79
+ if (afterUri is None) or (len(afterUri.strip()) == 0):
80
+ addUri = True
81
+
82
+ for item in self._Items:
83
+
84
+ if (not addUri):
85
+
86
+ # check for `afterUri` match; if found, we will start adding uri's
87
+ # to the list with the next item.
88
+ if (afterUri == item.Uri):
89
+ addUri = True
90
+
91
+ elif (addUri):
92
+
93
+ # add item uri to the list.
94
+ if (len(result) > 0):
95
+ result += ","
96
+ result += item.Uri
97
+
98
+ return result
99
+
100
+
60
101
  def ToString(self, includeItems:bool=False) -> str:
61
102
  """
62
103
  Returns a displayable string representation of the class.
@@ -39,7 +39,8 @@ class TrackPageSaved(PageObject):
39
39
  items:list = root.get('items',None)
40
40
  if items is not None:
41
41
  for item in items:
42
- self._Items.append(TrackSaved(root=item))
42
+ if item is not None:
43
+ self._Items.append(TrackSaved(root=item))
43
44
 
44
45
 
45
46
  def __repr__(self) -> str: