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.
- spotifywebapipython/__init__.py +35 -0
- spotifywebapipython/const.py +114 -0
- spotifywebapipython/docs/__init__.html +2156 -0
- spotifywebapipython/docs/codecopy.js +70 -0
- spotifywebapipython/docs/index.html +514 -0
- spotifywebapipython/docs/search.js +46 -0
- spotifywebapipython/docs/spotifywebapipython/const.html +636 -0
- spotifywebapipython/docs/spotifywebapipython/saappmessages.html +540 -0
- spotifywebapipython/docs/spotifywebapipython/sautils.html +620 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +341 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +598 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +612 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +16557 -0
- spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +518 -0
- spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +421 -0
- spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +400 -0
- spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +635 -0
- spotifywebapipython/docs/spotifywebapipython.ico +0 -0
- spotifywebapipython/docs/spotifywebapipython_OLD.ico +0 -0
- spotifywebapipython/models/__init__.py +121 -0
- spotifywebapipython/models/album.py +170 -0
- spotifywebapipython/models/albumpagesaved.py +85 -0
- spotifywebapipython/models/albumpagesimplified.py +88 -0
- spotifywebapipython/models/albumsaved.py +102 -0
- spotifywebapipython/models/albumsimplified.py +308 -0
- spotifywebapipython/models/artist.py +146 -0
- spotifywebapipython/models/artistinfo.py +282 -0
- spotifywebapipython/models/artistinfotourevent.py +127 -0
- spotifywebapipython/models/artistpage.py +70 -0
- spotifywebapipython/models/artistsimplified.py +163 -0
- spotifywebapipython/models/audiobook.py +87 -0
- spotifywebapipython/models/audiobookpagesimplified.py +88 -0
- spotifywebapipython/models/audiobooksimplified.py +362 -0
- spotifywebapipython/models/audiofeatures.py +333 -0
- spotifywebapipython/models/author.py +67 -0
- spotifywebapipython/models/category.py +168 -0
- spotifywebapipython/models/categorypage.py +70 -0
- spotifywebapipython/models/chapter.py +84 -0
- spotifywebapipython/models/chapterpagesimplified.py +88 -0
- spotifywebapipython/models/chaptersimplified.py +387 -0
- spotifywebapipython/models/context.py +113 -0
- spotifywebapipython/models/copyright.py +79 -0
- spotifywebapipython/models/device.py +301 -0
- spotifywebapipython/models/episode.py +84 -0
- spotifywebapipython/models/episodepagesaved.py +85 -0
- spotifywebapipython/models/episodepagesimplified.py +88 -0
- spotifywebapipython/models/episodesaved.py +99 -0
- spotifywebapipython/models/episodesimplified.py +370 -0
- spotifywebapipython/models/explicitcontent.py +80 -0
- spotifywebapipython/models/externalids.py +91 -0
- spotifywebapipython/models/externalurls.py +104 -0
- spotifywebapipython/models/followers.py +81 -0
- spotifywebapipython/models/imageobject.py +193 -0
- spotifywebapipython/models/imagevibrantcolors.py +182 -0
- spotifywebapipython/models/linkedfrom.py +165 -0
- spotifywebapipython/models/narrator.py +67 -0
- spotifywebapipython/models/owner.py +162 -0
- spotifywebapipython/models/pageobject.py +343 -0
- spotifywebapipython/models/playeractions.py +196 -0
- spotifywebapipython/models/playerlastplayedinfo.py +334 -0
- spotifywebapipython/models/playerplaystate.py +467 -0
- spotifywebapipython/models/playerqueueinfo.py +207 -0
- spotifywebapipython/models/playhistory.py +136 -0
- spotifywebapipython/models/playhistorypage.py +85 -0
- spotifywebapipython/models/playlist.py +125 -0
- spotifywebapipython/models/playlistpage.py +85 -0
- spotifywebapipython/models/playlistpagesimplified.py +107 -0
- spotifywebapipython/models/playlistsimplified.py +334 -0
- spotifywebapipython/models/playlisttrack.py +143 -0
- spotifywebapipython/models/playlisttracksummary.py +80 -0
- spotifywebapipython/models/recommendationseed.py +135 -0
- spotifywebapipython/models/restrictions.py +86 -0
- spotifywebapipython/models/resumepoint.py +79 -0
- spotifywebapipython/models/searchresponse.py +391 -0
- spotifywebapipython/models/searchresultbase.py +299 -0
- spotifywebapipython/models/show.py +87 -0
- spotifywebapipython/models/showpagesaved.py +85 -0
- spotifywebapipython/models/showpagesimplified.py +88 -0
- spotifywebapipython/models/showsaved.py +99 -0
- spotifywebapipython/models/showsimplified.py +320 -0
- spotifywebapipython/models/spotifyconnectdevice.py +321 -0
- spotifywebapipython/models/spotifyconnectdevices.py +441 -0
- spotifywebapipython/models/track.py +142 -0
- spotifywebapipython/models/trackpage.py +110 -0
- spotifywebapipython/models/trackpagesaved.py +85 -0
- spotifywebapipython/models/trackpagesimplified.py +88 -0
- spotifywebapipython/models/trackrecommendations.py +88 -0
- spotifywebapipython/models/tracksaved.py +99 -0
- spotifywebapipython/models/tracksimplified.py +398 -0
- spotifywebapipython/models/userprofile.py +184 -0
- spotifywebapipython/models/userprofilesimplified.py +206 -0
- spotifywebapipython/models/zeroconfdiscoveryresult.py +592 -0
- spotifywebapipython/models/zeroconfproperty.py +93 -0
- spotifywebapipython/oauthcli/__init__.py +7 -0
- spotifywebapipython/oauthcli/authclient.py +1229 -0
- spotifywebapipython/saappmessages.py +86 -0
- spotifywebapipython/sautils.py +285 -0
- spotifywebapipython/spotifyapierror.py +66 -0
- spotifywebapipython/spotifyapimessage.py +269 -0
- spotifywebapipython/spotifyauthtoken.py +264 -0
- spotifywebapipython/spotifyclient.py +17885 -0
- spotifywebapipython/spotifyconnect/__init__.py +19 -0
- spotifywebapipython/spotifyconnect/spotifyconnectdeviceeventargs.py +67 -0
- spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +2564 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +467 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastcontroller.py +701 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +359 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastmultizonelistener.py +212 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfexceptions.py +20 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconflistener.py +335 -0
- spotifywebapipython/spotifydiscovery.py +473 -0
- spotifywebapipython/spotifymediatypes.py +53 -0
- spotifywebapipython/spotifywebapiauthenticationerror.py +122 -0
- spotifywebapipython/spotifywebapierror.py +106 -0
- spotifywebapipython/spotifywebplayertoken.py +743 -0
- spotifywebapipython/vibrant/__init__.py +17 -0
- spotifywebapipython/vibrant/generator.py +261 -0
- spotifywebapipython/vibrant/image.py +100 -0
- spotifywebapipython/vibrant/main.py +34 -0
- spotifywebapipython/vibrant/models.py +93 -0
- spotifywebapipython/vibrant/utils.py +56 -0
- 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 +1327 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +572 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfoalias.py +109 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfodrmmediaformat.py +90 -0
- spotifywebapipython/zeroconfapi/zeroconfresponse.py +195 -0
- spotifywebapipython-1.0.242.dist-info/LICENSE +22 -0
- spotifywebapipython-1.0.242.dist-info/METADATA +219 -0
- spotifywebapipython-1.0.242.dist-info/RECORD +138 -0
- spotifywebapipython-1.0.242.dist-info/WHEEL +5 -0
- 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
|