spotifywebapiPython 1.0.242__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spotifywebapipython/__init__.py +35 -0
- spotifywebapipython/const.py +114 -0
- spotifywebapipython/docs/__init__.html +2156 -0
- spotifywebapipython/docs/codecopy.js +70 -0
- spotifywebapipython/docs/index.html +514 -0
- spotifywebapipython/docs/search.js +46 -0
- spotifywebapipython/docs/spotifywebapipython/const.html +636 -0
- spotifywebapipython/docs/spotifywebapipython/saappmessages.html +540 -0
- spotifywebapipython/docs/spotifywebapipython/sautils.html +620 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +341 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +598 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +612 -0
- spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +16557 -0
- spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +518 -0
- spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +421 -0
- spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +400 -0
- spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +635 -0
- spotifywebapipython/docs/spotifywebapipython.ico +0 -0
- spotifywebapipython/docs/spotifywebapipython_OLD.ico +0 -0
- spotifywebapipython/models/__init__.py +121 -0
- spotifywebapipython/models/album.py +170 -0
- spotifywebapipython/models/albumpagesaved.py +85 -0
- spotifywebapipython/models/albumpagesimplified.py +88 -0
- spotifywebapipython/models/albumsaved.py +102 -0
- spotifywebapipython/models/albumsimplified.py +308 -0
- spotifywebapipython/models/artist.py +146 -0
- spotifywebapipython/models/artistinfo.py +282 -0
- spotifywebapipython/models/artistinfotourevent.py +127 -0
- spotifywebapipython/models/artistpage.py +70 -0
- spotifywebapipython/models/artistsimplified.py +163 -0
- spotifywebapipython/models/audiobook.py +87 -0
- spotifywebapipython/models/audiobookpagesimplified.py +88 -0
- spotifywebapipython/models/audiobooksimplified.py +362 -0
- spotifywebapipython/models/audiofeatures.py +333 -0
- spotifywebapipython/models/author.py +67 -0
- spotifywebapipython/models/category.py +168 -0
- spotifywebapipython/models/categorypage.py +70 -0
- spotifywebapipython/models/chapter.py +84 -0
- spotifywebapipython/models/chapterpagesimplified.py +88 -0
- spotifywebapipython/models/chaptersimplified.py +387 -0
- spotifywebapipython/models/context.py +113 -0
- spotifywebapipython/models/copyright.py +79 -0
- spotifywebapipython/models/device.py +301 -0
- spotifywebapipython/models/episode.py +84 -0
- spotifywebapipython/models/episodepagesaved.py +85 -0
- spotifywebapipython/models/episodepagesimplified.py +88 -0
- spotifywebapipython/models/episodesaved.py +99 -0
- spotifywebapipython/models/episodesimplified.py +370 -0
- spotifywebapipython/models/explicitcontent.py +80 -0
- spotifywebapipython/models/externalids.py +91 -0
- spotifywebapipython/models/externalurls.py +104 -0
- spotifywebapipython/models/followers.py +81 -0
- spotifywebapipython/models/imageobject.py +193 -0
- spotifywebapipython/models/imagevibrantcolors.py +182 -0
- spotifywebapipython/models/linkedfrom.py +165 -0
- spotifywebapipython/models/narrator.py +67 -0
- spotifywebapipython/models/owner.py +162 -0
- spotifywebapipython/models/pageobject.py +343 -0
- spotifywebapipython/models/playeractions.py +196 -0
- spotifywebapipython/models/playerlastplayedinfo.py +334 -0
- spotifywebapipython/models/playerplaystate.py +467 -0
- spotifywebapipython/models/playerqueueinfo.py +207 -0
- spotifywebapipython/models/playhistory.py +136 -0
- spotifywebapipython/models/playhistorypage.py +85 -0
- spotifywebapipython/models/playlist.py +125 -0
- spotifywebapipython/models/playlistpage.py +85 -0
- spotifywebapipython/models/playlistpagesimplified.py +107 -0
- spotifywebapipython/models/playlistsimplified.py +334 -0
- spotifywebapipython/models/playlisttrack.py +143 -0
- spotifywebapipython/models/playlisttracksummary.py +80 -0
- spotifywebapipython/models/recommendationseed.py +135 -0
- spotifywebapipython/models/restrictions.py +86 -0
- spotifywebapipython/models/resumepoint.py +79 -0
- spotifywebapipython/models/searchresponse.py +391 -0
- spotifywebapipython/models/searchresultbase.py +299 -0
- spotifywebapipython/models/show.py +87 -0
- spotifywebapipython/models/showpagesaved.py +85 -0
- spotifywebapipython/models/showpagesimplified.py +88 -0
- spotifywebapipython/models/showsaved.py +99 -0
- spotifywebapipython/models/showsimplified.py +320 -0
- spotifywebapipython/models/spotifyconnectdevice.py +321 -0
- spotifywebapipython/models/spotifyconnectdevices.py +441 -0
- spotifywebapipython/models/track.py +142 -0
- spotifywebapipython/models/trackpage.py +110 -0
- spotifywebapipython/models/trackpagesaved.py +85 -0
- spotifywebapipython/models/trackpagesimplified.py +88 -0
- spotifywebapipython/models/trackrecommendations.py +88 -0
- spotifywebapipython/models/tracksaved.py +99 -0
- spotifywebapipython/models/tracksimplified.py +398 -0
- spotifywebapipython/models/userprofile.py +184 -0
- spotifywebapipython/models/userprofilesimplified.py +206 -0
- spotifywebapipython/models/zeroconfdiscoveryresult.py +592 -0
- spotifywebapipython/models/zeroconfproperty.py +93 -0
- spotifywebapipython/oauthcli/__init__.py +7 -0
- spotifywebapipython/oauthcli/authclient.py +1229 -0
- spotifywebapipython/saappmessages.py +86 -0
- spotifywebapipython/sautils.py +285 -0
- spotifywebapipython/spotifyapierror.py +66 -0
- spotifywebapipython/spotifyapimessage.py +269 -0
- spotifywebapipython/spotifyauthtoken.py +264 -0
- spotifywebapipython/spotifyclient.py +17885 -0
- spotifywebapipython/spotifyconnect/__init__.py +19 -0
- spotifywebapipython/spotifyconnect/spotifyconnectdeviceeventargs.py +67 -0
- spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +2564 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastapptask.py +467 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastcontroller.py +701 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastlistener.py +359 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfcastmultizonelistener.py +212 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconfexceptions.py +20 -0
- spotifywebapipython/spotifyconnect/spotifyconnectzeroconflistener.py +335 -0
- spotifywebapipython/spotifydiscovery.py +473 -0
- spotifywebapipython/spotifymediatypes.py +53 -0
- spotifywebapipython/spotifywebapiauthenticationerror.py +122 -0
- spotifywebapipython/spotifywebapierror.py +106 -0
- spotifywebapipython/spotifywebplayertoken.py +743 -0
- spotifywebapipython/vibrant/__init__.py +17 -0
- spotifywebapipython/vibrant/generator.py +261 -0
- spotifywebapipython/vibrant/image.py +100 -0
- spotifywebapipython/vibrant/main.py +34 -0
- spotifywebapipython/vibrant/models.py +93 -0
- spotifywebapipython/vibrant/utils.py +56 -0
- spotifywebapipython/zeroconfapi/__init__.py +23 -0
- spotifywebapipython/zeroconfapi/authenticationtypes.py +38 -0
- spotifywebapipython/zeroconfapi/blobbuilder.py +370 -0
- spotifywebapipython/zeroconfapi/credentials.py +37 -0
- spotifywebapipython/zeroconfapi/cryptodiffiehellman.py +89 -0
- spotifywebapipython/zeroconfapi/helpers.py +64 -0
- spotifywebapipython/zeroconfapi/spotifyzeroconfapierror.py +108 -0
- spotifywebapipython/zeroconfapi/zeroconfconnect.py +1327 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfo.py +572 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfoalias.py +109 -0
- spotifywebapipython/zeroconfapi/zeroconfgetinfodrmmediaformat.py +90 -0
- spotifywebapipython/zeroconfapi/zeroconfresponse.py +195 -0
- spotifywebapipython-1.0.242.dist-info/LICENSE +22 -0
- spotifywebapipython-1.0.242.dist-info/METADATA +219 -0
- spotifywebapipython-1.0.242.dist-info/RECORD +138 -0
- spotifywebapipython-1.0.242.dist-info/WHEEL +5 -0
- spotifywebapipython-1.0.242.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
# external package imports.
|
|
2
|
+
from pychromecast import CastInfo, HostServiceInfo
|
|
3
|
+
from pychromecast.discovery import AbstractCastListener
|
|
4
|
+
import threading
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
# our package imports.
|
|
8
|
+
from spotifywebapipython.models import ZeroconfProperty, ZeroconfDiscoveryResult
|
|
9
|
+
# note - cannot reference `SpotifyConnectDirectoryTask` due to circular import!
|
|
10
|
+
# from .spotifyconnectdirectorytask import SpotifyConnectDirectoryTask
|
|
11
|
+
|
|
12
|
+
# get smartinspect logger reference; create a new session for this module name.
|
|
13
|
+
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SIColors
|
|
14
|
+
import logging
|
|
15
|
+
|
|
16
|
+
_logsi:SISession = SIAuto.Si.GetSession(__name__)
|
|
17
|
+
if (_logsi == None):
|
|
18
|
+
_logsi = SIAuto.Si.AddSession(__name__, True)
|
|
19
|
+
_logsi.SystemLogger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
ZEROCONF_SERVICETYPE_GOOGLECAST:str = "_googlecast._tcp.local."
|
|
22
|
+
"""
|
|
23
|
+
Googlecast Zeroconf service type identifier (e.g. "_googlecast._tcp.local.").
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# chromecast types supported.
|
|
27
|
+
CAST_TYPE_CHROMECAST = "cast"
|
|
28
|
+
""" Regular chromecast, supports video/audio """
|
|
29
|
+
CAST_TYPE_AUDIO = "audio"
|
|
30
|
+
""" Cast Audio device, supports only audio """
|
|
31
|
+
CAST_TYPE_GROUP = "group"
|
|
32
|
+
""" Cast Audio group device, supports only audio """
|
|
33
|
+
CAST_TYPE_NONE = None
|
|
34
|
+
""" cast_type not set; assume it supports audio """
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SpotifyConnectZeroconfCastListener(AbstractCastListener):
|
|
38
|
+
"""
|
|
39
|
+
Google Chromecast Zeroconf Listener class.
|
|
40
|
+
|
|
41
|
+
Listens for Chromecast device connection updates for devices that
|
|
42
|
+
support Spotify Connect.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
parentDirectory,
|
|
48
|
+
zeroconf_RLock:threading.RLock,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Initializes a new instance of the class.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
parentDirectory (SpotifyConnectDirectoryTask):
|
|
55
|
+
Parent SpotifyConnectDirectoryTask instance.
|
|
56
|
+
zeroconf_RLock (threading.RLock):
|
|
57
|
+
Lock object used to enforce thread-safe updates.
|
|
58
|
+
"""
|
|
59
|
+
# invoke base class method.
|
|
60
|
+
super().__init__()
|
|
61
|
+
|
|
62
|
+
# initialize storage.
|
|
63
|
+
self._ParentDirectory = parentDirectory
|
|
64
|
+
self._Zeroconf_RLock:threading.RLock = zeroconf_RLock
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _GetZeroconfDiscoveryResult(
|
|
68
|
+
self,
|
|
69
|
+
uuid:UUID,
|
|
70
|
+
serviceName:str,
|
|
71
|
+
castInfo_removed:CastInfo = None
|
|
72
|
+
) -> ZeroconfDiscoveryResult:
|
|
73
|
+
"""
|
|
74
|
+
Builds a `ZeroconfDiscoveryResult` instance from Zeroconf CastInfo data
|
|
75
|
+
when an add or update service state change takes place.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
uuid (UUID):
|
|
79
|
+
The cast's uuid, which is the dictionary key to find the chromecast
|
|
80
|
+
metadata in CastBrowser.devices collection.
|
|
81
|
+
serviceName (str):
|
|
82
|
+
First known MDNS service name or host:port.
|
|
83
|
+
castInfo_removed (CastInfo):
|
|
84
|
+
CastInfo for the service to aid cleanup; only supplied for removal
|
|
85
|
+
requests since the CastBrowser.services collection no longer contains
|
|
86
|
+
a CastInfo object for the specified uuid.
|
|
87
|
+
"""
|
|
88
|
+
result:ZeroconfDiscoveryResult = None
|
|
89
|
+
castInfoHost:str = None
|
|
90
|
+
castInfoPort:int = 0
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
|
|
94
|
+
# is this an MDNSServiceInfo service record? we only want discovery results
|
|
95
|
+
# that contain MDNSServiceInfo service information.
|
|
96
|
+
if (serviceName.find(ZEROCONF_SERVICETYPE_GOOGLECAST) == -1):
|
|
97
|
+
_logsi.LogDebug("Chromecast Zeroconf discovery service notification: \"%s\" (%s)" % (serviceName, "ignored; not MDNSInfo"))
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
# get chromecast info.
|
|
101
|
+
castInfo:CastInfo = castInfo_removed
|
|
102
|
+
if (castInfo is None):
|
|
103
|
+
|
|
104
|
+
# map the CastInfo object.
|
|
105
|
+
castInfo:CastInfo = self._ParentDirectory._CastBrowser.services[uuid]
|
|
106
|
+
castInfoHost = castInfo.host
|
|
107
|
+
castInfoPort = castInfo.port
|
|
108
|
+
|
|
109
|
+
# 20250628 commented out the ipv6 address resolution code.
|
|
110
|
+
# the `get_multizone_status` (in directorytask) could possibly return ipv6 info, with no ipv4 data!
|
|
111
|
+
|
|
112
|
+
# # find ipv4 host and port number.
|
|
113
|
+
# # if the base CastInfo object is ipv6, then find the ipv4 HostServiceInfo
|
|
114
|
+
# # service record and get it from there.
|
|
115
|
+
# if ((castInfoHost + "").find(":") != -1):
|
|
116
|
+
|
|
117
|
+
# # cast info host information is ipv6!
|
|
118
|
+
# # find the ipv4 host service info object.
|
|
119
|
+
# _logsi.LogDebug("Chromecast Zeroconf discovery service notification contained IPV6 HostServiceInfo data: \"%s\" (%s)" % (serviceName, "searching for IPV4 info"), colorValue=SIColors.Coral)
|
|
120
|
+
# castHostIpv4:HostServiceInfo = None
|
|
121
|
+
# for castSvc in castInfo.services:
|
|
122
|
+
# if (isinstance(castSvc, HostServiceInfo)):
|
|
123
|
+
# if ((castSvc.host + "").find(":") == -1):
|
|
124
|
+
# castInfoHost = castSvc.host
|
|
125
|
+
# castInfoPort = castSvc.port
|
|
126
|
+
# castHostIpv4 = castSvc
|
|
127
|
+
# break
|
|
128
|
+
|
|
129
|
+
# # if ipv4 host service info not found then we are done, as Spotify Connect
|
|
130
|
+
# # zeroconf requires an IPV4 address structure.
|
|
131
|
+
# if (castHostIpv4 is None):
|
|
132
|
+
# _logsi.LogDebug("Chromecast Zeroconf discovery service notification did not contain IPV4 HostServiceInfo service entry: \"%s\" (%s)" % (serviceName, "IPV6 HostServiceInfo may fail"), colorValue=SIColors.Coral)
|
|
133
|
+
|
|
134
|
+
else:
|
|
135
|
+
|
|
136
|
+
# set host and port number for remove request.
|
|
137
|
+
castInfoHost = castInfo.host
|
|
138
|
+
castInfoPort = castInfo.port
|
|
139
|
+
|
|
140
|
+
# trace.
|
|
141
|
+
_logsi.LogObject(SILevel.Debug, "Chromecast Zeroconf service details: \"%s\" (%s) (CastInfo object)" % (castInfo.friendly_name, serviceName), castInfo)
|
|
142
|
+
|
|
143
|
+
# only certain cast_type values support Spotify Connect.
|
|
144
|
+
castType:str = castInfo.cast_type
|
|
145
|
+
if (not (castType in [CAST_TYPE_AUDIO, CAST_TYPE_GROUP, CAST_TYPE_CHROMECAST, CAST_TYPE_NONE])):
|
|
146
|
+
_logsi.LogDebug("Chromecast device cast_type of \"%s\" is not supported; ignoring: \"%s\" (%s)" % (castType, castInfo.friendly_name, serviceName), colorValue=SIColors.Coral)
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
# create new discovery result instance.
|
|
150
|
+
result:ZeroconfDiscoveryResult = ZeroconfDiscoveryResult()
|
|
151
|
+
result.DeviceName = castInfo.friendly_name
|
|
152
|
+
result.Domain = ".local"
|
|
153
|
+
result.HostIpAddresses = [castInfoHost]
|
|
154
|
+
result.HostIpPort = castInfoPort
|
|
155
|
+
result.HostTTL = 120
|
|
156
|
+
result.IsChromeCast = True
|
|
157
|
+
result.Key = str(uuid)
|
|
158
|
+
result.Name = serviceName
|
|
159
|
+
result.Priority = 0
|
|
160
|
+
result.OtherTTL = 4500
|
|
161
|
+
result.Server = serviceName
|
|
162
|
+
result.ServerKey = serviceName
|
|
163
|
+
result.ServiceType = ZEROCONF_SERVICETYPE_GOOGLECAST
|
|
164
|
+
result.Weight = 0
|
|
165
|
+
result.Properties.append(ZeroconfProperty("CPath", "/na"))
|
|
166
|
+
result.Properties.append(ZeroconfProperty("VERSION", "1.0"))
|
|
167
|
+
result.Properties.append(ZeroconfProperty("cast_type", castType))
|
|
168
|
+
result.SpotifyConnectCPath = "/na"
|
|
169
|
+
result.SpotifyConnectVersion = "1.0"
|
|
170
|
+
result.Id = "\"%s\" (%s:%s)" % (result.DeviceName, result.HostIpAddress, result.HostIpPort)
|
|
171
|
+
|
|
172
|
+
# trace.
|
|
173
|
+
_logsi.LogObject(SILevel.Debug, "Chromecast Zeroconf Discovery Result: %s (object)" % (result.Id), result, excludeNonPublic=True)
|
|
174
|
+
|
|
175
|
+
# return discovery result.
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
except Exception as ex:
|
|
179
|
+
|
|
180
|
+
# trace.
|
|
181
|
+
_logsi.LogException("Could not load ZeroconfDiscoveryResult instance from Chromecast Zeroconf service details", ex, logToSystemLogger=False)
|
|
182
|
+
|
|
183
|
+
# ignore exception
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def add_cast(
|
|
188
|
+
self,
|
|
189
|
+
uuid:UUID,
|
|
190
|
+
serviceName:str
|
|
191
|
+
) -> None:
|
|
192
|
+
"""
|
|
193
|
+
Called when a new cast has been discovered.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
uuid (UUID):
|
|
197
|
+
The cast's uuid, which is the dictionary key to find the chromecast
|
|
198
|
+
metadata in CastBrowser.devices collection.
|
|
199
|
+
serviceName (str):
|
|
200
|
+
First known MDNS service name or host:port.
|
|
201
|
+
"""
|
|
202
|
+
# use lock, as multiple threads could be calling this method simultaneously.
|
|
203
|
+
with self._Zeroconf_RLock:
|
|
204
|
+
|
|
205
|
+
# copy passed parameters to thread-safe storage.
|
|
206
|
+
argsUuid:UUID = uuid
|
|
207
|
+
argsServiceName:str = serviceName
|
|
208
|
+
|
|
209
|
+
apiMethodName:str = "add_cast"
|
|
210
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
|
|
214
|
+
# trace.
|
|
215
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
216
|
+
apiMethodParms.AppendKeyValue("uuid", argsUuid)
|
|
217
|
+
apiMethodParms.AppendKeyValue("serviceName", argsServiceName)
|
|
218
|
+
_logsi.LogMethodParmList(SILevel.Debug, "Chromecast Zeroconf discovery service notification: \"%s\" (%s)" % (argsServiceName, apiMethodName), apiMethodParms)
|
|
219
|
+
|
|
220
|
+
# build discovery result instance from service state information.
|
|
221
|
+
# if nothing returned, then don't bother!
|
|
222
|
+
result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(argsUuid, argsServiceName, None)
|
|
223
|
+
if (result is None):
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
# let the parent handle the event.
|
|
227
|
+
self._ParentDirectory.OnServiceInfoAddedUpdatedChromecast(result, argsUuid, argsServiceName, apiMethodName)
|
|
228
|
+
|
|
229
|
+
except Exception as ex:
|
|
230
|
+
|
|
231
|
+
# trace.
|
|
232
|
+
_logsi.LogException("Unhandled exception occured while processing Chromecast Zeroconf service browser ServiceStateChange notification (%s)" % (apiMethodName), ex, logToSystemLogger=False)
|
|
233
|
+
|
|
234
|
+
# ignore exception, as nothing can be done about it.
|
|
235
|
+
|
|
236
|
+
finally:
|
|
237
|
+
|
|
238
|
+
# trace.
|
|
239
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def remove_cast(
|
|
243
|
+
self,
|
|
244
|
+
uuid:UUID,
|
|
245
|
+
serviceName:str,
|
|
246
|
+
castInfo:CastInfo
|
|
247
|
+
) -> None:
|
|
248
|
+
"""
|
|
249
|
+
Called when a cast has been lost (MDNS info expired or host down).
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
uuid (UUID):
|
|
253
|
+
The cast's uuid, which is the dictionary key to find the chromecast
|
|
254
|
+
metadata in CastBrowser.devices collection.
|
|
255
|
+
serviceName (str):
|
|
256
|
+
Last valid MDNS service name or host:port.
|
|
257
|
+
castInfo (CastInfo):
|
|
258
|
+
CastInfo for the service to aid cleanup.
|
|
259
|
+
"""
|
|
260
|
+
# use lock, as multiple threads could be calling this method simultaneously.
|
|
261
|
+
with self._Zeroconf_RLock:
|
|
262
|
+
|
|
263
|
+
# copy passed parameters to thread-safe storage.
|
|
264
|
+
argsUuid:UUID = uuid
|
|
265
|
+
argsServiceName:str = serviceName
|
|
266
|
+
argsCastInfo:CastInfo = castInfo
|
|
267
|
+
|
|
268
|
+
apiMethodName:str = "remove_cast"
|
|
269
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
|
|
273
|
+
# trace.
|
|
274
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
275
|
+
apiMethodParms.AppendKeyValue("uuid", argsUuid)
|
|
276
|
+
apiMethodParms.AppendKeyValue("serviceName", argsServiceName)
|
|
277
|
+
_logsi.LogMethodParmList(SILevel.Debug, "Chromecast Zeroconf discovery service notification: \"%s\" (%s)" % (argsServiceName, apiMethodName), apiMethodParms)
|
|
278
|
+
|
|
279
|
+
# for service removal notifications, there will be no zeroconf service info.
|
|
280
|
+
# build discovery result instance from service state information.
|
|
281
|
+
# if nothing returned, then it's ok - build a minimal discovery instance that
|
|
282
|
+
# will contain the necessary details to remove the device entry.
|
|
283
|
+
result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(argsUuid, argsServiceName, argsCastInfo)
|
|
284
|
+
if (result is None):
|
|
285
|
+
result = ZeroconfDiscoveryResult()
|
|
286
|
+
result.DeviceName = castInfo.friendly_name
|
|
287
|
+
result.Key = str(uuid)
|
|
288
|
+
result.Name = serviceName
|
|
289
|
+
result.ServiceType = ZEROCONF_SERVICETYPE_GOOGLECAST
|
|
290
|
+
|
|
291
|
+
# let the parent handle the event.
|
|
292
|
+
self._ParentDirectory.OnServiceInfoRemovedChromecast(result, argsUuid, argsServiceName, argsCastInfo)
|
|
293
|
+
|
|
294
|
+
except Exception as ex:
|
|
295
|
+
|
|
296
|
+
# trace.
|
|
297
|
+
_logsi.LogException("Unhandled exception occured while processing Chromecast Zeroconf service browser ServiceStateChange notification (%s)" % (apiMethodName), ex, logToSystemLogger=False)
|
|
298
|
+
|
|
299
|
+
# ignore exception, as nothing can be done about it.
|
|
300
|
+
|
|
301
|
+
finally:
|
|
302
|
+
|
|
303
|
+
# trace.
|
|
304
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def update_cast(
|
|
308
|
+
self,
|
|
309
|
+
uuid:UUID,
|
|
310
|
+
serviceName:str
|
|
311
|
+
) -> None:
|
|
312
|
+
"""
|
|
313
|
+
Called when a cast has been updated (MDNS info renewed or changed).
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
uuid (UUID):
|
|
317
|
+
The cast's uuid, which is the dictionary key to find the chromecast
|
|
318
|
+
metadata in CastBrowser.devices collection.
|
|
319
|
+
serviceName (str):
|
|
320
|
+
MDNS service name or host:port.
|
|
321
|
+
"""
|
|
322
|
+
# use lock, as multiple threads could be calling this method simultaneously.
|
|
323
|
+
with self._Zeroconf_RLock:
|
|
324
|
+
|
|
325
|
+
# copy passed parameters to thread-safe storage.
|
|
326
|
+
argsUuid:UUID = uuid
|
|
327
|
+
argsServiceName:str = serviceName
|
|
328
|
+
|
|
329
|
+
apiMethodName:str = "update_cast"
|
|
330
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
331
|
+
|
|
332
|
+
try:
|
|
333
|
+
|
|
334
|
+
# trace.
|
|
335
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
336
|
+
apiMethodParms.AppendKeyValue("uuid", argsUuid)
|
|
337
|
+
apiMethodParms.AppendKeyValue("serviceName", argsServiceName)
|
|
338
|
+
_logsi.LogMethodParmList(SILevel.Debug, "Chromecast Zeroconf discovery service notification: \"%s\" (%s)" % (serviceName, apiMethodName), apiMethodParms)
|
|
339
|
+
|
|
340
|
+
# build discovery result instance from service state information.
|
|
341
|
+
# if nothing returned, then don't bother!
|
|
342
|
+
result:ZeroconfDiscoveryResult = self._GetZeroconfDiscoveryResult(argsUuid, argsServiceName, None)
|
|
343
|
+
if (result is None):
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
# let the parent handle the event.
|
|
347
|
+
self._ParentDirectory.OnServiceInfoAddedUpdatedChromecast(result, argsUuid, argsServiceName, apiMethodName)
|
|
348
|
+
|
|
349
|
+
except Exception as ex:
|
|
350
|
+
|
|
351
|
+
# trace.
|
|
352
|
+
_logsi.LogException("Unhandled exception occured while processing Chromecast Zeroconf service browser ServiceStateChange notification (%s)" % (apiMethodName), ex, logToSystemLogger=False)
|
|
353
|
+
|
|
354
|
+
# ignore exception, as nothing can be done about it.
|
|
355
|
+
|
|
356
|
+
finally:
|
|
357
|
+
|
|
358
|
+
# trace.
|
|
359
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# external package imports.
|
|
2
|
+
from pychromecast.controllers.multizone import MultizoneController, MultiZoneControllerListener
|
|
3
|
+
from pychromecast.dial import MultizoneStatus, get_multizone_status
|
|
4
|
+
from pychromecast import Chromecast
|
|
5
|
+
import pychromecast
|
|
6
|
+
import threading
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
from zeroconf import Zeroconf
|
|
9
|
+
|
|
10
|
+
# our package imports.
|
|
11
|
+
from spotifywebapipython.models import SpotifyConnectDevice, ZeroconfDiscoveryResult
|
|
12
|
+
# note - cannot reference `SpotifyConnectDirectoryTask` due to circular import!
|
|
13
|
+
# from .spotifyconnectdirectorytask import SpotifyConnectDirectoryTask
|
|
14
|
+
|
|
15
|
+
# get smartinspect logger reference; create a new session for this module name.
|
|
16
|
+
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SIColors
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
_logsi:SISession = SIAuto.Si.GetSession(__name__)
|
|
20
|
+
if (_logsi == None):
|
|
21
|
+
_logsi = SIAuto.Si.AddSession(__name__, True)
|
|
22
|
+
_logsi.SystemLogger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SpotifyConnectZeroconfCastMultiZoneListener(MultiZoneControllerListener):
|
|
26
|
+
"""
|
|
27
|
+
Google Chromecast Zeroconf MultiZone Listener class.
|
|
28
|
+
|
|
29
|
+
Listens for Chromecast device multi-zone status updates for devices that
|
|
30
|
+
support multi-zone / group functionality.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
parentDirectory,
|
|
36
|
+
zeroconfInstance:Zeroconf,
|
|
37
|
+
zeroconf_RLock:threading.RLock,
|
|
38
|
+
castMultizoneController:MultizoneController,
|
|
39
|
+
castDevice:Chromecast,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Initializes a new instance of the class.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
parentDirectory (SpotifyConnectDirectoryTask):
|
|
46
|
+
Parent SpotifyConnectDirectoryTask instance.
|
|
47
|
+
zeroconfInstance (Zeroconf):
|
|
48
|
+
Zeroconf instance used to query zeroconf.
|
|
49
|
+
zeroconf_RLock (threading.RLock):
|
|
50
|
+
Lock object used to enforce thread-safe updates.
|
|
51
|
+
castMultizoneController (MultizoneController)
|
|
52
|
+
MultizoneController parent instance.
|
|
53
|
+
castDevice (Chromecast):
|
|
54
|
+
Chromecast device object.
|
|
55
|
+
"""
|
|
56
|
+
# invoke base class method.
|
|
57
|
+
super().__init__()
|
|
58
|
+
|
|
59
|
+
# initialize storage.
|
|
60
|
+
self._CastMultizoneController:MultizoneController = castMultizoneController
|
|
61
|
+
self._CastDevice = castDevice
|
|
62
|
+
self._ParentDirectory = parentDirectory
|
|
63
|
+
self._ZeroconfInstance:Zeroconf = zeroconfInstance
|
|
64
|
+
self._Zeroconf_RLock:threading.RLock = zeroconf_RLock
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def multizone_member_added(
|
|
68
|
+
self,
|
|
69
|
+
group_uuid:str,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Called when cast has been added to group identified by group_uuid.
|
|
73
|
+
"""
|
|
74
|
+
# use lock, as multiple threads could be calling this method simultaneously.
|
|
75
|
+
with self._Zeroconf_RLock:
|
|
76
|
+
|
|
77
|
+
# copy passed parameters to thread-safe storage.
|
|
78
|
+
argsGroupUuid:UUID = group_uuid
|
|
79
|
+
|
|
80
|
+
apiMethodName:str = "multizone_member_added"
|
|
81
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
|
|
85
|
+
# trace.
|
|
86
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
87
|
+
apiMethodParms.AppendKeyValue("group_uuid", argsGroupUuid)
|
|
88
|
+
_logsi.LogMethodParmList(SILevel.Debug, "Chromecast multizone group notification was received for group \"%s\"" % (argsGroupUuid), apiMethodParms, colorValue=SIColors.Lavender)
|
|
89
|
+
|
|
90
|
+
# get device instance.
|
|
91
|
+
scDevice:SpotifyConnectDevice = self._ParentDirectory._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(argsGroupUuid))
|
|
92
|
+
if (scDevice is None):
|
|
93
|
+
|
|
94
|
+
# trace.
|
|
95
|
+
_logsi.LogVerbose("Could not find SpotifyConnectDevices instance for Cast Group discovery key: \"%s\"" % (str(argsGroupUuid)), colorValue=SIColors.Lavender)
|
|
96
|
+
|
|
97
|
+
else:
|
|
98
|
+
|
|
99
|
+
# trace.
|
|
100
|
+
_logsi.LogVerbose("Chromecast member device %s was added to multizone group \"%s\"" % (scDevice.Title, self._CastDevice.cast_info.friendly_name), colorValue=SIColors.Lavender)
|
|
101
|
+
|
|
102
|
+
# dump the multizone status for the newly added member.
|
|
103
|
+
castMultiZoneStatus:MultizoneStatus = get_multizone_status(scDevice.DiscoveryResult.HostIpAddress, self._CastDevice.cast_info.services, self._ZeroconfInstance, 5)
|
|
104
|
+
_logsi.LogObject(SILevel.Verbose, "Chromecast member device multizone status: %s [%s] (get_multizone_status)" % (scDevice.Title, scDevice.DiscoveryResult.HostIpTitle), castMultiZoneStatus, colorValue=SIColors.Lavender)
|
|
105
|
+
|
|
106
|
+
except Exception as ex:
|
|
107
|
+
|
|
108
|
+
# trace.
|
|
109
|
+
_logsi.LogException("Unhandled exception occured while processing Chromecast MultiZoneController notification (%s)" % (apiMethodName), ex, logToSystemLogger=False)
|
|
110
|
+
|
|
111
|
+
# ignore exception, as nothing can be done about it.
|
|
112
|
+
|
|
113
|
+
finally:
|
|
114
|
+
|
|
115
|
+
# trace.
|
|
116
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def multizone_member_removed(
|
|
120
|
+
self,
|
|
121
|
+
group_uuid,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Called when cast has been removed from group identified by group_uuid.
|
|
125
|
+
"""
|
|
126
|
+
# use lock, as multiple threads could be calling this method simultaneously.
|
|
127
|
+
with self._Zeroconf_RLock:
|
|
128
|
+
|
|
129
|
+
# copy passed parameters to thread-safe storage.
|
|
130
|
+
argsGroupUuid:UUID = group_uuid
|
|
131
|
+
|
|
132
|
+
apiMethodName:str = "multizone_member_removed"
|
|
133
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
|
|
137
|
+
# trace.
|
|
138
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
139
|
+
apiMethodParms.AppendKeyValue("group_uuid", argsGroupUuid)
|
|
140
|
+
_logsi.LogMethodParmList(SILevel.Debug, "Chromecast multizone group notification was received for group \"%s\"" % (argsGroupUuid), apiMethodParms, colorValue=SIColors.Lavender)
|
|
141
|
+
|
|
142
|
+
# get device instance.
|
|
143
|
+
scDevice:SpotifyConnectDevice = self._ParentDirectory._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(argsGroupUuid))
|
|
144
|
+
if (scDevice is None):
|
|
145
|
+
|
|
146
|
+
# trace.
|
|
147
|
+
_logsi.LogVerbose("Could not find SpotifyConnectDevices instance for Cast Group discovery key: \"%s\"" % (str(argsGroupUuid)), colorValue=SIColors.Lavender)
|
|
148
|
+
|
|
149
|
+
else:
|
|
150
|
+
|
|
151
|
+
# trace.
|
|
152
|
+
_logsi.LogVerbose("Chromecast member device %s was removed from multizone group \"%s\"" % (scDevice.Title, self._CastDevice.cast_info.friendly_name), colorValue=SIColors.Lavender)
|
|
153
|
+
|
|
154
|
+
except Exception as ex:
|
|
155
|
+
|
|
156
|
+
# trace.
|
|
157
|
+
_logsi.LogException("Unhandled exception occured while processing Chromecast MultiZoneController notification (%s)" % (apiMethodName), ex, logToSystemLogger=False)
|
|
158
|
+
|
|
159
|
+
# ignore exception, as nothing can be done about it.
|
|
160
|
+
|
|
161
|
+
finally:
|
|
162
|
+
|
|
163
|
+
# trace.
|
|
164
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def multizone_status_received(
|
|
168
|
+
self,
|
|
169
|
+
) -> None:
|
|
170
|
+
"""
|
|
171
|
+
Called when Multizone status has been updated.
|
|
172
|
+
"""
|
|
173
|
+
# use lock, as multiple threads could be calling this method simultaneously.
|
|
174
|
+
with self._Zeroconf_RLock:
|
|
175
|
+
|
|
176
|
+
# copy passed parameters to thread-safe storage.
|
|
177
|
+
# n/a
|
|
178
|
+
|
|
179
|
+
apiMethodName:str = "multizone_status_received"
|
|
180
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
|
|
184
|
+
# trace.
|
|
185
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
186
|
+
_logsi.LogMethodParmList(SILevel.Debug, "Chromecast multizone group status has been updated", apiMethodParms, colorValue=SIColors.Lavender)
|
|
187
|
+
|
|
188
|
+
# trace.
|
|
189
|
+
if (_logsi.IsOn(SILevel.Verbose)):
|
|
190
|
+
|
|
191
|
+
# build member list with friendly names.
|
|
192
|
+
result:list[str] = []
|
|
193
|
+
member:str = None
|
|
194
|
+
for member in self._CastMultizoneController.members:
|
|
195
|
+
scDevice:SpotifyConnectDevice = self._ParentDirectory._SpotifyConnectDevices.GetDeviceByDiscoveryKey(member)
|
|
196
|
+
if (scDevice is None):
|
|
197
|
+
result.append(member)
|
|
198
|
+
else:
|
|
199
|
+
result.append(scDevice.Title)
|
|
200
|
+
_logsi.LogArray(SILevel.Verbose, "Chromecast multizone group \"%s\" members (count=%d)" % (self._CastDevice.cast_info.friendly_name, len(result)), result, colorValue=SIColors.Lavender)
|
|
201
|
+
|
|
202
|
+
except Exception as ex:
|
|
203
|
+
|
|
204
|
+
# trace.
|
|
205
|
+
_logsi.LogException("Unhandled exception occured while processing Chromecast MultiZoneController notification (%s)" % (apiMethodName), ex, logToSystemLogger=False)
|
|
206
|
+
|
|
207
|
+
# ignore exception, as nothing can be done about it.
|
|
208
|
+
|
|
209
|
+
finally:
|
|
210
|
+
|
|
211
|
+
# trace.
|
|
212
|
+
_logsi.LeaveMethod(SILevel.Debug, apiMethodName)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# our package imports.
|
|
2
|
+
from spotifywebapipython import SpotifyApiError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class SpotifyConnectZeroconfLaunchError(Exception):
|
|
6
|
+
"""
|
|
7
|
+
Exception raised when a cast app fails to launch.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SpotifyConnectZeroconfPlaybackTransferError(Exception):
|
|
12
|
+
"""
|
|
13
|
+
Exception raised when a cast app fails to receive transfer of playback control.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SpotifyConnectDeviceNotFound(SpotifyApiError):
|
|
18
|
+
"""
|
|
19
|
+
Exception raised when a Spotify Connect device could not be resolved.
|
|
20
|
+
"""
|