spotifywebapiPython 1.0.242__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. spotifywebapipython/__init__.py +35 -0
  2. spotifywebapipython/const.py +114 -0
  3. spotifywebapipython/docs/__init__.html +2156 -0
  4. spotifywebapipython/docs/codecopy.js +70 -0
  5. spotifywebapipython/docs/index.html +514 -0
  6. spotifywebapipython/docs/search.js +46 -0
  7. spotifywebapipython/docs/spotifywebapipython/const.html +636 -0
  8. spotifywebapipython/docs/spotifywebapipython/saappmessages.html +540 -0
  9. spotifywebapipython/docs/spotifywebapipython/sautils.html +620 -0
  10. spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +341 -0
  11. spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +598 -0
  12. spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +612 -0
  13. spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +16557 -0
  14. spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +518 -0
  15. spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +421 -0
  16. spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +400 -0
  17. spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +635 -0
  18. spotifywebapipython/docs/spotifywebapipython.ico +0 -0
  19. spotifywebapipython/docs/spotifywebapipython_OLD.ico +0 -0
  20. spotifywebapipython/models/__init__.py +121 -0
  21. spotifywebapipython/models/album.py +170 -0
  22. spotifywebapipython/models/albumpagesaved.py +85 -0
  23. spotifywebapipython/models/albumpagesimplified.py +88 -0
  24. spotifywebapipython/models/albumsaved.py +102 -0
  25. spotifywebapipython/models/albumsimplified.py +308 -0
  26. spotifywebapipython/models/artist.py +146 -0
  27. spotifywebapipython/models/artistinfo.py +282 -0
  28. spotifywebapipython/models/artistinfotourevent.py +127 -0
  29. spotifywebapipython/models/artistpage.py +70 -0
  30. spotifywebapipython/models/artistsimplified.py +163 -0
  31. spotifywebapipython/models/audiobook.py +87 -0
  32. spotifywebapipython/models/audiobookpagesimplified.py +88 -0
  33. spotifywebapipython/models/audiobooksimplified.py +362 -0
  34. spotifywebapipython/models/audiofeatures.py +333 -0
  35. spotifywebapipython/models/author.py +67 -0
  36. spotifywebapipython/models/category.py +168 -0
  37. spotifywebapipython/models/categorypage.py +70 -0
  38. spotifywebapipython/models/chapter.py +84 -0
  39. spotifywebapipython/models/chapterpagesimplified.py +88 -0
  40. spotifywebapipython/models/chaptersimplified.py +387 -0
  41. spotifywebapipython/models/context.py +113 -0
  42. spotifywebapipython/models/copyright.py +79 -0
  43. spotifywebapipython/models/device.py +301 -0
  44. spotifywebapipython/models/episode.py +84 -0
  45. spotifywebapipython/models/episodepagesaved.py +85 -0
  46. spotifywebapipython/models/episodepagesimplified.py +88 -0
  47. spotifywebapipython/models/episodesaved.py +99 -0
  48. spotifywebapipython/models/episodesimplified.py +370 -0
  49. spotifywebapipython/models/explicitcontent.py +80 -0
  50. spotifywebapipython/models/externalids.py +91 -0
  51. spotifywebapipython/models/externalurls.py +104 -0
  52. spotifywebapipython/models/followers.py +81 -0
  53. spotifywebapipython/models/imageobject.py +193 -0
  54. spotifywebapipython/models/imagevibrantcolors.py +182 -0
  55. spotifywebapipython/models/linkedfrom.py +165 -0
  56. spotifywebapipython/models/narrator.py +67 -0
  57. spotifywebapipython/models/owner.py +162 -0
  58. spotifywebapipython/models/pageobject.py +343 -0
  59. spotifywebapipython/models/playeractions.py +196 -0
  60. spotifywebapipython/models/playerlastplayedinfo.py +334 -0
  61. spotifywebapipython/models/playerplaystate.py +467 -0
  62. spotifywebapipython/models/playerqueueinfo.py +207 -0
  63. spotifywebapipython/models/playhistory.py +136 -0
  64. spotifywebapipython/models/playhistorypage.py +85 -0
  65. spotifywebapipython/models/playlist.py +125 -0
  66. spotifywebapipython/models/playlistpage.py +85 -0
  67. spotifywebapipython/models/playlistpagesimplified.py +107 -0
  68. spotifywebapipython/models/playlistsimplified.py +334 -0
  69. spotifywebapipython/models/playlisttrack.py +143 -0
  70. spotifywebapipython/models/playlisttracksummary.py +80 -0
  71. spotifywebapipython/models/recommendationseed.py +135 -0
  72. spotifywebapipython/models/restrictions.py +86 -0
  73. spotifywebapipython/models/resumepoint.py +79 -0
  74. spotifywebapipython/models/searchresponse.py +391 -0
  75. spotifywebapipython/models/searchresultbase.py +299 -0
  76. spotifywebapipython/models/show.py +87 -0
  77. spotifywebapipython/models/showpagesaved.py +85 -0
  78. spotifywebapipython/models/showpagesimplified.py +88 -0
  79. spotifywebapipython/models/showsaved.py +99 -0
  80. spotifywebapipython/models/showsimplified.py +320 -0
  81. spotifywebapipython/models/spotifyconnectdevice.py +321 -0
  82. spotifywebapipython/models/spotifyconnectdevices.py +441 -0
  83. spotifywebapipython/models/track.py +142 -0
  84. spotifywebapipython/models/trackpage.py +110 -0
  85. spotifywebapipython/models/trackpagesaved.py +85 -0
  86. spotifywebapipython/models/trackpagesimplified.py +88 -0
  87. spotifywebapipython/models/trackrecommendations.py +88 -0
  88. spotifywebapipython/models/tracksaved.py +99 -0
  89. spotifywebapipython/models/tracksimplified.py +398 -0
  90. spotifywebapipython/models/userprofile.py +184 -0
  91. spotifywebapipython/models/userprofilesimplified.py +206 -0
  92. spotifywebapipython/models/zeroconfdiscoveryresult.py +592 -0
  93. spotifywebapipython/models/zeroconfproperty.py +93 -0
  94. spotifywebapipython/oauthcli/__init__.py +7 -0
  95. spotifywebapipython/oauthcli/authclient.py +1229 -0
  96. spotifywebapipython/saappmessages.py +86 -0
  97. spotifywebapipython/sautils.py +285 -0
  98. spotifywebapipython/spotifyapierror.py +66 -0
  99. spotifywebapipython/spotifyapimessage.py +269 -0
  100. spotifywebapipython/spotifyauthtoken.py +264 -0
  101. spotifywebapipython/spotifyclient.py +17885 -0
  102. spotifywebapipython/spotifyconnect/__init__.py +19 -0
  103. spotifywebapipython/spotifyconnect/spotifyconnectdeviceeventargs.py +67 -0
  104. spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +2564 -0
  105. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +467 -0
  106. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastcontroller.py +701 -0
  107. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +359 -0
  108. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastmultizonelistener.py +212 -0
  109. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfexceptions.py +20 -0
  110. spotifywebapipython/spotifyconnect/spotifyconnectzeroconflistener.py +335 -0
  111. spotifywebapipython/spotifydiscovery.py +473 -0
  112. spotifywebapipython/spotifymediatypes.py +53 -0
  113. spotifywebapipython/spotifywebapiauthenticationerror.py +122 -0
  114. spotifywebapipython/spotifywebapierror.py +106 -0
  115. spotifywebapipython/spotifywebplayertoken.py +743 -0
  116. spotifywebapipython/vibrant/__init__.py +17 -0
  117. spotifywebapipython/vibrant/generator.py +261 -0
  118. spotifywebapipython/vibrant/image.py +100 -0
  119. spotifywebapipython/vibrant/main.py +34 -0
  120. spotifywebapipython/vibrant/models.py +93 -0
  121. spotifywebapipython/vibrant/utils.py +56 -0
  122. spotifywebapipython/zeroconfapi/__init__.py +23 -0
  123. spotifywebapipython/zeroconfapi/authenticationtypes.py +38 -0
  124. spotifywebapipython/zeroconfapi/blobbuilder.py +370 -0
  125. spotifywebapipython/zeroconfapi/credentials.py +37 -0
  126. spotifywebapipython/zeroconfapi/cryptodiffiehellman.py +89 -0
  127. spotifywebapipython/zeroconfapi/helpers.py +64 -0
  128. spotifywebapipython/zeroconfapi/spotifyzeroconfapierror.py +108 -0
  129. spotifywebapipython/zeroconfapi/zeroconfconnect.py +1327 -0
  130. spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +572 -0
  131. spotifywebapipython/zeroconfapi/zeroconfgetinfoalias.py +109 -0
  132. spotifywebapipython/zeroconfapi/zeroconfgetinfodrmmediaformat.py +90 -0
  133. spotifywebapipython/zeroconfapi/zeroconfresponse.py +195 -0
  134. spotifywebapipython-1.0.242.dist-info/LICENSE +22 -0
  135. spotifywebapipython-1.0.242.dist-info/METADATA +219 -0
  136. spotifywebapipython-1.0.242.dist-info/RECORD +138 -0
  137. spotifywebapipython-1.0.242.dist-info/WHEEL +5 -0
  138. spotifywebapipython-1.0.242.dist-info/top_level.txt +1 -0
@@ -0,0 +1,467 @@
1
+ # external package imports.
2
+ from pychromecast import Chromecast
3
+ import threading
4
+ import time
5
+
6
+ # our package imports.
7
+ from .spotifyconnectzeroconfcastcontroller import SpotifyConnectZeroconfCastController, TYPE_LAUNCH_ERROR, WAIT_INTERVAL
8
+ from spotifywebapipython.spotifyauthtoken import SpotifyAuthToken
9
+ from spotifywebapipython.zeroconfapi import ZeroconfGetInfo, ZeroconfResponse
10
+ from spotifywebapipython.oauthcli.authclient import AuthClient
11
+ from spotifywebapipython.const import (
12
+ SPOTIFY_API_AUTHORIZE_URL,
13
+ SPOTIFY_API_TOKEN_URL,
14
+ SPOTIFY_DESKTOP_APP_CLIENT_ID,
15
+ TRACE_METHOD_RESULT,
16
+ )
17
+
18
+ # get smartinspect logger reference; create a new session for this module name.
19
+ from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIColors
20
+ import logging
21
+
22
+ _logsi:SISession = SIAuto.Si.GetSession(__name__)
23
+ if (_logsi == None):
24
+ _logsi = SIAuto.Si.AddSession(__name__, True)
25
+ _logsi.SystemLogger = logging.getLogger(__name__)
26
+
27
+
28
+ class SpotifyConnectZeroconfCastAppTask(threading.Thread):
29
+ """
30
+ Spotify Connect Zeroconf Cast Application class.
31
+
32
+ Represents the Cast Application that is started on the Chromecast device
33
+ when a Spotify Connect connection request is made.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ castDevice:Chromecast,
39
+ spotifyClientInstance, # :SpotifyClient,
40
+ getInfoResponseReceivedCallback,
41
+ zeroconfResponseReceivedCallback,
42
+ transferPlayback:bool=False,
43
+ ) -> None:
44
+ """
45
+ Initializes a new instance of the class.
46
+
47
+ Args:
48
+ castDevice (Chromecast):
49
+ Chromecast device target.
50
+ spotifyClientInstance (SpotifyClient):
51
+ SpotifyClient instance used to transfer playback to the Chromecast device.
52
+ getInfoResponseReceivedCallback (function):
53
+ Function to call when a Spotify Connect GetInfoResponse data structure
54
+ has been received from the cast device.
55
+ zeroconfResponseReceivedCallback (function):
56
+ Function to call when a Spotify Connect ZeroconfResponse data structure
57
+ has been received from the cast device.
58
+ transferPlayback (bool):
59
+ True to transfer playback to the device; otherwise, False to just activate
60
+ the Spotify Cast App on the device.
61
+ """
62
+ # invoke base class method.
63
+ super().__init__()
64
+
65
+ # note that the initialization method executes in the parent thread;
66
+ # the code in the `run` method executes on a child (daemon) thread.
67
+
68
+ # set thread name before we start logging.
69
+ self.name = "Spotify Connect Zeroconf Cast App Task: \"%s\"" % castDevice.name
70
+
71
+ # trace.
72
+ _logsi.LogVerbose("%s - Initializing storage" % (self.name))
73
+
74
+ # initialize storage.
75
+ self._DeviceIdActivated:str = None
76
+ self._CastDevice:Chromecast = castDevice
77
+ self._GetInfoResponseReceivedCallback = getInfoResponseReceivedCallback
78
+ self._IsStopRequested:bool = False
79
+ self._SpotifyConnectZeroconfCastController:SpotifyConnectZeroconfCastController = None
80
+ self._SpotifyClientInstance = spotifyClientInstance
81
+ self._TransferPlayback:bool = transferPlayback
82
+ self._ZeroconfResponseReceivedCallback = zeroconfResponseReceivedCallback
83
+
84
+
85
+ @property
86
+ def CastDevice(self) -> Chromecast:
87
+ """
88
+ Returns the Chromecast device instance if one has been initialized;
89
+ otherwise, None.
90
+ """
91
+ return self._CastDevice
92
+
93
+
94
+ @property
95
+ def DeviceIdActivated(self) -> str:
96
+ """
97
+ Returns the Chromecast device id that was activated.
98
+
99
+ The actual deviceId that was activated may be different than the requested deviceId.
100
+ This can sometimes occur when activating a group, as getInfoResponse will return the
101
+ deviceId of the group coordinator instead of the deviceId of the group itself.
102
+ This only seems to happen in non-Google manufactured devices that have not properly
103
+ implemented the Cast protocol for grouped devices (e.g. KEF, etc); it never occurs when
104
+ casting to groups of devices manufactured by Google!
105
+ """
106
+ return self._DeviceIdActivated
107
+
108
+
109
+ @property
110
+ def IsStopRequested(self) -> bool:
111
+ """
112
+ Indicator used to denote the task has been asked to stop by the main thread.
113
+ """
114
+ return self._IsStopRequested
115
+
116
+ @IsStopRequested.setter
117
+ def IsStopRequested(self, value:bool):
118
+ """
119
+ Sets the IsStopRequested property value.
120
+ """
121
+ if isinstance(value, bool):
122
+ self._IsStopRequested = value
123
+
124
+
125
+ @property
126
+ def SpotifyClientInstance(self) -> object: # SpotifyClient:
127
+ """
128
+ SpotifyClient instance used to transfer playback to the Chromecast device.
129
+ """
130
+ return self._SpotifyClientInstance
131
+
132
+
133
+ @property
134
+ def TransferPlayback(self) -> bool:
135
+ """
136
+ True to transfer playback to the device;
137
+ otherwise, False to just activate the Spotify Cast App on the device.
138
+ """
139
+ return self._TransferPlayback
140
+
141
+
142
+ def _CallGetInfoResponseReceivedCallback(
143
+ self,
144
+ zcResponse:ZeroconfGetInfo=None,
145
+ ) -> None:
146
+ """
147
+ Executes the callback to process the zeroconf getInfo response from the cast app task.
148
+
149
+ Args:
150
+ zcResponse (ZeroconfGetInfo):
151
+ Zeroconf getInfo response object.
152
+ """
153
+ # was the callback defined?
154
+ if (self._GetInfoResponseReceivedCallback):
155
+
156
+ try:
157
+
158
+ # trace.
159
+ _logsi.LogVerbose("%s - Zeroconf getInfoResponse received; Spotify Connect info: \"%s\" (%s)" % (self.name, zcResponse.RemoteName, zcResponse.DeviceId))
160
+
161
+ # invoke callback to process the GetInfoResponse data.
162
+ self._GetInfoResponseReceivedCallback(self._CastDevice.uuid, zcResponse)
163
+
164
+ except Exception as ex:
165
+
166
+ # trace.
167
+ _logsi.LogException("An unhandled exception occured in GetInfoResponseReceivedCallback: %s" % (str(ex)), ex, logToSystemLogger=False)
168
+ #ignore exceptions, as there is nothing we can do about them!
169
+
170
+
171
+ def _CallZeroconfResponseReceivedCallback(
172
+ self,
173
+ zcResponse:ZeroconfResponse=None,
174
+ ) -> None:
175
+ """
176
+ Executes the callback to process the various zeroconf responses from the cast app task.
177
+
178
+ Args:
179
+ zcResponse (ZeroconfResponse):
180
+ Zeroconf response object.
181
+ """
182
+ # was the callback defined?
183
+ if (self._ZeroconfResponseReceivedCallback):
184
+
185
+ try:
186
+
187
+ # trace.
188
+ _logsi.LogVerbose("%s - Zeroconf \"%s\" received; Spotify Connect response: \"%s\" (%s)" % (self.name, zcResponse.ResponseSource, zcResponse.StatusString, zcResponse.Status))
189
+
190
+ # invoke callback to process the ZeroconfResponse data.
191
+ self._ZeroconfResponseReceivedCallback(self._CastDevice.uuid, zcResponse)
192
+
193
+ except Exception as ex:
194
+
195
+ # trace.
196
+ _logsi.LogException("An unhandled exception occured in ZeroconfResponseReceivedCallback: %s" % (str(ex)), ex, logToSystemLogger=False)
197
+ #ignore exceptions, as there is nothing we can do about them!
198
+
199
+
200
+ def run(self):
201
+ """
202
+ The task to perform on a seperate thread.
203
+
204
+ Note that any exceptions must be processed via a call to _PostLaunchErrorEvent, if you
205
+ want the exception to be see by the user; just raising an exception will only log it!
206
+ """
207
+ try:
208
+
209
+ # trace.
210
+ _logsi.LogVerbose("%s - Starting thread RUN / TASK method" % (self.name))
211
+ _logsi.LogThread(SILevel.Debug, "%s - Thread information" % (self.name), self)
212
+
213
+ # start the connection worker thread, if needed.
214
+ # failure to do this will result in the following exception being thrown when
215
+ # the `launch_app()` method is called:
216
+ # - `pychromecast.error.NotConnected: Chromecast unknown:8009 is connecting...`
217
+ if (self._CastDevice.is_idle):
218
+ if (self._CastDevice.socket_client is not None) and (self._CastDevice.socket_client.first_connection):
219
+ _logsi.LogVerbose("%s - Starting Chromecast connection worker thread for loginId \"%s\"" % (self.name, self.SpotifyClientInstance.SpotifyConnectLoginId))
220
+ self._CastDevice.start()
221
+ self._CastDevice.wait(10)
222
+
223
+ # get spotify desktop authorization token.
224
+ _logsi.LogVerbose("%s - Retrieving Spotify Desktop authorization token for loginId \"%s\"" % (self.name, self.SpotifyClientInstance.SpotifyConnectLoginId), colorValue=SIColors.Gold)
225
+ tokenSP:SpotifyAuthToken = self._GetSpotifyDesktopAuthorizationToken(self.SpotifyClientInstance.SpotifyConnectLoginId)
226
+
227
+ # if token was not created, then an exception occured during the launch.
228
+ # in this case, we can't do anything else.
229
+ if (tokenSP is None):
230
+ return
231
+
232
+ # create cast controller to control the Spotify Cast App on the device.
233
+ _logsi.LogVerbose("%s - Creating Spotify Cast App controller for loginId \"%s\"" % (self.name, self.SpotifyClientInstance.SpotifyConnectLoginId))
234
+ self._SpotifyConnectZeroconfCastController = SpotifyConnectZeroconfCastController(self._CastDevice, tokenSP.AccessToken, tokenSP.ExpiresAt, self.SpotifyClientInstance.SpotifyConnectLoginId)
235
+
236
+ # register handler for the cast controller;
237
+ _logsi.LogVerbose("%s - Registering handler for Cast Controller" % (self.name))
238
+ self._CastDevice.register_handler(self._SpotifyConnectZeroconfCastController)
239
+
240
+ # launch spotify chromecast app on the device, passing it the spotify desktop player authorization token info.
241
+ _logsi.LogVerbose("%s - Launching Spotify Chromecast App for loginId \"%s\"" % (self.name, self.SpotifyClientInstance.SpotifyConnectLoginId))
242
+ self._SpotifyConnectZeroconfCastController.launch_app(10)
243
+
244
+ # at this point the cast app should be either fully launched, or an error
245
+ # occured while trying to launch.
246
+
247
+ # did an error occur?
248
+ if (not self._SpotifyConnectZeroconfCastController.isLaunched):
249
+
250
+ # was it a launchError or getInfoError?
251
+ if (not self._SpotifyConnectZeroconfCastController.isAddUserError):
252
+ self._CallZeroconfResponseReceivedCallback(self._SpotifyConnectZeroconfCastController.zeroconfResponse)
253
+ _logsi.LogException("%s - Failed to launch spotify controller due to timeout" % (self.name), None, logToSystemLogger=False)
254
+ return
255
+
256
+ # if getInfoResponse was received, then execute callback to process the getInfoResponse.
257
+ if (not self._SpotifyConnectZeroconfCastController.isGetInfoError) and (self._SpotifyConnectZeroconfCastController.zeroconfGetInfo is not None):
258
+ self._CallGetInfoResponseReceivedCallback(self._SpotifyConnectZeroconfCastController.zeroconfGetInfo)
259
+
260
+ # was it a addUserError? if so, then sp_dc / sp_key credentials are probably bad.
261
+ if (self._SpotifyConnectZeroconfCastController.isAddUserError):
262
+ self._CallZeroconfResponseReceivedCallback(self._SpotifyConnectZeroconfCastController.zeroconfResponse)
263
+ _logsi.LogException("%s - Failed to launch spotify controller due to credentials error" % (self.name), None, logToSystemLogger=False)
264
+ return
265
+
266
+ # at this point the cast app launch was successful; the following steps were performed:
267
+ # - issues a Spotify Connect Zeroconf `getInfo` request to retrieve info about the device.
268
+ # - waits for a `getInfoResponse` message and processes the results.
269
+ # - issues a Spotify Connect Zeroconf `addUser` request to login the user to Spotify Connect.
270
+ # - waits for a `addUserResponse` (or `addUserError`) message and processes the results.
271
+
272
+ # at this point, the Spotify Connect `getInfo` has been made and the results
273
+ # populated; note that `getInfo` results are the same BEFORE the call to `addUser`
274
+ # as well as AFTER the call to `addUser` (no need to call `getInfo` again AFTER addUser).
275
+
276
+ # call the callback to process the getInfoResponse.
277
+ self._CallGetInfoResponseReceivedCallback(self._SpotifyConnectZeroconfCastController.zeroconfGetInfo)
278
+
279
+ # call the callback to process the addUserResponse.
280
+ self._CallZeroconfResponseReceivedCallback(self._SpotifyConnectZeroconfCastController.zeroconfResponse)
281
+
282
+ # store the activated deviceId, that was supplied to the authorization token in the addUser request.
283
+ self._DeviceIdActivated = self._SpotifyConnectZeroconfCastController.deviceId
284
+
285
+ # trace.
286
+ _logsi.LogVerbose("%s - User is logged in to Spotify Cast App; waiting for transfer playback to device \"%s\" ..." % (self.name, self._DeviceIdActivated))
287
+
288
+ # was transfer playback selected?
289
+ if (self.TransferPlayback == True):
290
+
291
+ # transfer playback to the Chromecast device.
292
+ _logsi.LogVerbose("%s - Transferring playback for loginId \"%s\"" % (self.name, self.SpotifyClientInstance.SpotifyConnectLoginId))
293
+ self.SpotifyClientInstance.PlayerTransferPlayback(self._DeviceIdActivated, play=True, refreshDeviceList=False)
294
+
295
+ # the cast device will receive a Chromecast message of payload type `transferSuccess`
296
+ # on successful transfer of playback; this will set the `isPlaybackTransferred` flag to True.
297
+
298
+ # wait for the launched spotify app to receive transfer playback control, either
299
+ # through the `PlayerTransferPlayback` executed above OR from another Spotify Connect
300
+ # capable player.
301
+ timeout:float = 20.0
302
+ counter:float = 0
303
+ while counter < (timeout + 1):
304
+ if (self._SpotifyConnectZeroconfCastController.waitPlaybackTransfer.wait(WAIT_INTERVAL)):
305
+ if (self._SpotifyConnectZeroconfCastController.isPlaybackTransferError):
306
+ self._PostLaunchErrorEvent(1001, "Playback transfer error: %s" % (self._SpotifyConnectZeroconfCastController.zeroconfResponse.StatusString))
307
+ return
308
+ break
309
+ if (counter >= timeout):
310
+ self._PostLaunchErrorEvent(1002, "Playback transfer error - Timed out waiting for playback transfer to device.")
311
+ return
312
+ counter += WAIT_INTERVAL
313
+ _logsi.LogVerbose("Waiting for transferSuccess Chromecast Message (%f seconds from initial request)" % (counter))
314
+
315
+ # update task status.
316
+ _logsi.LogVerbose("%s - Transfer Playback complete for loginId \"%s\"" % (self.name, self.SpotifyClientInstance.SpotifyConnectLoginId))
317
+
318
+ # call the callback to process the transferSuccess or transferError.
319
+ self._CallZeroconfResponseReceivedCallback(self._SpotifyConnectZeroconfCastController.zeroconfResponse)
320
+
321
+ # event loop; we have to keep this thread active in order to keep the
322
+ # Spotify App on the Chromecast device active and available.
323
+ while True:
324
+
325
+ # keep going until we are asked to stop.
326
+ if (self.IsStopRequested):
327
+ _logsi.LogVerbose("%s - Thread task stop requested" % (self.name))
328
+ break
329
+ time.sleep(0.50)
330
+
331
+ # trace.
332
+ _logsi.LogVerbose("%s - Thread task was stopped" % (self.name))
333
+
334
+ except Exception as ex:
335
+
336
+ # post launchError event with exception details.
337
+ # this will also log a trace message.
338
+ self._PostLaunchErrorEvent(1000, str(ex))
339
+
340
+ # trace.
341
+ _logsi.LogVerbose("%s - Thread task is ending due to exception" % (self.name))
342
+
343
+ #ignore exceptions, as there is nothing we can do about them!
344
+
345
+ finally:
346
+
347
+ try:
348
+ # unregister handler for the cast controller.
349
+ if (self._SpotifyConnectZeroconfCastController is not None):
350
+ _logsi.LogVerbose("%s - Unregistering handler for Cast Controller" % (self.name))
351
+ self._CastDevice.unregister_handler(self._SpotifyConnectZeroconfCastController)
352
+ self._SpotifyConnectZeroconfCastController.tear_down()
353
+ except:
354
+ pass
355
+
356
+
357
+ def _PostLaunchErrorEvent(
358
+ self,
359
+ returnCode:int,
360
+ message:str
361
+ ) -> None:
362
+ """
363
+ Formats a `launchError` Zeroconf Response, and executes the callback.
364
+ """
365
+ try:
366
+
367
+ # trace.
368
+ _logsi.LogError("%s - LaunchError event; (%s) %s" % (self.name, returnCode, message), logToSystemLogger=False)
369
+
370
+ # post launchError event with error details.
371
+ response:ZeroconfResponse = ZeroconfResponse()
372
+ response.ResponseSource = TYPE_LAUNCH_ERROR
373
+ response.Status = returnCode
374
+ response.StatusString = message
375
+ self._CallZeroconfResponseReceivedCallback(response)
376
+
377
+ except:
378
+
379
+ # ignore exceptions, as there is nothing we can do about them.
380
+ pass
381
+
382
+
383
+
384
+ def _GetSpotifyDesktopAuthorizationToken(
385
+ self,
386
+ loginId:str,
387
+ ) -> SpotifyAuthToken:
388
+ """
389
+ Retrieve Spotify Desktop authorrization access token from the token cache file.
390
+ The resulting token will be used to launch the Spotify cast app.
391
+ """
392
+ apiMethodName:str = '_GetSpotifyDesktopAuthorizationToken'
393
+
394
+ try:
395
+
396
+ # trace.
397
+ _logsi.EnterMethod(SILevel.Debug, apiMethodName)
398
+ _logsi.LogVerbose("Retrieving Spotify Desktop authorization access token for Spotify LoginId: '%s'" % (loginId))
399
+
400
+ # Spotify Desktop App scopes requested for Spotify Connect (streaming only as of 2024/08/13)
401
+ SPOTIFY_SCOPES:list = \
402
+ [
403
+ 'streaming',
404
+ ]
405
+
406
+ # create oauth provider for spotify authentication code with pkce.
407
+ _logsi.LogVerbose('creating OAuth2 provider for Spotify Authentication Code with PKCE')
408
+ authClient:AuthClient = AuthClient(
409
+ authorizationType='Authorization Code PKCE',
410
+ authorizationUrl=SPOTIFY_API_AUTHORIZE_URL,
411
+ tokenUrl=SPOTIFY_API_TOKEN_URL,
412
+ scope=SPOTIFY_SCOPES,
413
+ clientId=SPOTIFY_DESKTOP_APP_CLIENT_ID,
414
+ tokenStorageDir=self._SpotifyClientInstance.TokenStorageDir,
415
+ tokenStorageFile=self._SpotifyClientInstance.TokenStorageFile,
416
+ tokenProviderId='SpotifyWebApiAuthCodePkce',
417
+ tokenProfileId=loginId,
418
+ )
419
+
420
+ # raise an exception if the authorization token is not present, or the scope has changed.
421
+ # the user must create the authorization token outside of this process, as the Spotify
422
+ # authorization token is driven by responding to a web request for access using OAuth.
423
+ # as this process is running on a server, there is no way for the user to respond to the
424
+ # request (e.g. via browser nor command-line).
425
+ isAuthorized = authClient.IsAuthorized
426
+ _logsi.LogVerbose('Checking OAuth2 authorization status: IsAuthorized=%s' % isAuthorized)
427
+ if (isAuthorized == False):
428
+ raise Exception("Spotify Desktop Player authorization token was not found in the token cache file.")
429
+ else:
430
+ _logsi.LogVerbose('OAuth2 authorization token has previously been authorized')
431
+
432
+ # process results.
433
+ oauth2token:dict = authClient.Session.token
434
+ authToken = SpotifyAuthToken(authClient.AuthorizationType, authClient.TokenProfileId, root=oauth2token)
435
+
436
+ # does token need to be refreshed?
437
+ if authToken.IsExpired:
438
+
439
+ # refresh the token.
440
+ # this will also store the refreshed token to disk to be used later if required.
441
+ _logsi.LogVerbose('OAuth2 authorization token has expired, or is about to; token will be refreshed')
442
+ oauth2token:dict = authClient.RefreshToken()
443
+ authToken = SpotifyAuthToken(authClient.AuthorizationType, authClient.TokenProfileId, root=oauth2token)
444
+ _logsi.LogObject(SILevel.Verbose, TRACE_METHOD_RESULT % apiMethodName, authToken, excludeNonPublic=True)
445
+
446
+ else:
447
+
448
+ _logsi.LogVerbose('OAuth2 authorization token has not expired')
449
+
450
+ # return spotify desktop token to caller.
451
+ return authToken
452
+
453
+ except Exception as ex:
454
+
455
+ # post launchError event with exception details.
456
+ # this will also log a trace message.
457
+ self._PostLaunchErrorEvent(1003, str(ex))
458
+
459
+ # trace.
460
+ _logsi.LogVerbose("%s - Thread task is ending due to exception" % (self.name))
461
+
462
+ #ignore exceptions, as there is nothing we can do about them!
463
+
464
+ finally:
465
+
466
+ # trace.
467
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)