spotifywebapiPython 1.0.47__py3-none-any.whl → 1.0.49__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.47.dist-info → spotifywebapiPython-1.0.49.dist-info}/METADATA +1 -1
- {spotifywebapiPython-1.0.47.dist-info → spotifywebapiPython-1.0.49.dist-info}/RECORD +24 -24
- spotifywebapipython/const.py +1 -1
- spotifywebapipython/docs/__init__.html +19 -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 +303 -3
- spotifywebapipython/docs/spotifywebapiPython/spotifydiscovery.html +15 -72
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapiauthenticationerror.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapierror.html +2 -2
- spotifywebapipython/models/userprofile.py +24 -0
- spotifywebapipython/spotifyclient.py +497 -8
- spotifywebapipython/spotifydiscovery.py +27 -83
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +10 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +21 -3
- {spotifywebapiPython-1.0.47.dist-info → spotifywebapiPython-1.0.49.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.47.dist-info → spotifywebapiPython-1.0.49.dist-info}/WHEEL +0 -0
- {spotifywebapiPython-1.0.47.dist-info → spotifywebapiPython-1.0.49.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._Zeroconf = 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._Zeroconf = 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':
|
|
@@ -5875,6 +5934,166 @@ class SpotifyClient:
|
|
|
5875
5934
|
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
5876
5935
|
|
|
5877
5936
|
|
|
5937
|
+
def PlayerActivateDevices(
|
|
5938
|
+
self,
|
|
5939
|
+
verifyUserContext:bool=False,
|
|
5940
|
+
delay:float=0.50,
|
|
5941
|
+
) -> None:
|
|
5942
|
+
"""
|
|
5943
|
+
Activates all Spotify Connect player devices, and (optionally) switches the active user
|
|
5944
|
+
context to the current user context.
|
|
5945
|
+
|
|
5946
|
+
Args:
|
|
5947
|
+
verifyUserContext (bool):
|
|
5948
|
+
If True, the active user context of the resolved device is checked to ensure it
|
|
5949
|
+
matches the user context specified on the class constructor.
|
|
5950
|
+
If False, the user context will not be checked.
|
|
5951
|
+
Default is False.
|
|
5952
|
+
delay (float):
|
|
5953
|
+
Time delay (in seconds) to wait AFTER issuing the final Connect command (if necessary).
|
|
5954
|
+
This delay will give the spotify web api time to process the device list change before
|
|
5955
|
+
another command is issued.
|
|
5956
|
+
Default is 0.50; value range is 0 - 10.
|
|
5957
|
+
|
|
5958
|
+
Raises:
|
|
5959
|
+
SpotifyWebApiError:
|
|
5960
|
+
If the Spotify Web API request was for a non-authorization service
|
|
5961
|
+
and the response contains error information.
|
|
5962
|
+
SpotifyZeroconfApiError:
|
|
5963
|
+
If any Spotify Zeroconf API request failed.
|
|
5964
|
+
If the `spotifyConnectUsername` aregument was not specified for the class contructor.
|
|
5965
|
+
SpotifApiError:
|
|
5966
|
+
If the method fails for any other reason.
|
|
5967
|
+
|
|
5968
|
+
A Zeroconf discovery process is initiated to search for all Spotify Connect devices connected
|
|
5969
|
+
to the local network. A Spotify Zeroconf API `getInfo` call is then issued for each device
|
|
5970
|
+
that was found to retrieve device information. A Spotify Zeroconf API `addUser` call is also
|
|
5971
|
+
issued for each device that does not have an active user context established, OR if the currently
|
|
5972
|
+
active user context does not match the `SpotifyConnectUsername` argument specified on the class constructor.
|
|
5973
|
+
|
|
5974
|
+
The user context switch is bypassed if the `verifyUserContext` argument is False. If the user context
|
|
5975
|
+
is to be switched, then a Disconnect will be issued if a user context is active on the device followed
|
|
5976
|
+
by a Connect to the user context specified on the class constructor.
|
|
5977
|
+
|
|
5978
|
+
<details>
|
|
5979
|
+
<summary>Sample Code</summary>
|
|
5980
|
+
```python
|
|
5981
|
+
.. include:: ../docs/include/samplecode/SpotifyClient/PlayerActivateDevices.py
|
|
5982
|
+
```
|
|
5983
|
+
</details>
|
|
5984
|
+
"""
|
|
5985
|
+
apiMethodName:str = 'PlayerActivateDevices'
|
|
5986
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
5987
|
+
result:str = ""
|
|
5988
|
+
|
|
5989
|
+
MSG_RESULT:str = '- Device Name="%s", ID="%s", ActiveUser="%s", Status=%s'
|
|
5990
|
+
|
|
5991
|
+
try:
|
|
5992
|
+
|
|
5993
|
+
# trace.
|
|
5994
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
5995
|
+
apiMethodParms.AppendKeyValue("verifyUserContext", verifyUserContext)
|
|
5996
|
+
apiMethodParms.AppendKeyValue("delay", delay)
|
|
5997
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Activating Spotify Connect Player Devices", apiMethodParms)
|
|
5998
|
+
|
|
5999
|
+
# validation - ensure spotify connect user context was specified on the class constructor.
|
|
6000
|
+
if (self._SpotifyConnectUsername is None):
|
|
6001
|
+
raise SpotifyZeroconfApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % (apiMethodName, 'SpotifyConnectUsername'), logsi=_logsi)
|
|
6002
|
+
|
|
6003
|
+
# validations.
|
|
6004
|
+
delay = self._ValidateDelay(delay, 0.50, 10)
|
|
6005
|
+
if (not isinstance(verifyUserContext, bool)):
|
|
6006
|
+
verifyUserContext = False
|
|
6007
|
+
|
|
6008
|
+
deviceActiveUser:str = None
|
|
6009
|
+
deviceIdResult:str = None
|
|
6010
|
+
|
|
6011
|
+
# discover Spotify Connect devices on the network, waiting up to the specified timeout
|
|
6012
|
+
# for all devices to be discovered. this will return all Spotify Connect devices that
|
|
6013
|
+
# are currently powered on, and are registered to Zeroconf / mDNS.
|
|
6014
|
+
_logsi.LogVerbose("Discovering Spotify Connect devices on the local network")
|
|
6015
|
+
discovery:SpotifyDiscovery = SpotifyDiscovery(self._Zeroconf, printToConsole=False)
|
|
6016
|
+
discovery.DiscoverDevices(timeout=self._SpotifyConnectDiscoveryTimeout)
|
|
6017
|
+
|
|
6018
|
+
# process all discovered devices.
|
|
6019
|
+
discoverResult:ZeroconfDiscoveryResult
|
|
6020
|
+
for discoverResult in discovery.DiscoveryResults:
|
|
6021
|
+
|
|
6022
|
+
# get the id from the device via the zeroconf API getInfo endpoint, as the id is not
|
|
6023
|
+
# returned in the zeroconf discovery result.
|
|
6024
|
+
zconn:ZeroconfConnect = ZeroconfConnect(discoverResult.HostIpv4Address,
|
|
6025
|
+
discoverResult.HostIpPort,
|
|
6026
|
+
discoverResult.SpotifyConnectCPath,
|
|
6027
|
+
useSSL=False)
|
|
6028
|
+
info:ZeroconfGetInfo = zconn.GetInformation()
|
|
6029
|
+
|
|
6030
|
+
# store the currently active user of the device, in case we need to switch users later on.
|
|
6031
|
+
deviceActiveUser = info.ActiveUser.lower()
|
|
6032
|
+
deviceIdResult = info.DeviceId
|
|
6033
|
+
|
|
6034
|
+
status:str = "INACTIVE"
|
|
6035
|
+
if (info.HasActiveUser):
|
|
6036
|
+
status = "ACTIVE, "
|
|
6037
|
+
|
|
6038
|
+
# did we resolve the device id?
|
|
6039
|
+
if deviceIdResult is not None:
|
|
6040
|
+
|
|
6041
|
+
# does our Spotify Connect user account need to take control of the device?
|
|
6042
|
+
# if not, then we are done.
|
|
6043
|
+
if (deviceActiveUser == self._SpotifyConnectUsername.lower()): # or (deviceActiveUser == self.UserProfile.Id.lower()):
|
|
6044
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' was verified for Device id '%s'; switch not necessary" % (deviceActiveUser, deviceIdResult))
|
|
6045
|
+
status = status + "user context switch not needed"
|
|
6046
|
+
|
|
6047
|
+
# did caller request user context switch bypass?
|
|
6048
|
+
# if so, then just return the device id.
|
|
6049
|
+
elif (info.HasActiveUser) and (not verifyUserContext):
|
|
6050
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' will be used for Device id '%s', as caller chose to bypass user context switch" % (deviceActiveUser, deviceIdResult))
|
|
6051
|
+
status = status + "caller bypassed user context switch"
|
|
6052
|
+
|
|
6053
|
+
else:
|
|
6054
|
+
zcfResult:ZeroconfResponse
|
|
6055
|
+
|
|
6056
|
+
# if a different user context has control of the device then we need to disconnect
|
|
6057
|
+
# the current user context before connecting a different user.
|
|
6058
|
+
if (info.HasActiveUser):
|
|
6059
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' is being disconnected for Device id '%s'" % (deviceActiveUser, deviceIdResult))
|
|
6060
|
+
zcfResult = zconn.Disconnect()
|
|
6061
|
+
status = status + 'disconnected from user context "%s";' % deviceActiveUser
|
|
6062
|
+
|
|
6063
|
+
# connect the device to OUR Spotify Connect user context.
|
|
6064
|
+
# note that the result here only indicates that the connect was submitted - NOT that it was successful!
|
|
6065
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' is being connected for Device id '%s'" % (self._SpotifyConnectUsername, deviceIdResult))
|
|
6066
|
+
zcfResult = zconn.Connect(self._SpotifyConnectUsername, self._SpotifyConnectPassword)
|
|
6067
|
+
status = status + 'connected to user context "%s"' % self._SpotifyConnectUsername
|
|
6068
|
+
|
|
6069
|
+
# trace.
|
|
6070
|
+
_logsi.LogVerbose("Spotify Connect user context switch from '%s' to '%s' was requested for Device id '%s'" % (deviceActiveUser, self._SpotifyConnectUsername, deviceIdResult))
|
|
6071
|
+
|
|
6072
|
+
# give spotify web api time to process the change and add the device to the device list.
|
|
6073
|
+
if delay > 0:
|
|
6074
|
+
_logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % delay)
|
|
6075
|
+
time.sleep(delay)
|
|
6076
|
+
|
|
6077
|
+
# append device status to results.
|
|
6078
|
+
result = "%s%s\n" % (result, MSG_RESULT % (info.RemoteName, info.DeviceId, info.ActiveUser, status))
|
|
6079
|
+
|
|
6080
|
+
# return results.
|
|
6081
|
+
_logsi.LogText(SILevel.Verbose, "Spotify Connect Player Device activation results", result)
|
|
6082
|
+
return result
|
|
6083
|
+
|
|
6084
|
+
except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
|
|
6085
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
6086
|
+
except Exception as ex:
|
|
6087
|
+
|
|
6088
|
+
# format unhandled exception.
|
|
6089
|
+
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
|
|
6090
|
+
|
|
6091
|
+
finally:
|
|
6092
|
+
|
|
6093
|
+
# trace.
|
|
6094
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
6095
|
+
|
|
6096
|
+
|
|
5878
6097
|
def PlayerConvertDeviceNameToId(self,
|
|
5879
6098
|
value:str,
|
|
5880
6099
|
refresh:bool=False
|
|
@@ -5944,12 +6163,12 @@ class SpotifyClient:
|
|
|
5944
6163
|
|
|
5945
6164
|
try:
|
|
5946
6165
|
|
|
5947
|
-
# is this a hex string value?
|
|
6166
|
+
# is this a hex string value (e.g. device id)?
|
|
5948
6167
|
intValue:int = int(str(value), 16)
|
|
5949
6168
|
|
|
5950
6169
|
# if it IS a hex string, then it should have been converted to a number.
|
|
5951
6170
|
# 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
|
|
6171
|
+
# note that we will NOT perform a lookup in the device list .
|
|
5953
6172
|
_logsi.LogVerbose("Device id '%s' was specified, and will be used as-is" % value)
|
|
5954
6173
|
if intValue is not None:
|
|
5955
6174
|
return value
|
|
@@ -9012,6 +9231,35 @@ class SpotifyClient:
|
|
|
9012
9231
|
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
9013
9232
|
|
|
9014
9233
|
|
|
9234
|
+
@staticmethod
|
|
9235
|
+
def IsDeviceId(value:str) -> bool:
|
|
9236
|
+
"""
|
|
9237
|
+
Get the assumed device value format.
|
|
9238
|
+
|
|
9239
|
+
Args:
|
|
9240
|
+
value (str):
|
|
9241
|
+
The id or name of a Spotify Connect device.
|
|
9242
|
+
Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
|
|
9243
|
+
Example: `Web Player (Chrome)`
|
|
9244
|
+
|
|
9245
|
+
Returns:
|
|
9246
|
+
True if the specified value is a device id format;
|
|
9247
|
+
otherwise, False to indicate it is a device name format.
|
|
9248
|
+
|
|
9249
|
+
No exceptions are raised with this method.
|
|
9250
|
+
"""
|
|
9251
|
+
result:bool = False
|
|
9252
|
+
try:
|
|
9253
|
+
# if it is a hex string, then it is assumed to be a device id.
|
|
9254
|
+
intValue:int = int(str(value), 16)
|
|
9255
|
+
result = True
|
|
9256
|
+
except Exception:
|
|
9257
|
+
# otherwise, assume it's a device name.
|
|
9258
|
+
result = False
|
|
9259
|
+
|
|
9260
|
+
return result
|
|
9261
|
+
|
|
9262
|
+
|
|
9015
9263
|
def PlayerMediaPause(self,
|
|
9016
9264
|
deviceId:str=None,
|
|
9017
9265
|
delay:float=0.50
|
|
@@ -9199,7 +9447,11 @@ class SpotifyClient:
|
|
|
9199
9447
|
delay = self._ValidateDelay(delay, 0.50, 10)
|
|
9200
9448
|
|
|
9201
9449
|
# check for device name; convert to an id if a name was supplied.
|
|
9202
|
-
deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
9450
|
+
#deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
9451
|
+
|
|
9452
|
+
# ensure the specified device id / name is active and available, and
|
|
9453
|
+
# the user context is set correctly.
|
|
9454
|
+
deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
|
|
9203
9455
|
|
|
9204
9456
|
# build spotify web api request parameters.
|
|
9205
9457
|
reqData:dict = \
|
|
@@ -9315,7 +9567,11 @@ class SpotifyClient:
|
|
|
9315
9567
|
arrUris.append(trackSaved.Track.Uri)
|
|
9316
9568
|
|
|
9317
9569
|
# check for device name; convert to an id if a name was supplied.
|
|
9318
|
-
deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
9570
|
+
#deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
9571
|
+
|
|
9572
|
+
# ensure the specified device id / name is active and available, and
|
|
9573
|
+
# the user context is set correctly.
|
|
9574
|
+
deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
|
|
9319
9575
|
|
|
9320
9576
|
# set desired shuffle mode.
|
|
9321
9577
|
self.PlayerSetShuffleMode(shuffle, deviceId, delay)
|
|
@@ -9420,7 +9676,11 @@ class SpotifyClient:
|
|
|
9420
9676
|
arrUris[idx] = arrUris[idx].strip()
|
|
9421
9677
|
|
|
9422
9678
|
# check for device name; convert to an id if a name was supplied.
|
|
9423
|
-
deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
9679
|
+
#deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
9680
|
+
|
|
9681
|
+
# ensure the specified device id / name is active and available, and
|
|
9682
|
+
# the user context is set correctly.
|
|
9683
|
+
deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
|
|
9424
9684
|
|
|
9425
9685
|
# build spotify web api request parameters.
|
|
9426
9686
|
reqData:dict = \
|
|
@@ -9830,6 +10090,231 @@ class SpotifyClient:
|
|
|
9830
10090
|
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
9831
10091
|
|
|
9832
10092
|
|
|
10093
|
+
def PlayerResolveDeviceId(
|
|
10094
|
+
self,
|
|
10095
|
+
deviceValue:str,
|
|
10096
|
+
verifyUserContext:bool=True,
|
|
10097
|
+
verifyTimeout:float=5.0,
|
|
10098
|
+
) -> str:
|
|
10099
|
+
"""
|
|
10100
|
+
Resolves a Spotify Connect device identifier from a specified device id, name, alias id,
|
|
10101
|
+
or alias name. This will ensure that the device id can be found on the network, as well
|
|
10102
|
+
as connect to the device if necessary with the current user context.
|
|
10103
|
+
|
|
10104
|
+
Args:
|
|
10105
|
+
deviceValue (str):
|
|
10106
|
+
The device id / name value to check.
|
|
10107
|
+
verifyUserContext (bool):
|
|
10108
|
+
If True, the active user context of the resolved device is checked to ensure it
|
|
10109
|
+
matches the user context specified on the class constructor.
|
|
10110
|
+
If False, the user context will not be checked.
|
|
10111
|
+
Default is True.
|
|
10112
|
+
verifyTimeout (float):
|
|
10113
|
+
Maximum time to wait (in seconds) for the device to become active in the Spotify
|
|
10114
|
+
Connect device list. This value is only used if a Connect command has to be
|
|
10115
|
+
issued to activate the device.
|
|
10116
|
+
Default is 5; value range is 0 - 10.
|
|
10117
|
+
|
|
10118
|
+
Returns:
|
|
10119
|
+
A device Id for the deviceValue if one could be resolved;
|
|
10120
|
+
otherwise, just the original deviceValue with the understanding that
|
|
10121
|
+
subsequent operations will probably fail since it's not in the device list.
|
|
10122
|
+
|
|
10123
|
+
Raises:
|
|
10124
|
+
SpotifyWebApiError:
|
|
10125
|
+
If the Spotify Web API request was for a non-authorization service
|
|
10126
|
+
and the response contains error information.
|
|
10127
|
+
SpotifyZeroconfApiError:
|
|
10128
|
+
If any Spotify Zeroconf API request failed.
|
|
10129
|
+
SpotifApiError:
|
|
10130
|
+
If the method fails for any other reason.
|
|
10131
|
+
|
|
10132
|
+
A Zeroconf discovery process is initiated to search for all Spotify Connect devices connected
|
|
10133
|
+
to the local network. A Spotify Zeroconf API `getInfo` call is then issued for each device
|
|
10134
|
+
that was found, searching for a match on the `deviceValue` argument. Depending on the format
|
|
10135
|
+
of the `deviceValue`, the search will match on either a Device ID or RemoteName as well as any
|
|
10136
|
+
alias ID's or Names that are in use (for multi-room configurations).
|
|
10137
|
+
|
|
10138
|
+
The device ID value is returned if a match is found; otherwise, null is returned.
|
|
10139
|
+
|
|
10140
|
+
A user context switch will also be performed if a device id is resolved AND the active user of the
|
|
10141
|
+
device does not match the `SpotifyConnectUsername` argument specified on the class constructor.
|
|
10142
|
+
The user context switch is bypassed if the `verifyUserContext` argument is False, or if the
|
|
10143
|
+
`SpotifyConnectUsername` was not supplied on the class constructor. If the user context is to be
|
|
10144
|
+
switched, then a Disconnect will be issued if a user context is active on the device followed by
|
|
10145
|
+
a Connect to the user context specified on the class constructor.
|
|
10146
|
+
|
|
10147
|
+
<details>
|
|
10148
|
+
<summary>Sample Code</summary>
|
|
10149
|
+
```python
|
|
10150
|
+
.. include:: ../docs/include/samplecode/SpotifyClient/PlayerResolveDeviceId.py
|
|
10151
|
+
```
|
|
10152
|
+
</details>
|
|
10153
|
+
"""
|
|
10154
|
+
apiMethodName:str = 'PlayerResolveDeviceId'
|
|
10155
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
10156
|
+
deviceIdResult:str = None
|
|
10157
|
+
|
|
10158
|
+
try:
|
|
10159
|
+
|
|
10160
|
+
# trace.
|
|
10161
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
10162
|
+
apiMethodParms.AppendKeyValue("deviceValue", deviceValue)
|
|
10163
|
+
apiMethodParms.AppendKeyValue("verifyUserContext", verifyUserContext)
|
|
10164
|
+
apiMethodParms.AppendKeyValue("verifyTimeout", verifyTimeout)
|
|
10165
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Resolving Spotify Connect Player Device Id", apiMethodParms)
|
|
10166
|
+
|
|
10167
|
+
# if device value not specified then we are done.
|
|
10168
|
+
if (deviceValue is None) or (len(deviceValue.strip()) == 0):
|
|
10169
|
+
_logsi.LogVerbose("deviceValue not specified; nothing to do")
|
|
10170
|
+
return None
|
|
10171
|
+
|
|
10172
|
+
# validations.
|
|
10173
|
+
verifyTimeout = self._ValidateDelay(verifyTimeout, 5, 10)
|
|
10174
|
+
if (not isinstance(verifyUserContext, bool)):
|
|
10175
|
+
verifyUserContext = True
|
|
10176
|
+
|
|
10177
|
+
# determine if the device value specified is a name (e.g. "Bose-ST10-1") or
|
|
10178
|
+
# an id (e.g. "30fbc80e35598f3c242f2120413c943dfd9715fe").
|
|
10179
|
+
isDeviceId:bool = SpotifyClient.IsDeviceId(deviceValue)
|
|
10180
|
+
deviceValueCompare:str = deviceValue.lower()
|
|
10181
|
+
deviceActiveUser:str = None
|
|
10182
|
+
|
|
10183
|
+
# discover Spotify Connect devices on the network, waiting up to the specified timeout
|
|
10184
|
+
# for all devices to be discovered. this will return all Spotify Connect devices that
|
|
10185
|
+
# are currently powered on, and are registered to Zeroconf / mDNS.
|
|
10186
|
+
_logsi.LogVerbose("Discovering Spotify Connect devices on the local network")
|
|
10187
|
+
discovery:SpotifyDiscovery = SpotifyDiscovery(self._Zeroconf, printToConsole=False)
|
|
10188
|
+
discovery.DiscoverDevices(timeout=self._SpotifyConnectDiscoveryTimeout)
|
|
10189
|
+
|
|
10190
|
+
# process all discovered devices.
|
|
10191
|
+
discoverResult:ZeroconfDiscoveryResult
|
|
10192
|
+
for discoverResult in discovery.DiscoveryResults:
|
|
10193
|
+
|
|
10194
|
+
# get the id from the device via the zeroconf API getInfo endpoint, as the id is not
|
|
10195
|
+
# returned in the zeroconf discovery result.
|
|
10196
|
+
zconn:ZeroconfConnect = ZeroconfConnect(discoverResult.HostIpv4Address,
|
|
10197
|
+
discoverResult.HostIpPort,
|
|
10198
|
+
discoverResult.SpotifyConnectCPath,
|
|
10199
|
+
useSSL=False)
|
|
10200
|
+
info:ZeroconfGetInfo = zconn.GetInformation()
|
|
10201
|
+
|
|
10202
|
+
# store the currently active user of the device, in case we need to switch users later on.
|
|
10203
|
+
deviceActiveUser = info.ActiveUser.lower()
|
|
10204
|
+
|
|
10205
|
+
# are aliases being used (RemoteName is null if so)?
|
|
10206
|
+
if info.RemoteName is None:
|
|
10207
|
+
|
|
10208
|
+
# if aliases are defined for the device, then we have to check each alias for a match.
|
|
10209
|
+
infoAlias:ZeroconfGetInfoAlias
|
|
10210
|
+
for infoAlias in info.Aliases:
|
|
10211
|
+
|
|
10212
|
+
# if input deviceValue is a device id then compare the id; otherwise, compare the name.
|
|
10213
|
+
if isDeviceId:
|
|
10214
|
+
if (infoAlias.Id.lower() == deviceValueCompare):
|
|
10215
|
+
_logsi.LogVerbose("Device value '%s' was matched to device Alias Id '%s' (%s)" % (deviceValue, infoAlias.Name, infoAlias.Id))
|
|
10216
|
+
deviceIdResult = infoAlias.Id
|
|
10217
|
+
break # alias processing loop
|
|
10218
|
+
else:
|
|
10219
|
+
if (infoAlias.Name.lower() == deviceValueCompare):
|
|
10220
|
+
_logsi.LogVerbose("Device value '%s' was matched to device Alias Name '%s' (%s)" % (deviceValue, infoAlias.Name, infoAlias.Id))
|
|
10221
|
+
deviceIdResult = infoAlias.Id
|
|
10222
|
+
break # alias processing loop
|
|
10223
|
+
|
|
10224
|
+
else:
|
|
10225
|
+
|
|
10226
|
+
# if input deviceValue is a device id then compare the id; otherwise, compare the name.
|
|
10227
|
+
if isDeviceId:
|
|
10228
|
+
if (info.DeviceId.lower() == deviceValueCompare):
|
|
10229
|
+
_logsi.LogVerbose("Device value '%s' was matched to device Id '%s' (%s)" % (deviceValue, info.RemoteName, info.DeviceId))
|
|
10230
|
+
deviceIdResult = info.DeviceId
|
|
10231
|
+
else:
|
|
10232
|
+
if (info.RemoteName.lower() == deviceValueCompare):
|
|
10233
|
+
_logsi.LogVerbose("Device value '%s' was matched to device RemoteName '%s' (%s)" % (deviceValue, info.RemoteName, info.DeviceId))
|
|
10234
|
+
deviceIdResult = info.DeviceId
|
|
10235
|
+
|
|
10236
|
+
# did we resolve the device id?
|
|
10237
|
+
if deviceIdResult is not None:
|
|
10238
|
+
|
|
10239
|
+
# did caller request user context switch bypass?
|
|
10240
|
+
# if so, then just return the device id.
|
|
10241
|
+
if (not verifyUserContext):
|
|
10242
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' will be used for Device id '%s', as user chose to bypass user context switch" % (deviceActiveUser, deviceIdResult))
|
|
10243
|
+
break
|
|
10244
|
+
|
|
10245
|
+
# was a Spotify Connect user account specified on the class constructor?
|
|
10246
|
+
# if not, then do not switch the user context.
|
|
10247
|
+
if (self._SpotifyConnectUsername is None):
|
|
10248
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' will be used for Device id '%s', as a SpotifyConnectUsername parameter was not supplied" % (deviceActiveUser, deviceIdResult))
|
|
10249
|
+
break
|
|
10250
|
+
|
|
10251
|
+
# does our Spotify Connect user account need to take control of the device?
|
|
10252
|
+
# if not, then we are done.
|
|
10253
|
+
if (deviceActiveUser == self._SpotifyConnectUsername.lower()) or (deviceActiveUser == self.UserProfile.Id.lower()):
|
|
10254
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' was verified for Device id '%s'; switch not necessary" % (deviceActiveUser, deviceIdResult))
|
|
10255
|
+
break
|
|
10256
|
+
|
|
10257
|
+
zcfResult:ZeroconfResponse
|
|
10258
|
+
|
|
10259
|
+
# if a different user context has control of the device then we need to disconnect
|
|
10260
|
+
# the current user context before connecting a different user.
|
|
10261
|
+
if (info.HasActiveUser):
|
|
10262
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' is being disconnected for Device id '%s'" % (deviceActiveUser, deviceIdResult))
|
|
10263
|
+
zcfResult = zconn.Disconnect()
|
|
10264
|
+
|
|
10265
|
+
# connect the device to OUR Spotify Connect user context.
|
|
10266
|
+
# note that the result here only indicates that the connect was submitted - NOT that it was successful!
|
|
10267
|
+
_logsi.LogVerbose("Spotify Connect user context '%s' is being connected for Device id '%s'" % (self._SpotifyConnectUsername, deviceIdResult))
|
|
10268
|
+
zcfResult = zconn.Connect(self._SpotifyConnectUsername, self._SpotifyConnectPassword)
|
|
10269
|
+
|
|
10270
|
+
# trace.
|
|
10271
|
+
_logsi.LogVerbose("Spotify Connect user context switch from '%s' to '%s' was requested for Device id '%s'" % (deviceActiveUser, self._SpotifyConnectUsername, deviceIdResult))
|
|
10272
|
+
|
|
10273
|
+
loopTotalDelay:float = 0
|
|
10274
|
+
LOOP_DELAY:float = 0.25
|
|
10275
|
+
while True:
|
|
10276
|
+
|
|
10277
|
+
# wait just a bit between active device list queries.
|
|
10278
|
+
_logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % LOOP_DELAY)
|
|
10279
|
+
time.sleep(LOOP_DELAY)
|
|
10280
|
+
loopTotalDelay = loopTotalDelay + LOOP_DELAY
|
|
10281
|
+
|
|
10282
|
+
# check for the device in the active device list.
|
|
10283
|
+
# if we find it, then we are done.
|
|
10284
|
+
device:Device = self.GetPlayerDevice(deviceIdResult, True)
|
|
10285
|
+
if device is not None:
|
|
10286
|
+
_logsi.LogVerbose("Spotify Connect Device id '%s' is now in the active device list; device found within %f seconds of Connect" % (deviceIdResult, loopTotalDelay))
|
|
10287
|
+
break
|
|
10288
|
+
|
|
10289
|
+
# only check so many times before we give up;
|
|
10290
|
+
# we will keep checking until we find the device, or we exceed the verifyTimeout value.
|
|
10291
|
+
if (loopTotalDelay > verifyTimeout):
|
|
10292
|
+
_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))
|
|
10293
|
+
break
|
|
10294
|
+
|
|
10295
|
+
# end the device processing loop.
|
|
10296
|
+
break
|
|
10297
|
+
|
|
10298
|
+
# did we resolve the device id?
|
|
10299
|
+
if deviceIdResult is None:
|
|
10300
|
+
_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))
|
|
10301
|
+
|
|
10302
|
+
# return the resolved device id.
|
|
10303
|
+
return deviceIdResult
|
|
10304
|
+
|
|
10305
|
+
except SpotifyZeroconfApiError: raise # pass handled exceptions on thru
|
|
10306
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
10307
|
+
except Exception as ex:
|
|
10308
|
+
|
|
10309
|
+
# format unhandled exception.
|
|
10310
|
+
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
|
|
10311
|
+
|
|
10312
|
+
finally:
|
|
10313
|
+
|
|
10314
|
+
# trace.
|
|
10315
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
10316
|
+
|
|
10317
|
+
|
|
9833
10318
|
def PlayerSetRepeatMode(self,
|
|
9834
10319
|
state:str='off',
|
|
9835
10320
|
deviceId:str=None,
|
|
@@ -10187,7 +10672,11 @@ class SpotifyClient:
|
|
|
10187
10672
|
delay = self._ValidateDelay(delay, 0.50, 10)
|
|
10188
10673
|
|
|
10189
10674
|
# check for device name; convert to an id if a name was supplied.
|
|
10190
|
-
deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
10675
|
+
#deviceId = self.PlayerConvertDeviceNameToId(deviceId)
|
|
10676
|
+
|
|
10677
|
+
# ensure the specified device id / name is active and available, and
|
|
10678
|
+
# the user context is set correctly.
|
|
10679
|
+
deviceId = self.PlayerResolveDeviceId(deviceId, verifyUserContext=True)
|
|
10191
10680
|
|
|
10192
10681
|
# build spotify web api request parameters.
|
|
10193
10682
|
reqData:dict = \
|