spotifywebapiPython 1.0.48__py3-none-any.whl → 1.0.50__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.
Potentially problematic release.
This version of spotifywebapiPython might be problematic. Click here for more details.
- {spotifywebapiPython-1.0.48.dist-info → spotifywebapiPython-1.0.50.dist-info}/METADATA +1 -1
- {spotifywebapiPython-1.0.48.dist-info → spotifywebapiPython-1.0.50.dist-info}/RECORD +24 -24
- spotifywebapipython/const.py +1 -1
- spotifywebapipython/docs/__init__.html +20 -2
- spotifywebapipython/docs/index.html +2 -2
- spotifywebapipython/docs/search.js +1 -1
- spotifywebapipython/docs/spotifywebapiPython/const.html +3 -3
- spotifywebapipython/docs/spotifywebapiPython/saappmessages.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/sautils.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifyapierror.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifyapimessage.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifyauthtoken.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifyclient.html +319 -3
- spotifywebapipython/docs/spotifywebapiPython/spotifydiscovery.html +20 -61
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapiauthenticationerror.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapierror.html +2 -2
- spotifywebapipython/models/userprofile.py +24 -0
- spotifywebapipython/spotifyclient.py +505 -8
- spotifywebapipython/spotifydiscovery.py +32 -80
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +10 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +21 -3
- {spotifywebapiPython-1.0.48.dist-info → spotifywebapiPython-1.0.50.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.48.dist-info → spotifywebapiPython-1.0.50.dist-info}/WHEEL +0 -0
- {spotifywebapiPython-1.0.48.dist-info → spotifywebapiPython-1.0.50.dist-info}/top_level.txt +0 -0
|
@@ -9,6 +9,7 @@ from typing import Tuple, Callable
|
|
|
9
9
|
from urllib3 import PoolManager, Timeout, HTTPResponse
|
|
10
10
|
from urllib.parse import urlencode
|
|
11
11
|
from lxml.etree import fromstring, Element
|
|
12
|
+
from zeroconf import Zeroconf
|
|
12
13
|
|
|
13
14
|
# our package imports.
|
|
14
15
|
from .oauthcli import AuthClient
|
|
@@ -19,8 +20,11 @@ from .saappmessages import SAAppMessages
|
|
|
19
20
|
from .spotifyapierror import SpotifyApiError
|
|
20
21
|
from .spotifyapimessage import SpotifyApiMessage
|
|
21
22
|
from .spotifyauthtoken import SpotifyAuthToken
|
|
23
|
+
from .spotifydiscovery import SpotifyDiscovery
|
|
22
24
|
from .spotifywebapiauthenticationerror import SpotifyWebApiAuthenticationError
|
|
23
25
|
from .spotifywebapierror import SpotifyWebApiError
|
|
26
|
+
from .zeroconfapi.zeroconfconnect import ZeroconfConnect, ZeroconfGetInfo, ZeroconfResponse, SpotifyZeroconfApiError
|
|
27
|
+
from .zeroconfapi.zeroconfgetinfoalias import ZeroconfGetInfoAlias
|
|
24
28
|
from .const import (
|
|
25
29
|
SPOTIFY_API_AUTHORIZE_URL,
|
|
26
30
|
SPOTIFY_API_TOKEN_URL,
|
|
@@ -71,7 +75,11 @@ class SpotifyClient:
|
|
|
71
75
|
def __init__(self,
|
|
72
76
|
manager:PoolManager=None,
|
|
73
77
|
tokenStorageDir:str=None,
|
|
74
|
-
tokenUpdater:Callable=None
|
|
78
|
+
tokenUpdater:Callable=None,
|
|
79
|
+
zeroconfClient:Zeroconf=None,
|
|
80
|
+
spotifyConnectUsername:str=None,
|
|
81
|
+
spotifyConnectPassword:str=None,
|
|
82
|
+
spotifyConnectDiscoveryTimeout:float=2.0
|
|
75
83
|
) -> None:
|
|
76
84
|
"""
|
|
77
85
|
Initializes a new instance of the class.
|
|
@@ -87,14 +95,58 @@ class SpotifyClient:
|
|
|
87
95
|
A method to call when a token needs to be refreshed by an external provider.
|
|
88
96
|
The defined method is called with no parameters, and should return a token dictionary.
|
|
89
97
|
Default is null.
|
|
90
|
-
|
|
98
|
+
zeroconfClient (Zeroconf)
|
|
99
|
+
A Zeroconf client instance that will be used to discover Spotify Connect devices,
|
|
100
|
+
or null to create a new instance of Zeroconf.
|
|
101
|
+
Default is null.
|
|
102
|
+
spotifyConnectUsername (str):
|
|
103
|
+
Spotify Connect user name used to login to a Spotify Connect device.
|
|
104
|
+
This MUST match the account name (or one of them) that was used to configure Spotify Connect
|
|
105
|
+
on a manufacturer device.
|
|
106
|
+
spotifyConnectPassword (str):
|
|
107
|
+
Spotify Connect password used to login to a Spotify Connect device.
|
|
108
|
+
This value is required if a non-null value was specified for the `spotifyConnectUsername` argument.
|
|
109
|
+
spotifyConnectDiscoveryTimeout (float):
|
|
110
|
+
Maximum amount of time to wait (in seconds) for the Spotify Connect discovery to complete.
|
|
111
|
+
Default is 2 seconds.
|
|
112
|
+
|
|
113
|
+
The `spotifyConnectUsername` and `spotifyConnectPassword` arguments are only used when a
|
|
114
|
+
Spotify Connect account switch is performed on a selected player device. Note that these credentials
|
|
115
|
+
are NOT used to access the Spotify Web API (the Spotify Web API token is used for that).
|
|
116
|
+
Please see the `PlayerResolveDeviceId` method for more details.
|
|
117
|
+
|
|
118
|
+
Note that if you don't have a password setup for your Spotify account (e.g. you utilize the "Continue with Google"
|
|
119
|
+
or other non-password methods for login), then you will need to define a "device password" in order to use the
|
|
120
|
+
ZeroConf Connect service; use the [Spotify Set Device Password](https://www.spotify.com/uk/account/set-device-password/)
|
|
121
|
+
page to define a device password. You will then use your Spotify username and the device password as your
|
|
122
|
+
Spotify Connect credentials.
|
|
123
|
+
"""
|
|
124
|
+
# validations.
|
|
125
|
+
if zeroconfClient is not None and (not isinstance(zeroconfClient, Zeroconf)):
|
|
126
|
+
raise SpotifyApiError(SAAppMessages.ARGUMENT_TYPE_ERROR % ("__init__", 'zeroconfClient', 'Zeroconf', type(zeroconfClient).__name__), logsi=_logsi)
|
|
127
|
+
|
|
128
|
+
# timeout value needs to be a float.
|
|
129
|
+
if (spotifyConnectDiscoveryTimeout is None):
|
|
130
|
+
spotifyConnectDiscoveryTimeout = 2.0
|
|
131
|
+
if (not isinstance(spotifyConnectDiscoveryTimeout, int) and (not isinstance(spotifyConnectDiscoveryTimeout, float))):
|
|
132
|
+
spotifyConnectDiscoveryTimeout = 2.0
|
|
133
|
+
|
|
134
|
+
# password is required if username was specified.
|
|
135
|
+
if spotifyConnectUsername is not None:
|
|
136
|
+
if (spotifyConnectPassword is None) or (not isinstance(spotifyConnectPassword,str)):
|
|
137
|
+
raise SpotifyApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % ("__init__", 'spotifyConnectPassword'), logsi=_logsi)
|
|
138
|
+
|
|
91
139
|
self._AuthToken:SpotifyAuthToken = None
|
|
92
140
|
self._AuthClient:AuthClient = None
|
|
93
141
|
self._ConfigurationCache:dict = {}
|
|
94
142
|
self._Manager:PoolManager = manager
|
|
143
|
+
self._SpotifyConnectUsername:str = spotifyConnectUsername
|
|
144
|
+
self._SpotifyConnectPassword:str = spotifyConnectPassword
|
|
145
|
+
self._SpotifyConnectDiscoveryTimeout:float = float(spotifyConnectDiscoveryTimeout)
|
|
95
146
|
self._TokenStorageDir:str = tokenStorageDir
|
|
96
147
|
self._TokenUpdater:Callable = tokenUpdater
|
|
97
148
|
self._UserProfile:UserProfile = None
|
|
149
|
+
self._ZeroconfClient = zeroconfClient
|
|
98
150
|
|
|
99
151
|
# if pool manager instance is none or not a PoolManager instance, then create one.
|
|
100
152
|
# we increase the maximum number of connections to keep in the pool (maxsize=) to avoid the following warnings:
|
|
@@ -109,6 +161,13 @@ class SpotifyClient:
|
|
|
109
161
|
maxsize=30, # maximum number of connections to keep in the pool.
|
|
110
162
|
block=True # limit number of connections to the device.
|
|
111
163
|
)
|
|
164
|
+
|
|
165
|
+
# create the zeroconf client if one was not specified.
|
|
166
|
+
if zeroconfClient is None:
|
|
167
|
+
_logsi.LogVerbose("Creating new Zeroconf instance for discovery")
|
|
168
|
+
self._ZeroconfClient = Zeroconf()
|
|
169
|
+
else:
|
|
170
|
+
_logsi.LogObject(SILevel.Verbose, "Using existing Zeroconf instance for discovery", zeroconfClient)
|
|
112
171
|
|
|
113
172
|
|
|
114
173
|
def __enter__(self) -> 'SpotifyClient':
|
|
@@ -240,6 +299,14 @@ class SpotifyClient:
|
|
|
240
299
|
return self._UserProfile
|
|
241
300
|
|
|
242
301
|
|
|
302
|
+
@property
|
|
303
|
+
def ZeroconfClient(self) -> Zeroconf:
|
|
304
|
+
"""
|
|
305
|
+
Zeroconf client instance that will be used to discover Spotify Connect devices.
|
|
306
|
+
"""
|
|
307
|
+
return self._ZeroconfClient
|
|
308
|
+
|
|
309
|
+
|
|
243
310
|
def _CheckForNextPageWithOffset(self,
|
|
244
311
|
pageObj:PageObject,
|
|
245
312
|
resultItemsCount,
|
|
@@ -5875,6 +5942,166 @@ class SpotifyClient:
|
|
|
5875
5942
|
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
5876
5943
|
|
|
5877
5944
|
|
|
5945
|
+
def PlayerActivateDevices(
|
|
5946
|
+
self,
|
|
5947
|
+
verifyUserContext:bool=False,
|
|
5948
|
+
delay:float=0.50,
|
|
5949
|
+
) -> None:
|
|
5950
|
+
"""
|
|
5951
|
+
Activates all Spotify Connect player devices, and (optionally) switches the active user
|
|
5952
|
+
context to the current user context.
|
|
5953
|
+
|
|
5954
|
+
Args:
|
|
5955
|
+
verifyUserContext (bool):
|
|
5956
|
+
If True, the active user context of the resolved device is checked to ensure it
|
|
5957
|
+
matches the user context specified on the class constructor.
|
|
5958
|
+
If False, the user context will not be checked.
|
|
5959
|
+
Default is False.
|
|
5960
|
+
delay (float):
|
|
5961
|
+
Time delay (in seconds) to wait AFTER issuing the final Connect command (if necessary).
|
|
5962
|
+
This delay will give the spotify web api time to process the device list change before
|
|
5963
|
+
another command is issued.
|
|
5964
|
+
Default is 0.50; value range is 0 - 10.
|
|
5965
|
+
|
|
5966
|
+
Raises:
|
|
5967
|
+
SpotifyWebApiError:
|
|
5968
|
+
If the Spotify Web API request was for a non-authorization service
|
|
5969
|
+
and the response contains error information.
|
|
5970
|
+
SpotifyZeroconfApiError:
|
|
5971
|
+
If any Spotify Zeroconf API request failed.
|
|
5972
|
+
If the `spotifyConnectUsername` aregument was not specified for the class contructor.
|
|
5973
|
+
SpotifApiError:
|
|
5974
|
+
If the method fails for any other reason.
|
|
5975
|
+
|
|
5976
|
+
A Zeroconf discovery process is initiated to search for all Spotify Connect devices connected
|
|
5977
|
+
to the local network. A Spotify Zeroconf API `getInfo` call is then issued for each device
|
|
5978
|
+
that was found to retrieve device information. A Spotify Zeroconf API `addUser` call is also
|
|
5979
|
+
issued for each device that does not have an active user context established, OR if the currently
|
|
5980
|
+
active user context does not match the `SpotifyConnectUsername` argument specified on the class constructor.
|
|
5981
|
+
|
|
5982
|
+
The user context switch is bypassed if the `verifyUserContext` argument is False. If the user context
|
|
5983
|
+
is to be switched, then a Disconnect will be issued if a user context is active on the device followed
|
|
5984
|
+
by a Connect to the user context specified on the class constructor.
|
|
5985
|
+
|
|
5986
|
+
<details>
|
|
5987
|
+
<summary>Sample Code</summary>
|
|
5988
|
+
```python
|
|
5989
|
+
.. include:: ../docs/include/samplecode/SpotifyClient/PlayerActivateDevices.py
|
|
5990
|
+
```
|
|
5991
|
+
</details>
|
|
5992
|
+
"""
|
|
5993
|
+
apiMethodName:str = 'PlayerActivateDevices'
|
|
5994
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
5995
|
+
result:str = ""
|
|
5996
|
+
|
|
5997
|
+
MSG_RESULT:str = '- Device Name="%s", ID="%s", ActiveUser="%s", Status=%s'
|
|
5998
|
+
|
|
5999
|
+
try:
|
|
6000
|
+
|
|
6001
|
+
# trace.
|
|
6002
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
6003
|
+
apiMethodParms.AppendKeyValue("verifyUserContext", verifyUserContext)
|
|
6004
|
+
apiMethodParms.AppendKeyValue("delay", delay)
|
|
6005
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Activating Spotify Connect Player Devices", apiMethodParms)
|
|
6006
|
+
|
|
6007
|
+
# validation - ensure spotify connect user context was specified on the class constructor.
|
|
6008
|
+
if (self._SpotifyConnectUsername is None):
|
|
6009
|
+
raise SpotifyZeroconfApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % (apiMethodName, 'SpotifyConnectUsername'), logsi=_logsi)
|
|
6010
|
+
|
|
6011
|
+
# validations.
|
|
6012
|
+
delay = self._ValidateDelay(delay, 0.50, 10)
|
|
6013
|
+
if (not isinstance(verifyUserContext, bool)):
|
|
6014
|
+
verifyUserContext = False
|
|
6015
|
+
|
|
6016
|
+
deviceActiveUser:str = None
|
|
6017
|
+
deviceIdResult:str = None
|
|
6018
|
+
|
|
6019
|
+
# discover Spotify Connect devices on the network, waiting up to the specified timeout
|
|
6020
|
+
# for all devices to be discovered. this will return all Spotify Connect devices that
|
|
6021
|
+
# are currently powered on, and are registered to Zeroconf / mDNS.
|
|
6022
|
+
_logsi.LogVerbose("Discovering Spotify Connect devices on the local network")
|
|
6023
|
+
discovery:SpotifyDiscovery = SpotifyDiscovery(self._ZeroconfClient, printToConsole=False)
|
|
6024
|
+
discovery.DiscoverDevices(timeout=self._SpotifyConnectDiscoveryTimeout)
|
|
6025
|
+
|
|
6026
|
+
# process all discovered devices.
|
|
6027
|
+
discoverResult:ZeroconfDiscoveryResult
|
|
6028
|
+
for discoverResult in discovery.DiscoveryResults:
|
|
6029
|
+
|
|
6030
|
+
# get the id from the device via the zeroconf API getInfo endpoint, as the id is not
|
|
6031
|
+
# returned in the zeroconf discovery result.
|
|
6032
|
+
zconn:ZeroconfConnect = ZeroconfConnect(discoverResult.HostIpv4Address,
|
|
6033
|
+
discoverResult.HostIpPort,
|
|
6034
|
+
discoverResult.SpotifyConnectCPath,
|
|
6035
|
+
useSSL=False)
|
|
6036
|
+
info:ZeroconfGetInfo = zconn.GetInformation()
|
|
6037
|
+
|
|
6038
|
+
# store the currently active user of the device, in case we need to switch users later on.
|
|
6039
|
+
deviceActiveUser = info.ActiveUser.lower()
|
|
6040
|
+
deviceIdResult = info.DeviceId
|
|
6041
|
+
|
|
6042
|
+
status:str = "INACTIVE"
|
|
6043
|
+
if (info.HasActiveUser):
|
|
6044
|
+
status = "ACTIVE, "
|
|
6045
|
+
|
|
6046
|
+
# did we resolve the device id?
|
|
6047
|
+
if deviceIdResult is not None:
|
|
6048
|
+
|
|
6049
|
+
# does our Spotify Connect user account need to take control of the device?
|
|
6050
|
+
# if not, then we are done.
|
|
6051
|
+
if (deviceActiveUser == self._SpotifyConnectUsername.lower()): # or (deviceActiveUser == self.UserProfile.Id.lower()):
|
|
6052
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' was verified for Device id '%s'; switch not necessary" % (deviceActiveUser, deviceIdResult))
|
|
6053
|
+
status = status + "user context switch not needed"
|
|
6054
|
+
|
|
6055
|
+
# did caller request user context switch bypass?
|
|
6056
|
+
# if so, then just return the device id.
|
|
6057
|
+
elif (info.HasActiveUser) and (not verifyUserContext):
|
|
6058
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' will be used for Device id '%s', as caller chose to bypass user context switch" % (deviceActiveUser, deviceIdResult))
|
|
6059
|
+
status = status + "caller bypassed user context switch"
|
|
6060
|
+
|
|
6061
|
+
else:
|
|
6062
|
+
zcfResult:ZeroconfResponse
|
|
6063
|
+
|
|
6064
|
+
# if a different user context has control of the device then we need to disconnect
|
|
6065
|
+
# the current user context before connecting a different user.
|
|
6066
|
+
if (info.HasActiveUser):
|
|
6067
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' is being disconnected for Device id '%s'" % (deviceActiveUser, deviceIdResult))
|
|
6068
|
+
zcfResult = zconn.Disconnect()
|
|
6069
|
+
status = status + 'disconnected from user context "%s";' % deviceActiveUser
|
|
6070
|
+
|
|
6071
|
+
# connect the device to OUR Spotify Connect user context.
|
|
6072
|
+
# note that the result here only indicates that the connect was submitted - NOT that it was successful!
|
|
6073
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' is being connected for Device id '%s'" % (self._SpotifyConnectUsername, deviceIdResult))
|
|
6074
|
+
zcfResult = zconn.Connect(self._SpotifyConnectUsername, self._SpotifyConnectPassword)
|
|
6075
|
+
status = status + 'connected to user context "%s"' % self._SpotifyConnectUsername
|
|
6076
|
+
|
|
6077
|
+
# trace.
|
|
6078
|
+
_logsi.LogVerbose("Spotify Connect user context switch from '%s' to '%s' was requested for Device id '%s'" % (deviceActiveUser, self._SpotifyConnectUsername, deviceIdResult))
|
|
6079
|
+
|
|
6080
|
+
# give spotify web api time to process the change and add the device to the device list.
|
|
6081
|
+
if delay > 0:
|
|
6082
|
+
_logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % delay)
|
|
6083
|
+
time.sleep(delay)
|
|
6084
|
+
|
|
6085
|
+
# append device status to results.
|
|
6086
|
+
result = "%s%s\n" % (result, MSG_RESULT % (info.RemoteName, info.DeviceId, info.ActiveUser, status))
|
|
6087
|
+
|
|
6088
|
+
# return results.
|
|
6089
|
+
_logsi.LogText(SILevel.Verbose, "Spotify Connect Player Device activation results", result)
|
|
6090
|
+
return result
|
|
6091
|
+
|
|
6092
|
+
except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
|
|
6093
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
6094
|
+
except Exception as ex:
|
|
6095
|
+
|
|
6096
|
+
# format unhandled exception.
|
|
6097
|
+
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
|
|
6098
|
+
|
|
6099
|
+
finally:
|
|
6100
|
+
|
|
6101
|
+
# trace.
|
|
6102
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
6103
|
+
|
|
6104
|
+
|
|
5878
6105
|
def PlayerConvertDeviceNameToId(self,
|
|
5879
6106
|
value:str,
|
|
5880
6107
|
refresh:bool=False
|
|
@@ -5944,12 +6171,12 @@ class SpotifyClient:
|
|
|
5944
6171
|
|
|
5945
6172
|
try:
|
|
5946
6173
|
|
|
5947
|
-
# is this a hex string value?
|
|
6174
|
+
# is this a hex string value (e.g. device id)?
|
|
5948
6175
|
intValue:int = int(str(value), 16)
|
|
5949
6176
|
|
|
5950
6177
|
# if it IS a hex string, then it should have been converted to a number.
|
|
5951
6178
|
# at this point we can assume it's a device id. in this case, just return it.
|
|
5952
|
-
# note that we will NOT perform a lookup in the device list
|
|
6179
|
+
# note that we will NOT perform a lookup in the device list .
|
|
5953
6180
|
_logsi.LogVerbose("Device id '%s' was specified, and will be used as-is" % value)
|
|
5954
6181
|
if intValue is not None:
|
|
5955
6182
|
return value
|
|
@@ -9012,6 +9239,35 @@ class SpotifyClient:
|
|
|
9012
9239
|
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
9013
9240
|
|
|
9014
9241
|
|
|
9242
|
+
@staticmethod
|
|
9243
|
+
def IsDeviceId(value:str) -> bool:
|
|
9244
|
+
"""
|
|
9245
|
+
Get the assumed device value format.
|
|
9246
|
+
|
|
9247
|
+
Args:
|
|
9248
|
+
value (str):
|
|
9249
|
+
The id or name of a Spotify Connect device.
|
|
9250
|
+
Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
|
|
9251
|
+
Example: `Web Player (Chrome)`
|
|
9252
|
+
|
|
9253
|
+
Returns:
|
|
9254
|
+
True if the specified value is a device id format;
|
|
9255
|
+
otherwise, False to indicate it is a device name format.
|
|
9256
|
+
|
|
9257
|
+
No exceptions are raised with this method.
|
|
9258
|
+
"""
|
|
9259
|
+
result:bool = False
|
|
9260
|
+
try:
|
|
9261
|
+
# if it is a hex string, then it is assumed to be a device id.
|
|
9262
|
+
intValue:int = int(str(value), 16)
|
|
9263
|
+
result = True
|
|
9264
|
+
except Exception:
|
|
9265
|
+
# otherwise, assume it's a device name.
|
|
9266
|
+
result = False
|
|
9267
|
+
|
|
9268
|
+
return result
|
|
9269
|
+
|
|
9270
|
+
|
|
9015
9271
|
def PlayerMediaPause(self,
|
|
9016
9272
|
deviceId:str=None,
|
|
9017
9273
|
delay:float=0.50
|
|
@@ -9199,7 +9455,11 @@ class SpotifyClient:
|
|
|
9199
9455
|
delay = self._ValidateDelay(delay, 0.50, 10)
|
|
9200
9456
|
|
|
9201
9457
|
# check for device name; convert to an id if a name was supplied.
|
|
9202
|
-
deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
9458
|
+
#deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
9459
|
+
|
|
9460
|
+
# ensure the specified device id / name is active and available, and
|
|
9461
|
+
# the user context is set correctly.
|
|
9462
|
+
deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
|
|
9203
9463
|
|
|
9204
9464
|
# build spotify web api request parameters.
|
|
9205
9465
|
reqData:dict = \
|
|
@@ -9315,7 +9575,11 @@ class SpotifyClient:
|
|
|
9315
9575
|
arrUris.append(trackSaved.Track.Uri)
|
|
9316
9576
|
|
|
9317
9577
|
# check for device name; convert to an id if a name was supplied.
|
|
9318
|
-
deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
9578
|
+
#deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
9579
|
+
|
|
9580
|
+
# ensure the specified device id / name is active and available, and
|
|
9581
|
+
# the user context is set correctly.
|
|
9582
|
+
deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
|
|
9319
9583
|
|
|
9320
9584
|
# set desired shuffle mode.
|
|
9321
9585
|
self.PlayerSetShuffleMode(shuffle, deviceId, delay)
|
|
@@ -9420,7 +9684,11 @@ class SpotifyClient:
|
|
|
9420
9684
|
arrUris[idx] = arrUris[idx].strip()
|
|
9421
9685
|
|
|
9422
9686
|
# check for device name; convert to an id if a name was supplied.
|
|
9423
|
-
deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
9687
|
+
#deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
9688
|
+
|
|
9689
|
+
# ensure the specified device id / name is active and available, and
|
|
9690
|
+
# the user context is set correctly.
|
|
9691
|
+
deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
|
|
9424
9692
|
|
|
9425
9693
|
# build spotify web api request parameters.
|
|
9426
9694
|
reqData:dict = \
|
|
@@ -9830,6 +10098,231 @@ class SpotifyClient:
|
|
|
9830
10098
|
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
9831
10099
|
|
|
9832
10100
|
|
|
10101
|
+
def PlayerResolveDeviceId(
|
|
10102
|
+
self,
|
|
10103
|
+
deviceValue:str,
|
|
10104
|
+
verifyUserContext:bool=True,
|
|
10105
|
+
verifyTimeout:float=5.0,
|
|
10106
|
+
) -> str:
|
|
10107
|
+
"""
|
|
10108
|
+
Resolves a Spotify Connect device identifier from a specified device id, name, alias id,
|
|
10109
|
+
or alias name. This will ensure that the device id can be found on the network, as well
|
|
10110
|
+
as connect to the device if necessary with the current user context.
|
|
10111
|
+
|
|
10112
|
+
Args:
|
|
10113
|
+
deviceValue (str):
|
|
10114
|
+
The device id / name value to check.
|
|
10115
|
+
verifyUserContext (bool):
|
|
10116
|
+
If True, the active user context of the resolved device is checked to ensure it
|
|
10117
|
+
matches the user context specified on the class constructor.
|
|
10118
|
+
If False, the user context will not be checked.
|
|
10119
|
+
Default is True.
|
|
10120
|
+
verifyTimeout (float):
|
|
10121
|
+
Maximum time to wait (in seconds) for the device to become active in the Spotify
|
|
10122
|
+
Connect device list. This value is only used if a Connect command has to be
|
|
10123
|
+
issued to activate the device.
|
|
10124
|
+
Default is 5; value range is 0 - 10.
|
|
10125
|
+
|
|
10126
|
+
Returns:
|
|
10127
|
+
A device Id for the deviceValue if one could be resolved;
|
|
10128
|
+
otherwise, just the original deviceValue with the understanding that
|
|
10129
|
+
subsequent operations will probably fail since it's not in the device list.
|
|
10130
|
+
|
|
10131
|
+
Raises:
|
|
10132
|
+
SpotifyWebApiError:
|
|
10133
|
+
If the Spotify Web API request was for a non-authorization service
|
|
10134
|
+
and the response contains error information.
|
|
10135
|
+
SpotifyZeroconfApiError:
|
|
10136
|
+
If any Spotify Zeroconf API request failed.
|
|
10137
|
+
SpotifApiError:
|
|
10138
|
+
If the method fails for any other reason.
|
|
10139
|
+
|
|
10140
|
+
A Zeroconf discovery process is initiated to search for all Spotify Connect devices connected
|
|
10141
|
+
to the local network. A Spotify Zeroconf API `getInfo` call is then issued for each device
|
|
10142
|
+
that was found, searching for a match on the `deviceValue` argument. Depending on the format
|
|
10143
|
+
of the `deviceValue`, the search will match on either a Device ID or RemoteName as well as any
|
|
10144
|
+
alias ID's or Names that are in use (for multi-room configurations).
|
|
10145
|
+
|
|
10146
|
+
The device ID value is returned if a match is found; otherwise, null is returned.
|
|
10147
|
+
|
|
10148
|
+
A user context switch will also be performed if a device id is resolved AND the active user of the
|
|
10149
|
+
device does not match the `SpotifyConnectUsername` argument specified on the class constructor.
|
|
10150
|
+
The user context switch is bypassed if the `verifyUserContext` argument is False, or if the
|
|
10151
|
+
`SpotifyConnectUsername` was not supplied on the class constructor. If the user context is to be
|
|
10152
|
+
switched, then a Disconnect will be issued if a user context is active on the device followed by
|
|
10153
|
+
a Connect to the user context specified on the class constructor.
|
|
10154
|
+
|
|
10155
|
+
<details>
|
|
10156
|
+
<summary>Sample Code</summary>
|
|
10157
|
+
```python
|
|
10158
|
+
.. include:: ../docs/include/samplecode/SpotifyClient/PlayerResolveDeviceId.py
|
|
10159
|
+
```
|
|
10160
|
+
</details>
|
|
10161
|
+
"""
|
|
10162
|
+
apiMethodName:str = 'PlayerResolveDeviceId'
|
|
10163
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
10164
|
+
deviceIdResult:str = None
|
|
10165
|
+
|
|
10166
|
+
try:
|
|
10167
|
+
|
|
10168
|
+
# trace.
|
|
10169
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
10170
|
+
apiMethodParms.AppendKeyValue("deviceValue", deviceValue)
|
|
10171
|
+
apiMethodParms.AppendKeyValue("verifyUserContext", verifyUserContext)
|
|
10172
|
+
apiMethodParms.AppendKeyValue("verifyTimeout", verifyTimeout)
|
|
10173
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Resolving Spotify Connect Player Device Id", apiMethodParms)
|
|
10174
|
+
|
|
10175
|
+
# if device value not specified then we are done.
|
|
10176
|
+
if (deviceValue is None) or (len(deviceValue.strip()) == 0):
|
|
10177
|
+
_logsi.LogVerbose("deviceValue not specified; nothing to do")
|
|
10178
|
+
return None
|
|
10179
|
+
|
|
10180
|
+
# validations.
|
|
10181
|
+
verifyTimeout = self._ValidateDelay(verifyTimeout, 5, 10)
|
|
10182
|
+
if (not isinstance(verifyUserContext, bool)):
|
|
10183
|
+
verifyUserContext = True
|
|
10184
|
+
|
|
10185
|
+
# determine if the device value specified is a name (e.g. "Bose-ST10-1") or
|
|
10186
|
+
# an id (e.g. "30fbc80e35598f3c242f2120413c943dfd9715fe").
|
|
10187
|
+
isDeviceId:bool = SpotifyClient.IsDeviceId(deviceValue)
|
|
10188
|
+
deviceValueCompare:str = deviceValue.lower()
|
|
10189
|
+
deviceActiveUser:str = None
|
|
10190
|
+
|
|
10191
|
+
# discover Spotify Connect devices on the network, waiting up to the specified timeout
|
|
10192
|
+
# for all devices to be discovered. this will return all Spotify Connect devices that
|
|
10193
|
+
# are currently powered on, and are registered to Zeroconf / mDNS.
|
|
10194
|
+
_logsi.LogVerbose("Discovering Spotify Connect devices on the local network")
|
|
10195
|
+
discovery:SpotifyDiscovery = SpotifyDiscovery(self._ZeroconfClient, printToConsole=False)
|
|
10196
|
+
discovery.DiscoverDevices(timeout=self._SpotifyConnectDiscoveryTimeout)
|
|
10197
|
+
|
|
10198
|
+
# process all discovered devices.
|
|
10199
|
+
discoverResult:ZeroconfDiscoveryResult
|
|
10200
|
+
for discoverResult in discovery.DiscoveryResults:
|
|
10201
|
+
|
|
10202
|
+
# get the id from the device via the zeroconf API getInfo endpoint, as the id is not
|
|
10203
|
+
# returned in the zeroconf discovery result.
|
|
10204
|
+
zconn:ZeroconfConnect = ZeroconfConnect(discoverResult.HostIpv4Address,
|
|
10205
|
+
discoverResult.HostIpPort,
|
|
10206
|
+
discoverResult.SpotifyConnectCPath,
|
|
10207
|
+
useSSL=False)
|
|
10208
|
+
info:ZeroconfGetInfo = zconn.GetInformation()
|
|
10209
|
+
|
|
10210
|
+
# store the currently active user of the device, in case we need to switch users later on.
|
|
10211
|
+
deviceActiveUser = info.ActiveUser.lower()
|
|
10212
|
+
|
|
10213
|
+
# are aliases being used (RemoteName is null if so)?
|
|
10214
|
+
if info.RemoteName is None:
|
|
10215
|
+
|
|
10216
|
+
# if aliases are defined for the device, then we have to check each alias for a match.
|
|
10217
|
+
infoAlias:ZeroconfGetInfoAlias
|
|
10218
|
+
for infoAlias in info.Aliases:
|
|
10219
|
+
|
|
10220
|
+
# if input deviceValue is a device id then compare the id; otherwise, compare the name.
|
|
10221
|
+
if isDeviceId:
|
|
10222
|
+
if (infoAlias.Id.lower() == deviceValueCompare):
|
|
10223
|
+
_logsi.LogVerbose("Device value '%s' was matched to device Alias Id '%s' (%s)" % (deviceValue, infoAlias.Name, infoAlias.Id))
|
|
10224
|
+
deviceIdResult = infoAlias.Id
|
|
10225
|
+
break # alias processing loop
|
|
10226
|
+
else:
|
|
10227
|
+
if (infoAlias.Name.lower() == deviceValueCompare):
|
|
10228
|
+
_logsi.LogVerbose("Device value '%s' was matched to device Alias Name '%s' (%s)" % (deviceValue, infoAlias.Name, infoAlias.Id))
|
|
10229
|
+
deviceIdResult = infoAlias.Id
|
|
10230
|
+
break # alias processing loop
|
|
10231
|
+
|
|
10232
|
+
else:
|
|
10233
|
+
|
|
10234
|
+
# if input deviceValue is a device id then compare the id; otherwise, compare the name.
|
|
10235
|
+
if isDeviceId:
|
|
10236
|
+
if (info.DeviceId.lower() == deviceValueCompare):
|
|
10237
|
+
_logsi.LogVerbose("Device value '%s' was matched to device Id '%s' (%s)" % (deviceValue, info.RemoteName, info.DeviceId))
|
|
10238
|
+
deviceIdResult = info.DeviceId
|
|
10239
|
+
else:
|
|
10240
|
+
if (info.RemoteName.lower() == deviceValueCompare):
|
|
10241
|
+
_logsi.LogVerbose("Device value '%s' was matched to device RemoteName '%s' (%s)" % (deviceValue, info.RemoteName, info.DeviceId))
|
|
10242
|
+
deviceIdResult = info.DeviceId
|
|
10243
|
+
|
|
10244
|
+
# did we resolve the device id?
|
|
10245
|
+
if deviceIdResult is not None:
|
|
10246
|
+
|
|
10247
|
+
# did caller request user context switch bypass?
|
|
10248
|
+
# if so, then just return the device id.
|
|
10249
|
+
if (not verifyUserContext):
|
|
10250
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' will be used for Device id '%s', as user chose to bypass user context switch" % (deviceActiveUser, deviceIdResult))
|
|
10251
|
+
break
|
|
10252
|
+
|
|
10253
|
+
# was a Spotify Connect user account specified on the class constructor?
|
|
10254
|
+
# if not, then do not switch the user context.
|
|
10255
|
+
if (self._SpotifyConnectUsername is None):
|
|
10256
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' will be used for Device id '%s', as a SpotifyConnectUsername parameter was not supplied" % (deviceActiveUser, deviceIdResult))
|
|
10257
|
+
break
|
|
10258
|
+
|
|
10259
|
+
# does our Spotify Connect user account need to take control of the device?
|
|
10260
|
+
# if not, then we are done.
|
|
10261
|
+
if (deviceActiveUser == self._SpotifyConnectUsername.lower()) or (deviceActiveUser == self.UserProfile.Id.lower()):
|
|
10262
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' was verified for Device id '%s'; switch not necessary" % (deviceActiveUser, deviceIdResult))
|
|
10263
|
+
break
|
|
10264
|
+
|
|
10265
|
+
zcfResult:ZeroconfResponse
|
|
10266
|
+
|
|
10267
|
+
# if a different user context has control of the device then we need to disconnect
|
|
10268
|
+
# the current user context before connecting a different user.
|
|
10269
|
+
if (info.HasActiveUser):
|
|
10270
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' is being disconnected for Device id '%s'" % (deviceActiveUser, deviceIdResult))
|
|
10271
|
+
zcfResult = zconn.Disconnect()
|
|
10272
|
+
|
|
10273
|
+
# connect the device to OUR Spotify Connect user context.
|
|
10274
|
+
# note that the result here only indicates that the connect was submitted - NOT that it was successful!
|
|
10275
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' is being connected for Device id '%s'" % (self._SpotifyConnectUsername, deviceIdResult))
|
|
10276
|
+
zcfResult = zconn.Connect(self._SpotifyConnectUsername, self._SpotifyConnectPassword)
|
|
10277
|
+
|
|
10278
|
+
# trace.
|
|
10279
|
+
_logsi.LogVerbose("Spotify Connect user context switch from '%s' to '%s' was requested for Device id '%s'" % (deviceActiveUser, self._SpotifyConnectUsername, deviceIdResult))
|
|
10280
|
+
|
|
10281
|
+
loopTotalDelay:float = 0
|
|
10282
|
+
LOOP_DELAY:float = 0.25
|
|
10283
|
+
while True:
|
|
10284
|
+
|
|
10285
|
+
# wait just a bit between active device list queries.
|
|
10286
|
+
_logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % LOOP_DELAY)
|
|
10287
|
+
time.sleep(LOOP_DELAY)
|
|
10288
|
+
loopTotalDelay = loopTotalDelay + LOOP_DELAY
|
|
10289
|
+
|
|
10290
|
+
# check for the device in the active device list.
|
|
10291
|
+
# if we find it, then we are done.
|
|
10292
|
+
device:Device = self.GetPlayerDevice(deviceIdResult, True)
|
|
10293
|
+
if device is not None:
|
|
10294
|
+
_logsi.LogVerbose("Spotify Connect Device id '%s' is now in the active device list; device found within %f seconds of Connect" % (deviceIdResult, loopTotalDelay))
|
|
10295
|
+
break
|
|
10296
|
+
|
|
10297
|
+
# only check so many times before we give up;
|
|
10298
|
+
# we will keep checking until we find the device, or we exceed the verifyTimeout value.
|
|
10299
|
+
if (loopTotalDelay > verifyTimeout):
|
|
10300
|
+
_logsi.LogWarning("Verification timeout waiting for Spotify Connect Device id '%s' to be added to the active device list; device not found within %f seconds of Connect" % (deviceIdResult, verifyTimeout))
|
|
10301
|
+
break
|
|
10302
|
+
|
|
10303
|
+
# end the device processing loop.
|
|
10304
|
+
break
|
|
10305
|
+
|
|
10306
|
+
# did we resolve the device id?
|
|
10307
|
+
if deviceIdResult is None:
|
|
10308
|
+
_logsi.LogWarning("Device value '%s' could not be resolved to an active Spotify Connect device, which denotes subsequent operations for the device will probably fail; please ensure that the specified device is available on the network" % (deviceValue))
|
|
10309
|
+
|
|
10310
|
+
# return the resolved device id.
|
|
10311
|
+
return deviceIdResult
|
|
10312
|
+
|
|
10313
|
+
except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
|
|
10314
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
10315
|
+
except Exception as ex:
|
|
10316
|
+
|
|
10317
|
+
# format unhandled exception.
|
|
10318
|
+
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
|
|
10319
|
+
|
|
10320
|
+
finally:
|
|
10321
|
+
|
|
10322
|
+
# trace.
|
|
10323
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
10324
|
+
|
|
10325
|
+
|
|
9833
10326
|
def PlayerSetRepeatMode(self,
|
|
9834
10327
|
state:str='off',
|
|
9835
10328
|
deviceId:str=None,
|
|
@@ -10187,7 +10680,11 @@ class SpotifyClient:
|
|
|
10187
10680
|
delay = self._ValidateDelay(delay, 0.50, 10)
|
|
10188
10681
|
|
|
10189
10682
|
# check for device name; convert to an id if a name was supplied.
|
|
10190
|
-
deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
10683
|
+
#deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
10684
|
+
|
|
10685
|
+
# ensure the specified device id / name is active and available, and
|
|
10686
|
+
# the user context is set correctly.
|
|
10687
|
+
deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
|
|
10191
10688
|
|
|
10192
10689
|
# build spotify web api request parameters.
|
|
10193
10690
|
reqData:dict = \
|