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