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.
- {spotifywebapiPython-1.0.35.dist-info → spotifywebapiPython-1.0.147.dist-info}/LICENSE +1 -1
- {spotifywebapiPython-1.0.35.dist-info → spotifywebapiPython-1.0.147.dist-info}/METADATA +7 -6
- spotifywebapiPython-1.0.147.dist-info/RECORD +117 -0
- spotifywebapipython/__init__.py +2 -0
- spotifywebapipython/const.py +19 -1
- spotifywebapipython/docs/__init__.html +799 -3
- spotifywebapipython/docs/index.html +21 -3
- spotifywebapipython/docs/search.js +1 -1
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/const.html +72 -4
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/saappmessages.html +77 -3
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/sautils.html +63 -3
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifyapierror.html +3 -3
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifyapimessage.html +20 -3
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifyauthtoken.html +39 -3
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifyclient.html +2553 -364
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifydiscovery.html +85 -52
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifywebapiauthenticationerror.html +3 -3
- spotifywebapipython/docs/{spotifywebapiPython → spotifywebapipython}/spotifywebapierror.html +3 -3
- spotifywebapipython/models/__init__.py +8 -0
- spotifywebapipython/models/albumpagesaved.py +2 -1
- spotifywebapipython/models/albumpagesimplified.py +2 -1
- spotifywebapipython/models/albumsimplified.py +4 -5
- spotifywebapipython/models/artist.py +4 -5
- spotifywebapipython/models/artistinfo.py +282 -0
- spotifywebapipython/models/artistinfotourevent.py +127 -0
- spotifywebapipython/models/artistpage.py +2 -1
- spotifywebapipython/models/artistsimplified.py +4 -0
- spotifywebapipython/models/audiobookpagesimplified.py +2 -1
- spotifywebapipython/models/audiobooksimplified.py +4 -5
- spotifywebapipython/models/category.py +16 -0
- spotifywebapipython/models/categorypage.py +2 -1
- spotifywebapipython/models/chapterpagesimplified.py +2 -1
- spotifywebapipython/models/chaptersimplified.py +6 -7
- spotifywebapipython/models/context.py +1 -3
- spotifywebapipython/models/device.py +85 -2
- spotifywebapipython/models/episodepagesaved.py +2 -1
- spotifywebapipython/models/episodepagesimplified.py +2 -1
- spotifywebapipython/models/episodesaved.py +2 -2
- spotifywebapipython/models/episodesimplified.py +5 -6
- spotifywebapipython/models/imageobject.py +98 -0
- spotifywebapipython/models/pageobject.py +29 -4
- spotifywebapipython/models/playeractions.py +33 -11
- spotifywebapipython/models/playerplaystate.py +128 -13
- spotifywebapipython/models/playerqueueinfo.py +61 -4
- spotifywebapipython/models/playhistorypage.py +2 -1
- spotifywebapipython/models/playlist.py +6 -3
- spotifywebapipython/models/playlistpage.py +2 -1
- spotifywebapipython/models/playlistpagesimplified.py +2 -1
- spotifywebapipython/models/playlistsimplified.py +49 -7
- spotifywebapipython/models/playlisttrack.py +1 -0
- spotifywebapipython/models/playlisttracksummary.py +3 -1
- spotifywebapipython/models/searchresponse.py +1 -1
- spotifywebapipython/models/showpagesaved.py +2 -1
- spotifywebapipython/models/showpagesimplified.py +2 -1
- spotifywebapipython/models/showsaved.py +2 -2
- spotifywebapipython/models/showsimplified.py +8 -5
- spotifywebapipython/models/spotifyconnectdevice.py +188 -0
- spotifywebapipython/models/spotifyconnectdevices.py +628 -0
- spotifywebapipython/models/track.py +2 -1
- spotifywebapipython/models/trackpage.py +42 -1
- spotifywebapipython/models/trackpagesaved.py +2 -1
- spotifywebapipython/models/trackpagesimplified.py +2 -1
- spotifywebapipython/models/trackrecommendations.py +5 -5
- spotifywebapipython/models/tracksimplified.py +10 -3
- spotifywebapipython/models/userprofile.py +25 -1
- spotifywebapipython/models/userprofilesimplified.py +4 -5
- spotifywebapipython/models/zeroconfdiscoveryresult.py +599 -0
- spotifywebapipython/models/zeroconfproperty.py +93 -0
- spotifywebapipython/oauthcli/authclient.py +119 -19
- spotifywebapipython/saappmessages.py +21 -1
- spotifywebapipython/sautils.py +88 -1
- spotifywebapipython/spotifyapimessage.py +18 -1
- spotifywebapipython/spotifyauthtoken.py +26 -0
- spotifywebapipython/spotifyclient.py +4311 -525
- spotifywebapipython/spotifyconnectdeviceeventargs.py +67 -0
- spotifywebapipython/spotifydiscovery.py +326 -136
- spotifywebapipython/zeroconfapi/__init__.py +23 -0
- spotifywebapipython/zeroconfapi/authenticationtypes.py +38 -0
- spotifywebapipython/zeroconfapi/blobbuilder.py +370 -0
- spotifywebapipython/zeroconfapi/credentials.py +37 -0
- spotifywebapipython/zeroconfapi/cryptodiffiehellman.py +89 -0
- spotifywebapipython/zeroconfapi/helpers.py +64 -0
- spotifywebapipython/zeroconfapi/spotifyzeroconfapierror.py +108 -0
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +1315 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +619 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfoalias.py +105 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfodrmmediaformat.py +90 -0
- spotifywebapipython/zeroconfapi/zeroconfresponse.py +141 -0
- spotifywebapiPython-1.0.35.dist-info/RECORD +0 -98
- {spotifywebapiPython-1.0.35.dist-info → spotifywebapiPython-1.0.147.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
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
|
-
|
|
42
|
+
if item is not None:
|
|
43
|
+
self._Items.append(TrackSaved(root=item))
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def __repr__(self) -> str:
|