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,701 @@
1
+ # external package imports.
2
+ from __future__ import annotations
3
+ import hashlib
4
+ import uuid
5
+ import json
6
+ import pychromecast
7
+ from pychromecast import Chromecast
8
+ from pychromecast.controllers import BaseController
9
+ from pychromecast.const import CAST_TYPE_GROUP
10
+ import requests
11
+ import threading
12
+ import time
13
+
14
+ # our package imports.
15
+ from .spotifyconnectzeroconfexceptions import SpotifyConnectZeroconfLaunchError
16
+ from spotifywebapipython import SpotifyApiError
17
+ from spotifywebapipython.saappmessages import SAAppMessages
18
+ from spotifywebapipython.zeroconfapi import SpotifyZeroconfApiError, ZeroconfGetInfo, ZeroconfResponse
19
+ from spotifywebapipython.const import (
20
+ SPOTIFY_API_AUTHORIZE_URL,
21
+ SPOTIFY_API_TOKEN_URL,
22
+ SPOTIFY_DESKTOP_APP_CLIENT_ID,
23
+ TRACE_METHOD_RESULT,
24
+ )
25
+
26
+ # get smartinspect logger reference; create a new session for this module name.
27
+ from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SIColors
28
+ import logging
29
+
30
+ _logsi:SISession = SIAuto.Si.GetSession(__name__)
31
+ if (_logsi == None):
32
+ _logsi = SIAuto.Si.AddSession(__name__, True)
33
+ _logsi.SystemLogger = logging.getLogger(__name__)
34
+
35
+ # constants.
36
+ APP_SPOTIFY = "CC32E753"
37
+ APP_NAMESPACE = "urn:x-cast:com.spotify.chromecast.secure.v1"
38
+ TYPE_GET_INFO = "getInfo"
39
+ TYPE_GET_INFO_ERROR = "getInfoError"
40
+ TYPE_GET_INFO_RESPONSE = "getInfoResponse"
41
+ TYPE_ADD_USER = "addUser"
42
+ TYPE_ADD_USER_RESPONSE = "addUserResponse"
43
+ TYPE_ADD_USER_ERROR = "addUserError"
44
+ TYPE_TRANSFER_SUCCESS = "transferSuccess"
45
+ TYPE_TRANSFER_ERROR = "transferError"
46
+ # not an actual Google Cast zeroconf response; used by this api to denote a launch / config error.
47
+ TYPE_LAUNCH_ERROR = "launchError"
48
+
49
+ SPOTIFY_WEB_API_DEVICEAUTH_REFRESH = "https://spclient.wg.spotify.com/device-auth/v1/refresh"
50
+
51
+ WAIT_INTERVAL:float = 0.5
52
+
53
+
54
+ class SpotifyConnectZeroconfCastController(BaseController):
55
+ """
56
+ Controller to interact with Spotify Connect Zeroconf API.
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ castDevice:Chromecast,
62
+ accessToken:str=None,
63
+ expiresAt:int=None,
64
+ spotifyConnectLoginId:str=None
65
+ ) -> None:
66
+ """
67
+ Initializes a new instance of the class.
68
+
69
+ Args:
70
+ castDevice (CastDevice):
71
+ Chromecast device target.
72
+ accessToken (str):
73
+ Access token provided to the Spotify Web Player application.
74
+ expiresAt (int):
75
+ DateTime (in epoch seconds) that the authorization token will expire.
76
+ spotifyConnectLoginId (str):
77
+ Spotify Connect LoginId value of the connecting user.
78
+ """
79
+
80
+ # invoke base class method.
81
+ super().__init__(APP_NAMESPACE, APP_SPOTIFY)
82
+
83
+ # initialize storage.
84
+ self.accessToken:str = accessToken
85
+ self.castDevice:Chromecast = castDevice
86
+ self.clientId:str = None
87
+ self.expiresAt:int = expiresAt
88
+ self.deviceId:str = None
89
+ self.isAddUserError:bool = False
90
+ self.isGetInfoError:bool = False
91
+ self.isLaunched:bool = False
92
+ self.isPlaybackTransferError:bool = False
93
+ self.isMessageException:bool = False
94
+ self.spotifyConnectLoginId:str = spotifyConnectLoginId
95
+ self.waitAddUser:threading.Event = threading.Event()
96
+ self.waitGetInfo:threading.Event = threading.Event()
97
+ self.waitPlaybackTransfer:threading.Event = threading.Event()
98
+ self.zeroconfGetInfo:ZeroconfGetInfo = None # for GetInformation response
99
+ self.zeroconfResponse:ZeroconfResponse = None # for non-GetInformation responses.
100
+
101
+ # create unique interactionID for this controller.
102
+ self.interactionID = str(uuid.uuid4())
103
+
104
+ # clear wait events.
105
+ self.waitAddUser.clear()
106
+ self.waitGetInfo.clear()
107
+ self.waitPlaybackTransfer.clear()
108
+
109
+
110
+ def launch_app(self,
111
+ timeout:float=15.0,
112
+ timeoutTransfer:float=20.0):
113
+ """
114
+ Launch Spotify Cast Application on the remote cast device.
115
+
116
+ Args:
117
+ timeout (float):
118
+ Number of seconds to wait for the Spotify cast app to be
119
+ launched on the Chromecast device.
120
+ timeoutTransfer (float):
121
+ Number of seconds to wait for the Spotify cast app to receive
122
+ playback control on the Chromecast device.
123
+
124
+ Launches the spotify controller and returns when it's ready.
125
+ To actually play media, another application using spotify
126
+ connect is required.
127
+
128
+ Will raise a SpotifyConnectZeroconfLaunchError exception if there is no
129
+ response from the Spotify app within timeout seconds.
130
+
131
+ Will raise a SpotifyConnectZeroconfLaunchError exception if player transfer
132
+ has not been completed to the Spotify app within timeoutTransfer seconds.
133
+
134
+ """
135
+ # validations.
136
+ if self.accessToken is None or self.expiresAt is None:
137
+ raise ValueError("accessToken and expiresAt cannot be empty")
138
+
139
+ # initialize launch variables.
140
+ self.deviceId = None
141
+ self.isLaunched = False
142
+ self.isAddUserError = False
143
+ self.isGetInfoError = False
144
+ self.isPlaybackTransferError = False
145
+ self.isMessageException = False
146
+ self.waitAddUser.clear()
147
+ self.waitGetInfo.clear()
148
+ self.waitPlaybackTransfer.clear()
149
+
150
+ def callback(*_):
151
+ """
152
+ Callback function
153
+ """
154
+ # for cast groups, we will wait a couple of seconds for any cast messages to be
155
+ # processed, before we query the device for Spotify Connect getInfo.
156
+ if (self.castDevice.cast_type == CAST_TYPE_GROUP):
157
+ delay:float = 3.0
158
+ _logsi.LogVerbose("Cast controller is for a Cast Group request; delaying processing for %s seconds to allow cast zeroconf responses to be processed" % (str(delay)), colorValue=SIColors.Red)
159
+ time.sleep(delay)
160
+
161
+ # send the initial getInfo request after launching the app.
162
+ # this will trigger an `addUser` request if the getInfo Response was successful.
163
+ self.GetInformation()
164
+
165
+ # launch the spotify app on the device.
166
+ # this will perform the following steps, initiated via the callback function:
167
+ # - issues a Spotify Connect Zeroconf `getInfo` request to retrieve info about the device.
168
+ # - waits for a `getInfoResponse` (or `getInfoError`) message and processes the results.
169
+ # - issues a Spotify Connect Zeroconf `addUser` request to login the user to Spotify Connect.
170
+ # - waits for a `addUserResponse` (or `addUserError`) message and processes the results.
171
+ self.launch(callback_function=callback)
172
+
173
+ # wait for the launched spotify app to fully initialize. this occurs when we receive one
174
+ # of the following messages: `addUserResponse`, `addUserError`, `getInfoError`, `launchError`.
175
+ counter:float = 0
176
+ while counter < (timeout + 1):
177
+ if (self.waitAddUser.wait(WAIT_INTERVAL)):
178
+ break
179
+ if (counter >= timeout):
180
+ raise SpotifyZeroconfApiError(0, "Timed out while waiting for a response", "launch_app", "timeout")
181
+ counter += WAIT_INTERVAL
182
+ _logsi.LogVerbose("Waiting for Spotify Cast App to launch (%f seconds from initial request)" % (counter))
183
+
184
+ # check for error conditions.
185
+ if (self.isAddUserError):
186
+ raise SpotifyZeroconfApiError(self.zeroconfResponse.Status, "AddUser request failed", "launch_app", self.zeroconfResponse.StatusString)
187
+ if (self.isGetInfoError):
188
+ raise SpotifyZeroconfApiError(self.zeroconfResponse.Status, "GetInformation request failed", "launch_app", self.zeroconfResponse.StatusString)
189
+ if (not self.isLaunched):
190
+ raise SpotifyConnectZeroconfLaunchError("Spotify Cast App could not be launched")
191
+
192
+
193
+ def stop_app(
194
+ self,
195
+ ) -> None:
196
+ """
197
+ Stops Spotify Cast Application on the remote cast device.
198
+
199
+ This will also log the user out of Spotify on the device, as well as cause a
200
+ `remove_cast` event to occur.
201
+ """
202
+ def callback(*_):
203
+ """
204
+ Callback function
205
+ """
206
+ # nothing to do here, but left the callback code here in case we need it in the future.
207
+ pass
208
+
209
+ # if socket is connected and running, then try to stop the Spotify Cast App.
210
+ if self._socket_client is not None:
211
+ if self._socket_client.is_connected:
212
+ if self._socket_client.receiver_controller is not None:
213
+ self._socket_client.receiver_controller.stop_app(
214
+ callback_function=callback,
215
+ )
216
+
217
+
218
+ def receive_message(
219
+ self,
220
+ message,
221
+ data: dict
222
+ ) -> None:
223
+ """
224
+ Called when a message is received from the Spotify cast app.
225
+
226
+ Args:
227
+ message (str):
228
+ Message header values; typical keys are:
229
+ - "protocol_version": cast protocol version (e.g. "CASTV2_1_0").
230
+ - "source_id": source application uuid (e.g. "a23351 ... 82ff84").
231
+ - "destination_id": destination id (e.g. "sender-0").
232
+ - "namespace": cast app namespace (e.g. "urn:x-cast:com.spotify.chromecast.secure.v1").
233
+ - "payload_type": payload type (e.g. "STRING").
234
+ data (dict):
235
+ Message payload dictionary; the important keys are:
236
+ - "type": type of payload (e.g. "getInfoResponse", "addUserResponse", "addUserError", etc).
237
+ - "payload": contains specific fields related to the "type" of message.
238
+
239
+ Handle the auth flow and active player selection.
240
+ """
241
+ payloadDataType:str = None
242
+ payloadData:dict = {}
243
+
244
+ try:
245
+
246
+ # trace.
247
+ _logsi.LogDictionary(SILevel.Verbose, "Chromecast Message payload received: %s" % message, data, prettyPrint=True)
248
+
249
+ # message argument example: "getInfoResponse"
250
+ # protocol_version: CASTV2_1_0
251
+ # source_id: "a233518a-505f-4d23-abcb-dfce4a82ff84"
252
+ # destination_id: "sender-0"
253
+ # namespace: "urn:x-cast:com.spotify.chromecast.secure.v1"
254
+ # payload_type: STRING
255
+ # payload_utf8: "{\"type\":\"getInfoResponse\",\"payload\":{\"remoteName\":\"Nest Audio 01\",\"deviceID\":\"bddc ... a3de\",\"deviceAPI_isGroup\":false,\"version\":\"2.9.0\",\"publicKey\":\"empty\",\"deviceType\":\"cast_audio\",\"brandDisplayName\":\"google\",\"modelDisplayName\":\"Google_Home\",\"libraryVersion\":\"5.44.1\",\"resolverVersion\":\"1\",\"groupStatus\":\"NONE\",\"tokenType\":\"accesstoken\",\"clientID\":\"d7d ... eae7\",\"productID\":0,\"scope\":\"streaming\",\"availability\":\"\",\"spotifyError\":0,\"status\":101,\"statusString\":\"OK\"}}"
256
+
257
+ # safely get the message payload structure type.
258
+ if (isinstance(data, dict)):
259
+ payloadDataType = data.get("type", None)
260
+ payloadData:dict = data.get("payload", {})
261
+
262
+ # process message payload.
263
+
264
+ # did getInfo succeed?
265
+ if payloadDataType == TYPE_GET_INFO_RESPONSE:
266
+
267
+ # example payload response (Nest Speaker):
268
+ # { 'type': 'getInfoResponse',
269
+ # 'payload': { 'remoteName': 'Nest Audio 01',
270
+ # 'deviceID': 'bddc7 ...',
271
+ # 'deviceAPI_isGroup': False,
272
+ # 'interactionIDs': ['df61044a-b785-4f99-97c3-5874d8e49d60'],
273
+ # 'version': '2.9.0',
274
+ # 'publicKey': 'empty',
275
+ # 'deviceType': 'cast_audio',
276
+ # 'brandDisplayName': 'google',
277
+ # 'modelDisplayName': 'Google_Home',
278
+ # 'libraryVersion': '5.44.1',
279
+ # 'resolverVersion': '1',
280
+ # 'groupStatus': 'NONE',
281
+ # 'tokenType': 'accesstoken',
282
+ # 'clientID': 'd7df0 ...',
283
+ # 'productID': 0,
284
+ # 'scope': 'streaming',
285
+ # 'availability': '',
286
+ # 'spotifyError': 0,
287
+ # 'status': 101,
288
+ # 'statusString': 'OK'}}
289
+
290
+ # example payload response (Cast Group):
291
+ # { 'type': 'getInfoResponse',
292
+ # 'payload': { 'remoteName': 'Nest Group 01',
293
+ # 'deviceID': 'c40cf ...',
294
+ # 'deviceAPI_isGroup': True,
295
+ # 'interactionIDs': ['df61044a-b785-4f99-97c3-5874d8e49d60'],
296
+ # 'version': '2.9.0',
297
+ # 'publicKey': 'empty',
298
+ # 'deviceType': 'cast_audio',
299
+ # 'brandDisplayName': 'google',
300
+ # 'modelDisplayName': 'Google_Home',
301
+ # 'libraryVersion': '5.48.2',
302
+ # 'resolverVersion': '1',
303
+ # 'groupStatus': 'GROUP',
304
+ # 'tokenType': 'accesstoken',
305
+ # 'clientID': 'd7df0 ...',
306
+ # 'productID': 0,
307
+ # 'scope': 'streaming',
308
+ # 'availability': '',
309
+ # 'spotifyError': 0,
310
+ # 'status': 101,
311
+ # 'statusString': 'OK'}}
312
+
313
+ # process message payload data.
314
+ self.zeroconfGetInfo = ZeroconfGetInfo(payloadData)
315
+ if (self.zeroconfGetInfo.PublicKey == 'empty'):
316
+ self.zeroconfGetInfo.PublicKey = None
317
+ self.zeroconfGetInfo.ResponseSource = payloadDataType
318
+
319
+ # trace.
320
+ _logsi.LogObject(SILevel.Verbose, "GetInformation result (ZeroconfGetInfo) - \"%s\" (%s)" % (self.zeroconfGetInfo.RemoteName, self.zeroconfGetInfo.DeviceId), self.zeroconfGetInfo, excludeNonPublic=True)
321
+
322
+ # getInfo response received from the initial launch.
323
+ self.deviceId = self.zeroconfGetInfo.DeviceId
324
+ self.clientId = self.zeroconfGetInfo.ClientId
325
+
326
+ # set status indicators and events.
327
+ self.isGetInfoError = False
328
+ self.waitGetInfo.set()
329
+
330
+ # if cast app is already launched, then we are done.
331
+ if (self.isLaunched):
332
+ return True
333
+
334
+ # otherwise, we are still in the process of launching the cast app via the callback.
335
+ # we will now send the initial addUser request to login to Spotify Connect, and
336
+ # we will wait for a response in the post-launch code.
337
+ self.AddUser()
338
+ return True
339
+
340
+ # did getInfo fail?
341
+ if payloadDataType == TYPE_GET_INFO_ERROR:
342
+
343
+ # example payload response (Nest Speaker):
344
+ # { 'type': 'getInfoError',
345
+ # 'payload': { 'status': 303,
346
+ # 'statusString': 'ERROR-INVALID-ARGUMENTS',
347
+ # 'spotifyError': 0,
348
+ # 'interactionIDs': ['df61044a-b785-4f99-97c3-5874d8e49d60']}}
349
+
350
+ # process message payload data.
351
+ self.zeroconfResponse = ZeroconfResponse(payloadData)
352
+ self.zeroconfResponse.ResponseSource = payloadDataType
353
+
354
+ # set status indicators and events.
355
+ self.isGetInfoError = True
356
+ self.deviceId = None
357
+ self.waitGetInfo.set()
358
+ if (not self.isLaunched):
359
+ # if app is not launched, then init is waiting on the `waitAddUser` event!
360
+ self.waitAddUser.set()
361
+ return True
362
+
363
+ # did addUser succeed?
364
+ if payloadDataType == TYPE_ADD_USER_RESPONSE:
365
+
366
+ # example payload response (Nest Speaker):
367
+ # { 'type': 'addUserResponse',
368
+ # 'payload': { 'status': 101,
369
+ # 'statusString': 'OK',
370
+ # 'spotifyError': 0,
371
+ # 'user': {'id': '31l77 ...'},
372
+ # 'deviceId': 'bddc ...',
373
+ # 'interactionIDs': ['df61044a-b785-4f99-97c3-5874d8e49d60']}}
374
+
375
+ # process message payload data.
376
+ self.zeroconfResponse = ZeroconfResponse(payloadData)
377
+ self.zeroconfResponse.ResponseSource = payloadDataType
378
+
379
+ try:
380
+ # stash the `user` value into the getInfo `ActiveUser` property as well.
381
+ if (self.zeroconfGetInfo.ActiveUser is None) or (self.zeroconfGetInfo.ActiveUser == ""):
382
+ self.zeroconfGetInfo.ActiveUser = payloadData["user"]["id"]
383
+ except:
384
+ pass # ignore errors.
385
+
386
+ # set status indicators and events.
387
+ self.isAddUserError = False
388
+ self.isLaunched = True
389
+ self.waitAddUser.set()
390
+ return True
391
+
392
+ # did addUser fail?
393
+ if payloadDataType == TYPE_ADD_USER_ERROR:
394
+
395
+ # example payload response (Nest Speaker):
396
+ # { 'type': 'addUserError',
397
+ # 'payload': { 'status': 303,
398
+ # 'statusString': 'ERROR-INVALID-ARGUMENTS',
399
+ # 'spotifyError': 0,
400
+ # 'interactionIDs': ['df61044a-b785-4f99-97c3-5874d8e49d60']}}
401
+
402
+ # process message payload data.
403
+ self.zeroconfResponse = ZeroconfResponse(payloadData)
404
+ self.zeroconfResponse.ResponseSource = payloadDataType
405
+
406
+ # set status indicators and events.
407
+ self.isAddUserError = True
408
+ self.deviceId = None
409
+ self.waitAddUser.set()
410
+ return True
411
+
412
+ # was playback transferred to the device?
413
+ if payloadDataType == TYPE_TRANSFER_SUCCESS:
414
+
415
+ # example payload response (Nest Speaker):
416
+ # { 'type': 'transferSuccess',
417
+ # 'payload': { 'status': 101,
418
+ # 'statusString': 'OK',
419
+ # 'spotifyError': 0,
420
+ # 'interactionIDs': ['df61044a-b785-4f99-97c3-5874d8e49d60']}}
421
+
422
+ # process message payload data.
423
+ self.zeroconfResponse = ZeroconfResponse(payloadData)
424
+ self.zeroconfResponse.ResponseSource = payloadDataType
425
+
426
+ # set status indicators and events.
427
+ self.isPlaybackTransferError = False
428
+ self.waitPlaybackTransfer.set()
429
+ return True
430
+
431
+ # did an error occur transferring playback to the device?
432
+ if payloadDataType == TYPE_TRANSFER_ERROR:
433
+
434
+ # example payload response (Nest Speaker):
435
+ # { 'type': 'transferError',
436
+ # 'payload': { 'status': 402,
437
+ # 'statusString': 'ERROR-SPOTIFY-ERROR',
438
+ # 'spotifyError': 408,
439
+ # 'interactionIDs': ['df61044a-b785-4f99-97c3-5874d8e49d60']}}
440
+
441
+ # process message payload data.
442
+ self.zeroconfResponse = ZeroconfResponse(payloadData)
443
+ self.zeroconfResponse.ResponseSource = payloadDataType
444
+
445
+ # set status indicators and events.
446
+ self.isPlaybackTransferError = True
447
+ self.waitPlaybackTransfer.set()
448
+ return True
449
+
450
+ # trace.
451
+ _logsi.LogDictionary(SILevel.Verbose, "Chromecast Message ignored - payload type value not recognized: \"%s\"" % (payloadDataType), message, prettyPrint=True)
452
+
453
+ return True
454
+
455
+ except Exception as ex:
456
+
457
+ # load zeroconfResponse instance with exception information.
458
+ response: ZeroconfResponse = ZeroconfResponse()
459
+ response.SpotifyError = 0
460
+ response.Status = 1000
461
+ response.StatusString = "ERROR-CHROMECAST-RECEIVE-MESSAGE"
462
+ response.ResponseSource = "SpotifyConnectZeroconfCastController"
463
+ self.zeroconfResponse = response
464
+
465
+ # trace.
466
+ _logsi.LogException(SAAppMessages.UNHANDLED_EXCEPTION.format("receive_message", str(ex)), ex, logToSystemLogger=False)
467
+
468
+ # ignore exceptions, as there is nothing we can do at this point.
469
+
470
+
471
+ def getSpotifyDeviceID(self) -> str:
472
+ """
473
+ Retrieve the Spotify deviceID from provided chromecast info
474
+ """
475
+ deviceId:str = hashlib.md5(self.castDevice.cast_info.friendly_name.encode()).hexdigest()
476
+ return deviceId
477
+
478
+
479
+ def AddUser(
480
+ self,
481
+ timeout:int=10,
482
+ ) -> None:
483
+ """
484
+ Calls the `addUser` Spotify Zeroconf API endpoint to login the user to Spotify Connect.
485
+
486
+ Args:
487
+ timeout (int):
488
+ Max time to wait (in seconds) for a response to the request.
489
+ Only applies if the `isLaunched=False`.
490
+
491
+ Raises:
492
+ SpotifyZeroconfApiError:
493
+ If the Spotify Zeroconf API request response contains error information.
494
+
495
+ The request will timeout after the specified `timeout` value, and an exception raised if the
496
+ device cannot be reached or does not respond within that time frame.
497
+ </details>
498
+ """
499
+ apiMethodName:str = 'AddUser'
500
+ apiMethodParms:SIMethodParmListContext = None
501
+ tracePrefix:str = 'Spotify Web Player Device Auth Token Refresh'
502
+
503
+ try:
504
+
505
+ # trace.
506
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
507
+ apiMethodParms.AppendKeyValue("timeout", timeout)
508
+ _logsi.LogMethodParmList(SILevel.Verbose, "Add User to Spotify Connect Chromecast Zeroconf API (ip=%s:%s)" % (self.castDevice.socket_client.host, self.castDevice.socket_client.port), apiMethodParms)
509
+
510
+ # validations.
511
+ if (timeout is None):
512
+ timeout=10
513
+
514
+ # clear wait event.
515
+ self.waitAddUser.clear()
516
+ self.addUserError = False
517
+
518
+ # prepare to refresh the access token.
519
+ reqHeaders = {
520
+ "authority": "spclient.wg.spotify.com",
521
+ "authorization": "Bearer {}".format(self.accessToken),
522
+ "content-type": "text/plain;charset=UTF-8",
523
+ }
524
+ reqParms = json.dumps(
525
+ {
526
+ "clientId": self.clientId,
527
+ "deviceId": self.deviceId
528
+ }
529
+ )
530
+
531
+ # trace.
532
+ _logsi.LogVerbose("Refreshing Spotify web player device authorization token")
533
+ _logsi.LogDictionary(SILevel.Debug, "%s http request: \"%s\" (headers)" % (tracePrefix, SPOTIFY_WEB_API_DEVICEAUTH_REFRESH), reqHeaders)
534
+ _logsi.LogText(SILevel.Verbose, "%s http request: \"%s\" (parms)" % (tracePrefix, SPOTIFY_WEB_API_DEVICEAUTH_REFRESH), reqParms)
535
+
536
+ # refresh the authorization token.
537
+ response = requests.post(
538
+ SPOTIFY_WEB_API_DEVICEAUTH_REFRESH,
539
+ headers=reqHeaders,
540
+ data=reqParms,
541
+ )
542
+
543
+ # trace.
544
+ if _logsi.IsOn(SILevel.Debug):
545
+ _logsi.LogObject(SILevel.Debug, "%s http response object - type=\"%s\", module=\"%s\"" % (tracePrefix, type(response).__name__, type(response).__module__), response)
546
+
547
+ # trace.
548
+ if _logsi.IsOn(SILevel.Debug):
549
+ _logsi.LogObject(SILevel.Debug, "%s http response [%s-%s] (response)" % (tracePrefix, response.status_code, response.reason), response)
550
+ if (response.headers):
551
+ _logsi.LogCollection(SILevel.Debug, "%s http response [%s-%s] (headers)" % (tracePrefix, response.status_code, response.reason), response.headers.items())
552
+
553
+ # process json response data.
554
+ json_resp = response.json()
555
+
556
+ # get hashed user name value;
557
+ # this is the SHA1 hash of the Spotify Connect LogonId value (e.g. "31l77y2 ...").
558
+ hashedUserName:str = hashlib.sha1((self.spotifyConnectLoginId or "").encode()).hexdigest()
559
+
560
+ # send the Spotify Connect Zeroconf `addUser` request.
561
+ # send request to cast app.
562
+ # this will login to the device, which (if successful) will cause the
563
+ # device to be registered to Spotify Connect Zeroconf as well as appear
564
+ # in the Spotify Player device list.
565
+ castMsg:dict = {
566
+ "type": TYPE_ADD_USER,
567
+ "payload": {
568
+ "interactionIDs": [self.interactionID],
569
+ "hashedUserName": hashedUserName,
570
+ "tokenType": "accesstoken",
571
+ "blob": json_resp["accessToken"],
572
+ },
573
+ }
574
+ _logsi.LogDictionary(SILevel.Debug, "Sending Spotify Connect Zeroconf AddUser Chromecast Message payload to Chromecast device (ip=%s:%s)" % (self.castDevice.socket_client.host, self.castDevice.socket_client.port), castMsg, prettyPrint=True)
575
+ self.send_message(castMsg)
576
+
577
+ # if we are in the process of launching, then we will wait for a response in the
578
+ # post-launch code as we are still in the launch callback sequence.
579
+ if (not self.isLaunched):
580
+ return
581
+
582
+ # if we are NOT in the process of launching, then we will wait for a response here.
583
+ counter:float = 0
584
+ while counter < (timeout + 1):
585
+ if (self.waitAddUser.wait(WAIT_INTERVAL)):
586
+ if (self.isAddUserError):
587
+ raise SpotifyZeroconfApiError(self.zeroconfResponse.Status, "AddUser request failed", "AddUser", self.zeroconfResponse.StatusString)
588
+ break
589
+ if (counter >= timeout):
590
+ raise SpotifyZeroconfApiError(0, "Timed out while waiting for a response", "AddUser", "timeout")
591
+ counter += WAIT_INTERVAL
592
+ _logsi.LogVerbose("Waiting for addUserResponse Chromecast Message payload (%f seconds from initial request)" % (counter))
593
+
594
+ # if we make it here, then addUser request was processed successfully.
595
+ return
596
+
597
+ except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
598
+ except Exception as ex:
599
+
600
+ # format unhandled exception.
601
+ raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
602
+
603
+ finally:
604
+
605
+ # trace.
606
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
607
+
608
+
609
+ def GetInformation(
610
+ self,
611
+ timeout:int=10,
612
+ ) -> ZeroconfGetInfo:
613
+ """
614
+ Calls the `getInfo` Spotify Zeroconf API endpoint to return information about the device.
615
+
616
+ Args:
617
+ timeout (int):
618
+ Max time to wait (in seconds) for a response to the request.
619
+ Only applies if the `isLaunched=False`.
620
+
621
+ Returns:
622
+ A `ZeroconfGetInfo` object that indicates success or failure (see notes below), as well
623
+ as the device information.
624
+
625
+ Raises:
626
+ SpotifyZeroconfApiError:
627
+ If the Spotify Zeroconf API request response contains error information.
628
+
629
+ The request will timeout after the specified `timeout` value, and an exception raised if the
630
+ device cannot be reached or does not respond within that time frame.
631
+ """
632
+ apiMethodName:str = 'GetInformation'
633
+ apiMethodParms:SIMethodParmListContext = None
634
+ result:ZeroconfGetInfo = None
635
+
636
+ try:
637
+
638
+ # trace.
639
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
640
+ apiMethodParms.AppendKeyValue("timeout", timeout)
641
+ _logsi.LogMethodParmList(SILevel.Verbose, "Get information from Spotify Connect Chromecast Zeroconf API (ip=%s:%s)" % (self.castDevice.socket_client.host, self.castDevice.socket_client.port), apiMethodParms)
642
+
643
+ # validations.
644
+ if (timeout is None):
645
+ timeout=10
646
+
647
+ # clear wait event.
648
+ self.waitGetInfo.clear()
649
+ self.isGetInfoError = False
650
+
651
+ # determine if this is a cast group or not.
652
+ isGroup:int = 0 # False
653
+ if (self.castDevice.model_name == "Google Cast Group"):
654
+ isGroup = 1 # True
655
+
656
+ # send request to cast app.
657
+ castMsg:dict = {
658
+ "type": TYPE_GET_INFO,
659
+ "payload": {
660
+ "brandDisplayName": "Google",
661
+ "modelDisplayName": self.castDevice.cast_info.model_name,
662
+ "remoteName": self.castDevice.cast_info.friendly_name,
663
+ "deviceID": self.getSpotifyDeviceID(),
664
+ "deviceAPI_isGroup": isGroup,
665
+ "interactionIDs": [self.interactionID],
666
+ },
667
+ }
668
+ _logsi.LogDictionary(SILevel.Debug, "Sending Spotify Connect Zeroconf Getinformation Chromecast Message payload to Chromecast device (ip=%s:%s)" % (self.castDevice.socket_client.host, self.castDevice.socket_client.port), castMsg, prettyPrint=True)
669
+ self.send_message(castMsg)
670
+
671
+ # if we are in the process of launching, then we will wait for a response in the
672
+ # post-launch code as we are still in the launch callback sequence.
673
+ if (not self.isLaunched):
674
+ return True
675
+
676
+ # if we are NOT in the process of launching, then we will wait for a response here.
677
+ counter:float = 0
678
+ while counter < (timeout + 1):
679
+ if (self.waitGetInfo.wait(WAIT_INTERVAL)):
680
+ if (self.isGetInfoError):
681
+ raise SpotifyZeroconfApiError(self.zeroconfGetInfo.Status, "GetInformation request failed", "GetInformation", self.zeroconfGetInfo.StatusString)
682
+ break
683
+ if (counter >= timeout):
684
+ raise SpotifyZeroconfApiError(0, "Timed out while waiting for a response", "GetInformation", "timeout")
685
+ counter += WAIT_INTERVAL
686
+ _logsi.LogVerbose("Waiting for getInfoResponse Chromecast Message payload (%f seconds from initial request)" % (counter))
687
+
688
+ # if we make it here, then getInfo request was processed successfully.
689
+ # return response to the caller.
690
+ return self.zeroconfGetInfo
691
+
692
+ except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
693
+ except Exception as ex:
694
+
695
+ # format unhandled exception.
696
+ raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
697
+
698
+ finally:
699
+
700
+ # trace.
701
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)