spotifywebapiPython 1.0.43__py3-none-any.whl → 1.0.44__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.43.dist-info → spotifywebapiPython-1.0.44.dist-info}/METADATA +1 -1
- {spotifywebapiPython-1.0.43.dist-info → spotifywebapiPython-1.0.44.dist-info}/RECORD +33 -26
- spotifywebapipython/const.py +1 -1
- spotifywebapipython/docs/__init__.html +17 -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 +200 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifydiscovery.html +45 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapiauthenticationerror.html +2 -2
- spotifywebapipython/docs/spotifywebapiPython/spotifywebapierror.html +2 -2
- spotifywebapipython/models/__init__.py +14 -0
- spotifywebapipython/models/playlist.py +4 -1
- spotifywebapipython/models/playlistsimplified.py +4 -1
- spotifywebapipython/models/playlisttracksummary.py +3 -1
- spotifywebapipython/models/searchresponse.py +1 -1
- spotifywebapipython/models/zeroconfblobbuilder.py +144 -0
- spotifywebapipython/models/zeroconfdiscoveryresult.py +451 -0
- spotifywebapipython/models/zeroconfgetinfo.py +359 -0
- spotifywebapipython/models/zeroconfgetinfoalias.py +96 -0
- spotifywebapipython/models/zeroconfgetinfodrmmediaformat.py +90 -0
- spotifywebapipython/models/zeroconfproperty.py +93 -0
- spotifywebapipython/models/zeroconfresponse.py +108 -0
- spotifywebapipython/spotifyclient.py +293 -3
- spotifywebapipython/spotifydiscovery.py +98 -11
- {spotifywebapiPython-1.0.43.dist-info → spotifywebapiPython-1.0.44.dist-info}/LICENSE +0 -0
- {spotifywebapiPython-1.0.43.dist-info → spotifywebapiPython-1.0.44.dist-info}/WHEEL +0 -0
- {spotifywebapiPython-1.0.43.dist-info → spotifywebapiPython-1.0.44.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# external package imports.
|
|
2
|
+
|
|
3
|
+
# our package imports.
|
|
4
|
+
from ..sautils import export
|
|
5
|
+
|
|
6
|
+
@export
|
|
7
|
+
class ZeroconfResponse:
|
|
8
|
+
"""
|
|
9
|
+
Spotify Web API Zeroconf basic response object.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, root:dict=None) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Initializes a new instance of the class.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
root (dict):
|
|
18
|
+
Spotify Web API JSON response in dictionary format, used to load object
|
|
19
|
+
attributes; otherwise, None to not load attributes.
|
|
20
|
+
"""
|
|
21
|
+
self._SpotifyError:int = None
|
|
22
|
+
self._Status:int = None
|
|
23
|
+
self._StatusString:str = None
|
|
24
|
+
self._ResponseSource:str = None
|
|
25
|
+
|
|
26
|
+
if (root is None):
|
|
27
|
+
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
else:
|
|
31
|
+
|
|
32
|
+
self._ResponseSource = root.get('responseSource', None)
|
|
33
|
+
self._SpotifyError = root.get('spotifyError', None)
|
|
34
|
+
self._Status = root.get('status', None)
|
|
35
|
+
self._StatusString = root.get('statusString', None)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def __repr__(self) -> str:
|
|
39
|
+
return self.ToString()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def __str__(self) -> str:
|
|
43
|
+
return self.ToString()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def ResponseSource(self) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Token type provided by the client (e.g. "accesstoken").
|
|
50
|
+
"""
|
|
51
|
+
return self._ResponseSource
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def SpotifyError(self) -> int:
|
|
56
|
+
"""
|
|
57
|
+
The last error code returned by a Spotify API call or the SpCallbackError() callback (e.g. 0, -119, etc).
|
|
58
|
+
"""
|
|
59
|
+
return self._SpotifyError
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def Status(self) -> int:
|
|
64
|
+
"""
|
|
65
|
+
A code indicating the result of the operation (e.g. 101, 402, etc).
|
|
66
|
+
"""
|
|
67
|
+
return self._Status
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def StatusString(self) -> str:
|
|
72
|
+
"""
|
|
73
|
+
The string describing the status code (e.g. "OK", "ERROR-SPOTIFY-ERROR", etc).
|
|
74
|
+
"""
|
|
75
|
+
return self._StatusString
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def ToDictionary(self) -> dict:
|
|
79
|
+
"""
|
|
80
|
+
Returns a dictionary representation of the class.
|
|
81
|
+
"""
|
|
82
|
+
result:dict = \
|
|
83
|
+
{
|
|
84
|
+
'SpotifyError': self._SpotifyError,
|
|
85
|
+
'Status': self._Status,
|
|
86
|
+
'StatusString': self._StatusString,
|
|
87
|
+
'ResponseSource': self._ResponseSource,
|
|
88
|
+
}
|
|
89
|
+
return result
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def ToString(self, includeTitle:bool=False) -> str:
|
|
93
|
+
"""
|
|
94
|
+
Returns a displayable string representation of the class.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
includeTitle (str):
|
|
98
|
+
True to include the class name title prefix.
|
|
99
|
+
"""
|
|
100
|
+
msg:str = ''
|
|
101
|
+
if includeTitle:
|
|
102
|
+
msg = 'ZeroconfResponse:'
|
|
103
|
+
|
|
104
|
+
if self._SpotifyError is not None: msg = '%s\n SpotifyError="%s"' % (msg, str(self._SpotifyError))
|
|
105
|
+
if self._Status is not None: msg = '%s\n Status="%s"' % (msg, str(self._Status))
|
|
106
|
+
if self._StatusString is not None: msg = '%s\n StatusString="%s"' % (msg, str(self._StatusString))
|
|
107
|
+
if self._ResponseSource is not None: msg = '%s\n ResponseSource="%s"' % (msg, str(self._ResponseSource))
|
|
108
|
+
return msg
|
|
@@ -14,6 +14,7 @@ from lxml.etree import fromstring, Element
|
|
|
14
14
|
from .oauthcli import AuthClient
|
|
15
15
|
from .models import *
|
|
16
16
|
from .models import UserProfile as UserProfileCurrentUser
|
|
17
|
+
from .models.zeroconfblobbuilder import int_to_b64str
|
|
17
18
|
from .sautils import export, GetUnixTimestampMSFromUtcNow, _xmlGetInnerText
|
|
18
19
|
from .saappmessages import SAAppMessages
|
|
19
20
|
from .spotifyapierror import SpotifyApiError
|
|
@@ -40,6 +41,8 @@ from .const import (
|
|
|
40
41
|
CACHE_SOURCE_CACHED:str = "cached"
|
|
41
42
|
CACHE_SOURCE_CURRENT:str = "current"
|
|
42
43
|
|
|
44
|
+
SPOTIFY_DJ_PLAYLIST_ID = "37i9dqzf1eykqdzj48dyyq"
|
|
45
|
+
|
|
43
46
|
# get smartinspect logger reference; create a new session for this module name.
|
|
44
47
|
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SISourceId
|
|
45
48
|
import logging
|
|
@@ -454,6 +457,19 @@ class SpotifyClient:
|
|
|
454
457
|
|
|
455
458
|
elif responseData is not None:
|
|
456
459
|
|
|
460
|
+
# if response is a string (e.g. html response) and is not in the 2xx range then
|
|
461
|
+
# we will assume it's an error, even though no json 'error' response body was returned!
|
|
462
|
+
# this is usually due to a '503 Server Error' response.
|
|
463
|
+
if (response.status > 299) and (isinstance(responseData, str)):
|
|
464
|
+
|
|
465
|
+
errCode = response.status
|
|
466
|
+
errMessage = response.reason
|
|
467
|
+
|
|
468
|
+
if responseUTF8 is not None:
|
|
469
|
+
errMessage = responseUTF8
|
|
470
|
+
|
|
471
|
+
raise SpotifyWebApiError(errCode, errMessage, msg.MethodName, response.reason, _logsi)
|
|
472
|
+
|
|
457
473
|
# does json response contain error details?
|
|
458
474
|
if CONST_ERROR in responseData:
|
|
459
475
|
|
|
@@ -669,7 +685,8 @@ class SpotifyClient:
|
|
|
669
685
|
uri:str = msg.Uri
|
|
670
686
|
if (uri == self.SpotifyApiTokenUrl) \
|
|
671
687
|
or (uri == self.SpotifyApiAuthorizeUrl) \
|
|
672
|
-
or (uri.startswith('https:'))
|
|
688
|
+
or (uri.startswith('https:')) \
|
|
689
|
+
or (uri.startswith('http:')):
|
|
673
690
|
url = uri
|
|
674
691
|
else:
|
|
675
692
|
url = f'{self.SpotifyWebApiUrlBase}{uri}'
|
|
@@ -705,9 +722,13 @@ class SpotifyClient:
|
|
|
705
722
|
# call the appropriate poolmanager request method.
|
|
706
723
|
if msg.HasUrlParameters:
|
|
707
724
|
|
|
708
|
-
# add querystring parameters to url
|
|
725
|
+
# add querystring parameters to url; if url already has a partial parm
|
|
726
|
+
# string (e.g. has a '?xxx=...') then use the append separator (e.g. '...&xxx=...').
|
|
709
727
|
urlQS:str = urlencode(msg.UrlParameters)
|
|
710
|
-
|
|
728
|
+
urlParmSep:str = '?'
|
|
729
|
+
if (url.find('?') > 0):
|
|
730
|
+
urlParmSep = '&'
|
|
731
|
+
url = url + urlParmSep + urlQS
|
|
711
732
|
|
|
712
733
|
_logsi.LogDictionary(SILevel.Verbose, "SpotifyClient http request: '%s' (with urlparms)" % (url), msg.UrlParameters, prettyPrint=True)
|
|
713
734
|
response = self._Manager.request_encode_url(method, url, headers=msg.RequestHeaders)
|
|
@@ -6551,6 +6572,27 @@ class SpotifyClient:
|
|
|
6551
6572
|
|
|
6552
6573
|
# ensure market was either supplied or implied; default if neither.
|
|
6553
6574
|
market = self._ValidateMarket(market)
|
|
6575
|
+
|
|
6576
|
+
# was the Spotify DJ playlist specified?
|
|
6577
|
+
# the DJ is not fully integrated with the playlist API, so the GET request will fail.
|
|
6578
|
+
# we will manually build a basic playlist object to return for the playlist.
|
|
6579
|
+
if (playlistId is not None) and (playlistId.lower() == SPOTIFY_DJ_PLAYLIST_ID):
|
|
6580
|
+
|
|
6581
|
+
# build basic playlist response.
|
|
6582
|
+
result = Playlist()
|
|
6583
|
+
result._Collaborative = False
|
|
6584
|
+
result._Description = 'Spotify DJ Playlist'
|
|
6585
|
+
result._ExternalUrls._Spotify = 'https://open.spotify.com/playlist/%s' % playlistId
|
|
6586
|
+
result._Id = playlistId
|
|
6587
|
+
result._Name = 'DJ'
|
|
6588
|
+
result._Public = False
|
|
6589
|
+
result._Type = 'playlist'
|
|
6590
|
+
result._Uri = 'spotify:playlist:%s' % playlistId
|
|
6591
|
+
|
|
6592
|
+
# trace.
|
|
6593
|
+
_logsi.LogVerbose("Spotify DJ Playlist detected; request will be bypassed, and a basic PlayList object returned")
|
|
6594
|
+
_logsi.LogObject(SILevel.Verbose, TRACE_METHOD_RESULT_TYPE % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
|
|
6595
|
+
return result
|
|
6554
6596
|
|
|
6555
6597
|
# build spotify web api request parameters.
|
|
6556
6598
|
urlParms:dict = {}
|
|
@@ -13777,3 +13819,251 @@ class SpotifyClient:
|
|
|
13777
13819
|
|
|
13778
13820
|
# trace.
|
|
13779
13821
|
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
13822
|
+
|
|
13823
|
+
|
|
13824
|
+
|
|
13825
|
+
def ZeroconfGetInformation(self,
|
|
13826
|
+
actionUrl:str,
|
|
13827
|
+
) -> ZeroconfGetInfo:
|
|
13828
|
+
"""
|
|
13829
|
+
Retrieve Spotify Connect device information from the Spotify Zeroconf API `getInfo` endpoint.
|
|
13830
|
+
|
|
13831
|
+
Args:
|
|
13832
|
+
actionUrl (str):
|
|
13833
|
+
The Zeroconf action url to issue the request to.
|
|
13834
|
+
Example: `http://192.168.1.80:8200/zc?action=getInfo`
|
|
13835
|
+
|
|
13836
|
+
Returns:
|
|
13837
|
+
An `ZeroconfGetInfo` object that contains the response.
|
|
13838
|
+
|
|
13839
|
+
Raises:
|
|
13840
|
+
SpotifyWebApiError:
|
|
13841
|
+
If the Spotify Web API request was for a non-authorization service
|
|
13842
|
+
and the response contains error information.
|
|
13843
|
+
SpotifApiError:
|
|
13844
|
+
If the method fails for any other reason.
|
|
13845
|
+
|
|
13846
|
+
<details>
|
|
13847
|
+
<summary>Sample Code</summary>
|
|
13848
|
+
```python
|
|
13849
|
+
.. include:: ../docs/include/samplecode/SpotifyClient/ZeroconfGetInformation.py
|
|
13850
|
+
```
|
|
13851
|
+
</details>
|
|
13852
|
+
"""
|
|
13853
|
+
apiMethodName:str = 'ZeroconfGetInformation'
|
|
13854
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
13855
|
+
result:ZeroconfGetInfo = None
|
|
13856
|
+
|
|
13857
|
+
try:
|
|
13858
|
+
|
|
13859
|
+
# trace.
|
|
13860
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
13861
|
+
apiMethodParms.AppendKeyValue("actionUrl", actionUrl)
|
|
13862
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Get Spotify Zeroconf getInfo response", apiMethodParms)
|
|
13863
|
+
|
|
13864
|
+
# execute spotify zeroconf api request.
|
|
13865
|
+
msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, actionUrl)
|
|
13866
|
+
self.MakeRequest('GET', msg)
|
|
13867
|
+
|
|
13868
|
+
# process results.
|
|
13869
|
+
result = ZeroconfGetInfo(root=msg.ResponseData)
|
|
13870
|
+
|
|
13871
|
+
# trace.
|
|
13872
|
+
_logsi.LogObject(SILevel.Verbose, TRACE_METHOD_RESULT_TYPE % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
|
|
13873
|
+
return result
|
|
13874
|
+
|
|
13875
|
+
except SpotifyWebApiError: raise # pass handled exceptions on thru
|
|
13876
|
+
except SpotifyWebApiAuthenticationError: raise # pass handled exceptions on thru
|
|
13877
|
+
except Exception as ex:
|
|
13878
|
+
|
|
13879
|
+
# format unhandled exception.
|
|
13880
|
+
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
|
|
13881
|
+
|
|
13882
|
+
finally:
|
|
13883
|
+
|
|
13884
|
+
# trace.
|
|
13885
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
13886
|
+
|
|
13887
|
+
|
|
13888
|
+
def ZeroconfResetUsers(self,
|
|
13889
|
+
actionUrl:str,
|
|
13890
|
+
) -> ZeroconfResponse:
|
|
13891
|
+
"""
|
|
13892
|
+
Reset users for a Spotify Connect device by calling the Spotify Zeroconf API `resetUsers` endpoint.
|
|
13893
|
+
|
|
13894
|
+
Args:
|
|
13895
|
+
actionUrl (str):
|
|
13896
|
+
The Zeroconf action url to issue the request to.
|
|
13897
|
+
Example: `http://192.168.1.80:8200/zc?action=resetUsers`
|
|
13898
|
+
|
|
13899
|
+
Returns:
|
|
13900
|
+
An `ZeroconfResponse` object that contains the response.
|
|
13901
|
+
|
|
13902
|
+
Raises:
|
|
13903
|
+
SpotifyWebApiError:
|
|
13904
|
+
If the Spotify Web API request was for a non-authorization service
|
|
13905
|
+
and the response contains error information.
|
|
13906
|
+
SpotifApiError:
|
|
13907
|
+
If the method fails for any other reason.
|
|
13908
|
+
|
|
13909
|
+
<details>
|
|
13910
|
+
<summary>Sample Code</summary>
|
|
13911
|
+
```python
|
|
13912
|
+
.. include:: ../docs/include/samplecode/SpotifyClient/ZeroconfResetUsers.py
|
|
13913
|
+
```
|
|
13914
|
+
</details>
|
|
13915
|
+
"""
|
|
13916
|
+
apiMethodName:str = 'ZeroconfResetUsers'
|
|
13917
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
13918
|
+
result:ZeroconfResponse = None
|
|
13919
|
+
|
|
13920
|
+
try:
|
|
13921
|
+
|
|
13922
|
+
# trace.
|
|
13923
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
13924
|
+
apiMethodParms.AppendKeyValue("actionUrl", actionUrl)
|
|
13925
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Get Spotify Zeroconf GetInfo response", apiMethodParms)
|
|
13926
|
+
|
|
13927
|
+
# execute spotify zeroconf api request.
|
|
13928
|
+
msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, actionUrl)
|
|
13929
|
+
self.MakeRequest('GET', msg)
|
|
13930
|
+
|
|
13931
|
+
# process results.
|
|
13932
|
+
result = ZeroconfResponse(root=msg.ResponseData)
|
|
13933
|
+
|
|
13934
|
+
# trace.
|
|
13935
|
+
_logsi.LogObject(SILevel.Verbose, TRACE_METHOD_RESULT_TYPE % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
|
|
13936
|
+
return result
|
|
13937
|
+
|
|
13938
|
+
except SpotifyWebApiError: raise # pass handled exceptions on thru
|
|
13939
|
+
except SpotifyWebApiAuthenticationError: raise # pass handled exceptions on thru
|
|
13940
|
+
except Exception as ex:
|
|
13941
|
+
|
|
13942
|
+
# format unhandled exception.
|
|
13943
|
+
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
|
|
13944
|
+
|
|
13945
|
+
finally:
|
|
13946
|
+
|
|
13947
|
+
# trace.
|
|
13948
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
13949
|
+
|
|
13950
|
+
|
|
13951
|
+
def ZeroconfAddUser(self,
|
|
13952
|
+
actionUrl:str,
|
|
13953
|
+
getInfoResponse:ZeroconfGetInfo,
|
|
13954
|
+
username:str,
|
|
13955
|
+
password:str,
|
|
13956
|
+
) -> ZeroconfResponse:
|
|
13957
|
+
"""
|
|
13958
|
+
Add a user to a Spotify Connect device by calling the Spotify Zeroconf API `addUser` endpoint.
|
|
13959
|
+
|
|
13960
|
+
Args:
|
|
13961
|
+
actionUrl (str):
|
|
13962
|
+
The Zeroconf action url to issue the request to.
|
|
13963
|
+
Example: `http://192.168.1.80:8200/zc?action=addUser&version=1.0`
|
|
13964
|
+
|
|
13965
|
+
Returns:
|
|
13966
|
+
An `ZeroconfResponse` object that contains the response.
|
|
13967
|
+
|
|
13968
|
+
Raises:
|
|
13969
|
+
SpotifyWebApiError:
|
|
13970
|
+
If the Spotify Web API request was for a non-authorization service
|
|
13971
|
+
and the response contains error information.
|
|
13972
|
+
SpotifApiError:
|
|
13973
|
+
If the method fails for any other reason.
|
|
13974
|
+
|
|
13975
|
+
<details>
|
|
13976
|
+
<summary>Sample Code</summary>
|
|
13977
|
+
```python
|
|
13978
|
+
.. include:: ../docs/include/samplecode/SpotifyClient/ZeroconfGetInformation.py
|
|
13979
|
+
```
|
|
13980
|
+
</details>
|
|
13981
|
+
"""
|
|
13982
|
+
apiMethodName:str = 'ZeroconfAddUser'
|
|
13983
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
13984
|
+
result:ZeroconfResponse = None
|
|
13985
|
+
|
|
13986
|
+
try:
|
|
13987
|
+
|
|
13988
|
+
# trace.
|
|
13989
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
13990
|
+
apiMethodParms.AppendKeyValue("actionUrl", actionUrl)
|
|
13991
|
+
apiMethodParms.AppendKeyValue("getInfoResponse", getInfoResponse)
|
|
13992
|
+
apiMethodParms.AppendKeyValue("username", username)
|
|
13993
|
+
apiMethodParms.AppendKeyValue("password", password)
|
|
13994
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Get Spotify Zeroconf addUser response", apiMethodParms)
|
|
13995
|
+
|
|
13996
|
+
raise NotImplemented()
|
|
13997
|
+
|
|
13998
|
+
# build credentials object.
|
|
13999
|
+
self.credentials = ZeroconfCredentials(username, password)
|
|
14000
|
+
|
|
14001
|
+
# build authentication blob.
|
|
14002
|
+
builder = ZeroconfBlobBuilder(self.credentials, getInfoResponse.DeviceId, getInfoResponse.PublicKey)
|
|
14003
|
+
blob = builder.build()
|
|
14004
|
+
|
|
14005
|
+
# # call zeroconf to add the user to the device.
|
|
14006
|
+
# r = self._add_user(
|
|
14007
|
+
# self.credentials.username.decode('ascii'),
|
|
14008
|
+
# int_to_b64str(builder.dh_keys.public_key),
|
|
14009
|
+
# blob)
|
|
14010
|
+
|
|
14011
|
+
# def _add_user(self, username: str, client_key: str, blob: str): # -> bool
|
|
14012
|
+
# respone = requests.post(
|
|
14013
|
+
# self.uri,
|
|
14014
|
+
# headers={
|
|
14015
|
+
# 'Content-Type': 'application/x-www-form-urlencoded'
|
|
14016
|
+
# },
|
|
14017
|
+
# params={
|
|
14018
|
+
# 'action': 'addUser',
|
|
14019
|
+
# 'userName': username,
|
|
14020
|
+
# 'clientKey': client_key,
|
|
14021
|
+
# 'blob': blob
|
|
14022
|
+
# }
|
|
14023
|
+
# )
|
|
14024
|
+
# return respone
|
|
14025
|
+
|
|
14026
|
+
# build spotify zeroconf addUser parameters.
|
|
14027
|
+
urlParms:dict = \
|
|
14028
|
+
{
|
|
14029
|
+
'userName': self.credentials.username.decode('ascii'),
|
|
14030
|
+
'clientKey': int_to_b64str(builder.dh_keys.public_key),
|
|
14031
|
+
'blob': blob,
|
|
14032
|
+
'deviceId': getInfoResponse.DeviceId,
|
|
14033
|
+
'deviceName': getInfoResponse.RemoteName,
|
|
14034
|
+
'loginId': 'xxxxxxxxxxx',
|
|
14035
|
+
}
|
|
14036
|
+
|
|
14037
|
+
# let params = [
|
|
14038
|
+
# 3 ("action", "addUser"),
|
|
14039
|
+
# 3 ("userName", username),
|
|
14040
|
+
# 3 ("blob", blob),
|
|
14041
|
+
# 3 ("clientKey", my_public_key),
|
|
14042
|
+
# ("deviceId", &device_id),
|
|
14043
|
+
# ("deviceName", "spotify-connect"),
|
|
14044
|
+
# ("loginId", &login_id),
|
|
14045
|
+
|
|
14046
|
+
|
|
14047
|
+
# execute spotify zeroconf api request.
|
|
14048
|
+
msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, actionUrl)
|
|
14049
|
+
msg.UrlParameters = urlParms
|
|
14050
|
+
self.MakeRequest('POST', msg)
|
|
14051
|
+
|
|
14052
|
+
# process results.
|
|
14053
|
+
result = ZeroconfResponse(root=msg.ResponseData)
|
|
14054
|
+
|
|
14055
|
+
# trace.
|
|
14056
|
+
_logsi.LogObject(SILevel.Verbose, TRACE_METHOD_RESULT_TYPE % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
|
|
14057
|
+
return result
|
|
14058
|
+
|
|
14059
|
+
except SpotifyWebApiError: raise # pass handled exceptions on thru
|
|
14060
|
+
except SpotifyWebApiAuthenticationError: raise # pass handled exceptions on thru
|
|
14061
|
+
except Exception as ex:
|
|
14062
|
+
|
|
14063
|
+
# format unhandled exception.
|
|
14064
|
+
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
|
|
14065
|
+
|
|
14066
|
+
finally:
|
|
14067
|
+
|
|
14068
|
+
# trace.
|
|
14069
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
@@ -3,6 +3,7 @@ try:
|
|
|
3
3
|
from queue import Queue, Empty
|
|
4
4
|
except ImportError:
|
|
5
5
|
from Queue import Queue, Empty
|
|
6
|
+
from time import sleep
|
|
6
7
|
from zeroconf import Zeroconf, ServiceBrowser, ServiceInfo, ServiceStateChange, IPVersion
|
|
7
8
|
|
|
8
9
|
# our package imports.
|
|
@@ -10,6 +11,7 @@ from .saappmessages import SAAppMessages
|
|
|
10
11
|
from .sautils import export
|
|
11
12
|
from .models.device import Device
|
|
12
13
|
from .spotifyclient import SpotifyClient, SpotifyApiError
|
|
14
|
+
from .models import ZeroconfDiscoveryResult, ZeroconfProperty
|
|
13
15
|
|
|
14
16
|
# get smartinspect logger reference; create a new session for this module name.
|
|
15
17
|
from smartinspectpython.siauto import SIAuto, SILevel, SISession
|
|
@@ -63,6 +65,7 @@ class SpotifyDiscovery:
|
|
|
63
65
|
# initialize instance properties.
|
|
64
66
|
self._AreDevicesVerified:bool = areDevicesVerified
|
|
65
67
|
self._DiscoveredDeviceNames:dict = {}
|
|
68
|
+
self._DiscoveryResults:list[ZeroconfDiscoveryResult] = []
|
|
66
69
|
self._PrintToConsole:bool = printToConsole
|
|
67
70
|
self._SpotifyClient:SpotifyClient = spotifyClient
|
|
68
71
|
self._VerifiedDevices:dict = {}
|
|
@@ -119,6 +122,15 @@ class SpotifyDiscovery:
|
|
|
119
122
|
return self._DiscoveredDeviceNames
|
|
120
123
|
|
|
121
124
|
|
|
125
|
+
@property
|
|
126
|
+
def DiscoveryResults(self) -> list[ZeroconfDiscoveryResult]:
|
|
127
|
+
"""
|
|
128
|
+
An array of `ZeroconfDiscoveryResult` items that contain discovery details for
|
|
129
|
+
each service that was discovered.
|
|
130
|
+
"""
|
|
131
|
+
return self._DiscoveryResults
|
|
132
|
+
|
|
133
|
+
|
|
122
134
|
@property
|
|
123
135
|
def VerifiedDevices(self) -> dict:
|
|
124
136
|
"""
|
|
@@ -170,6 +182,23 @@ class SpotifyDiscovery:
|
|
|
170
182
|
# create device instances for each ipv4 address found.
|
|
171
183
|
for deviceIpAddress in ipAddressList:
|
|
172
184
|
|
|
185
|
+
# create new result instance.
|
|
186
|
+
result:ZeroconfDiscoveryResult = ZeroconfDiscoveryResult()
|
|
187
|
+
result.DeviceName = deviceName
|
|
188
|
+
result.Domain = '.local'
|
|
189
|
+
result.HostIpv4Address = deviceIpAddress
|
|
190
|
+
result.HostIpPort = serviceInfo.port
|
|
191
|
+
result.HostTTL = serviceInfo.host_ttl
|
|
192
|
+
result.Key = serviceInfo.key
|
|
193
|
+
result.Name = serviceInfo.name
|
|
194
|
+
result.OtherTTL = serviceInfo.other_ttl
|
|
195
|
+
result.Priority = serviceInfo.priority
|
|
196
|
+
result.Server = serviceInfo.server
|
|
197
|
+
result.ServerKey = serviceInfo.server_key
|
|
198
|
+
result.ServiceInfo = serviceInfo
|
|
199
|
+
result.ServiceType = serviceType
|
|
200
|
+
result.Weight = serviceInfo.weight
|
|
201
|
+
|
|
173
202
|
devicePort:int = serviceInfo.port
|
|
174
203
|
deviceKey:str = '%s:%i' % (deviceIpAddress, devicePort)
|
|
175
204
|
_logsi.LogVerbose("Discovered Spotify Connect device: %s - %s" % (deviceKey, deviceName))
|
|
@@ -177,20 +206,30 @@ class SpotifyDiscovery:
|
|
|
177
206
|
print("Discovered Spotify Connect device: %s - %s" % (deviceKey, deviceName))
|
|
178
207
|
_logsi.LogObject(SILevel.Verbose, "Discovered Spotify Connect device ServiceInfo: %s - %s" % (deviceKey, deviceName), serviceInfo)
|
|
179
208
|
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
209
|
+
# process service info propertys.
|
|
210
|
+
# note that the property keys and values must first be decoded to utf-8 encoding.
|
|
211
|
+
if serviceInfo.properties is not None:
|
|
212
|
+
dspProperties:dict = {}
|
|
213
|
+
for key, value in serviceInfo.properties.items():
|
|
214
|
+
keyStr = key.decode('utf-8')
|
|
215
|
+
valueStr = value.decode('utf-8')
|
|
216
|
+
result.Properties.append(ZeroconfProperty(keyStr, valueStr))
|
|
217
|
+
dspProperties[keyStr] = valueStr
|
|
218
|
+
# check for Spotify Connect common properties.
|
|
219
|
+
if keyStr.lower() == 'cpath':
|
|
220
|
+
result.SpotifyConnectCPath = valueStr
|
|
221
|
+
elif keyStr.lower() == 'version':
|
|
222
|
+
result.SpotifyConnectVersion = valueStr
|
|
223
|
+
_logsi.LogDictionary(SILevel.Verbose, "Discovered Spotify Connect device ServiceInfo.Properties: '%s' (%s:%i)" % (deviceName, deviceIpAddress, devicePort), dspProperties)
|
|
188
224
|
|
|
189
225
|
# add the device name to the list (if not already added) using its
|
|
190
226
|
# ipv4 address and port number as the key.
|
|
191
227
|
if deviceKey not in self._DiscoveredDeviceNames.keys():
|
|
192
228
|
self._DiscoveredDeviceNames[deviceKey] = deviceName
|
|
193
229
|
|
|
230
|
+
# add the device result to the list.
|
|
231
|
+
self._DiscoveryResults.append(result)
|
|
232
|
+
|
|
194
233
|
# are we verifying devices connections? if so, then create a SpotifyClient
|
|
195
234
|
# object, which will verify the connection and gather basic capabilities of the device.
|
|
196
235
|
# we will also add the SpotifyClient instance using the same key as the device name list.
|
|
@@ -198,12 +237,15 @@ class SpotifyDiscovery:
|
|
|
198
237
|
|
|
199
238
|
if deviceKey not in self._VerifiedDevices.keys():
|
|
200
239
|
|
|
201
|
-
#device:SpotifyClient = SpotifyClient(host=deviceIpAddress, port=devicePort)
|
|
202
|
-
|
|
203
240
|
# get Spotify Connect player device by it's Name value.
|
|
204
241
|
device:Device = self._SpotifyClient.GetPlayerDevice(deviceName)
|
|
205
242
|
if device is not None:
|
|
206
243
|
self._VerifiedDevices[deviceKey] = device
|
|
244
|
+
result.SpotifyConnectIsInDeviceList = True
|
|
245
|
+
|
|
246
|
+
# trace.
|
|
247
|
+
_logsi.LogObject(SILevel.Verbose, "Discovered Spotify Connect device result: '%s' (%s:%i)" % (deviceName, deviceIpAddress, devicePort), result, excludeNonPublic=True)
|
|
248
|
+
#_logsi.LogArray(SILevel.Verbose, "Discovered Spotify Connect device result.Properties: '%s' (%s:%i)" % (deviceName, deviceIpAddress, devicePort), result.Properties)
|
|
207
249
|
|
|
208
250
|
elif (serviceStateChange is ServiceStateChange.Removed):
|
|
209
251
|
_logsi.LogVerbose("Discovered Spotify Connect device removal (ignored): '%s' (%s:%s)", serviceName, serviceType, serviceStateChange)
|
|
@@ -258,6 +300,52 @@ class SpotifyDiscovery:
|
|
|
258
300
|
return self._DiscoveredDeviceNames
|
|
259
301
|
|
|
260
302
|
|
|
303
|
+
def BroadcastService(self, serviceInfo:ServiceInfo, port:int=5353) -> None:
|
|
304
|
+
"""
|
|
305
|
+
Generate a Zeroconf / mDNS broadcast to announce a service, and sends the broadcast out.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
serviceInfo (ServiceInfo):
|
|
309
|
+
Service information to broadcast.
|
|
310
|
+
port (int):
|
|
311
|
+
mDNS port number to send the broadcast on.
|
|
312
|
+
Default is 5353.
|
|
313
|
+
"""
|
|
314
|
+
try:
|
|
315
|
+
|
|
316
|
+
# create the zeroconf service.
|
|
317
|
+
zeroconf:Zeroconf = Zeroconf(ip_version=IPVersion.V4Only)
|
|
318
|
+
|
|
319
|
+
# # TEST TODO - hardcoded serviceinfo properties for testing.
|
|
320
|
+
# serviceType:str = '_spotify-connect._tcp.local.'
|
|
321
|
+
# serviceName:str = 'Bose-ST10-95._spotify-connect._tcp.local.'
|
|
322
|
+
# parsedAddresses:list[str] = ['192.168.1.82']
|
|
323
|
+
# port:int = 8200
|
|
324
|
+
# hostTTL:int = 120
|
|
325
|
+
# priority:int = 0
|
|
326
|
+
# otherTTL:int = 4500
|
|
327
|
+
# server:str = 'Bose-SM2-987bf3244-95.local.'
|
|
328
|
+
# weight:int = 0
|
|
329
|
+
# properties:dict = {b'CPath': b'/zc', b'VERSION': b'1.0'}
|
|
330
|
+
|
|
331
|
+
# # create the zeroconf serviceinfo object.
|
|
332
|
+
# zcfServiceInfo:ServiceInfo = ServiceInfo(serviceType, serviceName, port, weight, priority, properties, server, hostTTL, otherTTL, parsed_addresses=parsedAddresses)
|
|
333
|
+
|
|
334
|
+
# generate a broadcast packet to announce the service.
|
|
335
|
+
_logsi.LogVerbose("Generating Zeroconf Service Broadcast for service: '%s'" % serviceInfo.name)
|
|
336
|
+
dnsOutgoing = zeroconf.generate_service_broadcast(serviceInfo, serviceInfo.host_ttl, True)
|
|
337
|
+
|
|
338
|
+
# send the broadcast packet.
|
|
339
|
+
_logsi.LogVerbose("Sending Zeroconf Service Broadcast for service: '%s'" % serviceInfo.name)
|
|
340
|
+
zeroconf.send(dnsOutgoing, addr=None, port=port)
|
|
341
|
+
|
|
342
|
+
except Exception as ex:
|
|
343
|
+
|
|
344
|
+
# this is not really an exception, but more of an indicator that
|
|
345
|
+
# the timeout has been reached.
|
|
346
|
+
_logsi.LogException("Zeroconf Broadcast Service exception: %s" % str(ex), ex, logToSystemLogger=False)
|
|
347
|
+
|
|
348
|
+
|
|
261
349
|
def ToString(self, includeItems:bool=False) -> str:
|
|
262
350
|
"""
|
|
263
351
|
Returns a displayable string representation of the class.
|
|
@@ -273,7 +361,6 @@ class SpotifyDiscovery:
|
|
|
273
361
|
if includeItems == True:
|
|
274
362
|
|
|
275
363
|
for key, deviceName in self._DiscoveredDeviceNames.items():
|
|
276
|
-
|
|
277
364
|
device:Device
|
|
278
365
|
verifiedStatus:str = ''
|
|
279
366
|
device = self._VerifiedDevices.get(key, None)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|