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,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)
|