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,2564 @@
|
|
|
1
|
+
# external package imports.
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
import copy
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import hashlib
|
|
7
|
+
from pychromecast import CastBrowser, CastInfo, Chromecast, get_chromecast_from_cast_info, get_chromecast_from_host
|
|
8
|
+
from pychromecast.dial import MultizoneInfo, MultizoneStatus, get_multizone_status
|
|
9
|
+
from pychromecast.controllers.multizone import MultizoneController
|
|
10
|
+
from pychromecast.const import CAST_TYPE_GROUP
|
|
11
|
+
import socket
|
|
12
|
+
from soco import SoCo
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from uuid import UUID
|
|
16
|
+
from zeroconf import Zeroconf, ServiceBrowser
|
|
17
|
+
|
|
18
|
+
# our package imports.
|
|
19
|
+
from .spotifyconnectzeroconfexceptions import SpotifyConnectDeviceNotFound
|
|
20
|
+
from .spotifyconnectzeroconfcastapptask import SpotifyConnectZeroconfCastAppTask
|
|
21
|
+
from .spotifyconnectzeroconfcastlistener import SpotifyConnectZeroconfCastListener
|
|
22
|
+
from .spotifyconnectzeroconfcastmultizonelistener import SpotifyConnectZeroconfCastMultiZoneListener
|
|
23
|
+
from .spotifyconnectdeviceeventargs import SpotifyConnectDeviceEventArgs
|
|
24
|
+
from .spotifyconnectzeroconflistener import ZEROCONF_SERVICETYPE_SPOTIFYCONNECT, SpotifyConnectZeroconfListener
|
|
25
|
+
from .spotifyconnectzeroconfcastcontroller import (
|
|
26
|
+
TYPE_ADD_USER_RESPONSE,
|
|
27
|
+
TYPE_ADD_USER_ERROR,
|
|
28
|
+
TYPE_GET_INFO_ERROR,
|
|
29
|
+
TYPE_LAUNCH_ERROR,
|
|
30
|
+
TYPE_TRANSFER_ERROR,
|
|
31
|
+
TYPE_TRANSFER_SUCCESS
|
|
32
|
+
)
|
|
33
|
+
# note - cannot reference `SpotifyClient` due to circular import!
|
|
34
|
+
# from spotifywebapipython import SpotifyClient
|
|
35
|
+
from spotifywebapipython import SpotifyApiError
|
|
36
|
+
from spotifywebapipython.models import Device, PlayerPlayState, SpotifyConnectDevices, SpotifyConnectDevice, ZeroconfDiscoveryResult
|
|
37
|
+
from spotifywebapipython.saappmessages import SAAppMessages
|
|
38
|
+
from spotifywebapipython.sautils import Event, validateDelay
|
|
39
|
+
from spotifywebapipython.spotifymediatypes import SpotifyMediaTypes
|
|
40
|
+
from spotifywebapipython.zeroconfapi import ZeroconfGetInfo, ZeroconfConnect, ZeroconfResponse
|
|
41
|
+
|
|
42
|
+
# get smartinspect logger reference; create a new session for this module name.
|
|
43
|
+
from smartinspectpython.siauto import SIAuto, SILevel, SISession, SIMethodParmListContext, SIColors
|
|
44
|
+
import logging
|
|
45
|
+
|
|
46
|
+
_logsi:SISession = SIAuto.Si.GetSession(__name__)
|
|
47
|
+
if (_logsi == None):
|
|
48
|
+
_logsi = SIAuto.Si.AddSession(__name__, True)
|
|
49
|
+
_logsi.SystemLogger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SpotifyConnectDirectoryTask(threading.Thread):
|
|
53
|
+
"""
|
|
54
|
+
Spotify Connect Directory thread task.
|
|
55
|
+
|
|
56
|
+
This class discovers available Spotify Connect devices on the local network.
|
|
57
|
+
Discovered devices are then added to an internal list, which can be referenced
|
|
58
|
+
by clients.
|
|
59
|
+
|
|
60
|
+
The directory task is started immediately after the authorization token is
|
|
61
|
+
established in the SpotifyClient object.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
spotifyClientInstance, # :SpotifyClient,
|
|
67
|
+
zeroconfInstance:Zeroconf=None,
|
|
68
|
+
initialDiscoveryTimeout:float=3.0,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Initializes a new instance of the class.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
spotifyClientInstance (SpotifyClient):
|
|
75
|
+
SpotifyClient instance used to interface with the Spotify Web API.
|
|
76
|
+
zeroconfInstance (Zeroconf):
|
|
77
|
+
Zeroconf instance used to query zeroconf; otherwise, None to
|
|
78
|
+
create a new zeroconf instance.
|
|
79
|
+
initialDiscoveryTimeout (float):
|
|
80
|
+
Time (in seconds) to give child Zeroconf discovery threads time to process
|
|
81
|
+
initial Zeroconf service information changes.
|
|
82
|
+
Default is 3.
|
|
83
|
+
|
|
84
|
+
The `initialDiscoveryTimeout` argument gives the child Zeroconf discovery threads
|
|
85
|
+
time to process initial Zeroconf service information changes prior to setting the
|
|
86
|
+
`WaitForInitComplete` event as complete. This gives the directory thread task
|
|
87
|
+
time to discover all of the Spotify Connect devices currently attached to the
|
|
88
|
+
local network.
|
|
89
|
+
"""
|
|
90
|
+
# invoke base class method.
|
|
91
|
+
super().__init__()
|
|
92
|
+
|
|
93
|
+
# note that the initialization method executes in the parent thread;
|
|
94
|
+
# the code in the `run` method executes on a child (daemon) thread.
|
|
95
|
+
|
|
96
|
+
# set thread name before we start logging.
|
|
97
|
+
self.name = "Spotify Connect Directory Task"
|
|
98
|
+
|
|
99
|
+
# validations.
|
|
100
|
+
if (spotifyClientInstance is None):
|
|
101
|
+
raise SpotifyApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % ("__init__", 'spotifyClientInstance'), logsi=_logsi)
|
|
102
|
+
if (spotifyClientInstance.SpotifyConnectLoginId is None) or (spotifyClientInstance.SpotifyConnectLoginId.strip() == ""):
|
|
103
|
+
raise SpotifyApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % ("__init__", 'spotifyClientInstance.SpotifyConnectLoginId'), logsi=_logsi)
|
|
104
|
+
if (not isinstance(initialDiscoveryTimeout, float)) and (not isinstance(initialDiscoveryTimeout, int)):
|
|
105
|
+
initialDiscoveryTimeout = 3.0
|
|
106
|
+
|
|
107
|
+
# initialize storage.
|
|
108
|
+
self._CastAppTasks:dict[str, SpotifyConnectZeroconfCastAppTask] = {}
|
|
109
|
+
self._CastBrowser:CastBrowser = None
|
|
110
|
+
self._CastMultiZoneControllers:dict[str, MultizoneController] = {}
|
|
111
|
+
self._InitialDiscoveryTimeout = initialDiscoveryTimeout
|
|
112
|
+
self._IsStopRequested:bool = False
|
|
113
|
+
self._SonosPlayers:dict = {}
|
|
114
|
+
self._SpotifyClientInstance = spotifyClientInstance
|
|
115
|
+
self._SpotifyConnectBrowser:ServiceBrowser = None
|
|
116
|
+
self._SpotifyConnectDevices:SpotifyConnectDevices = SpotifyConnectDevices()
|
|
117
|
+
self._SpotifyConnectDevices_RLock = threading.RLock() # re-entrant lock to sync access to devices collection.
|
|
118
|
+
self._ZeroconfInstance:Zeroconf = zeroconfInstance
|
|
119
|
+
self._Zeroconf_RLock = threading.RLock() # re-entrant lock to sync access to zeroconf service state changes.
|
|
120
|
+
|
|
121
|
+
# define all events raised by this class.
|
|
122
|
+
self.DeviceAdded = Event()
|
|
123
|
+
"""
|
|
124
|
+
Event raised when a Spotify Connect device entry has been added.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
self.DeviceRemoved = Event()
|
|
128
|
+
"""
|
|
129
|
+
Event raised when a Spotify Connect device entry has been removed.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
self.DeviceUpdated = Event()
|
|
133
|
+
"""
|
|
134
|
+
Event raised when a Spotify Connect device entry has been updated.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
# define all threading events raised by this class.
|
|
138
|
+
self.WaitForInitComplete = threading.Event()
|
|
139
|
+
"""
|
|
140
|
+
Event that will be posted when the task has been initialized, and is
|
|
141
|
+
waiting for commands or device notifications.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
self.WaitForActivationComplete = threading.Event()
|
|
145
|
+
"""
|
|
146
|
+
Event that will be posted when the task has completed the activation sequence for
|
|
147
|
+
the device, which is either a `addUserResponse` or `addUserError` notification.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
self.WaitForTransferComplete = threading.Event()
|
|
151
|
+
"""
|
|
152
|
+
Event that will be posted when the task has completed the playback transfer sequence for
|
|
153
|
+
the device, which is either a `transferSuccess` or `transferError` notification.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
# wire up event handlers.
|
|
157
|
+
self.DeviceAdded += self.OnDeviceAdded
|
|
158
|
+
self.DeviceRemoved += self.OnDeviceRemoved
|
|
159
|
+
self.DeviceUpdated += self.OnDeviceUpdated
|
|
160
|
+
|
|
161
|
+
# clear event notifiers.
|
|
162
|
+
self.WaitForInitComplete.clear()
|
|
163
|
+
self.WaitForActivationComplete.set() # indicate activation is complete (initial state).
|
|
164
|
+
self.WaitForTransferComplete.set() # indicate transfer is complete (initial state).
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def InitialDiscoveryTimeout(self) -> float:
|
|
169
|
+
"""
|
|
170
|
+
Time to wait (in seconds) after starting the Zeroconf discovery thread
|
|
171
|
+
tasks before marking the directory task initialization as complete.
|
|
172
|
+
"""
|
|
173
|
+
return self._InitialDiscoveryTimeout
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def IsStopRequested(self) -> bool:
|
|
178
|
+
"""
|
|
179
|
+
Indicator used to denote the task has been asked to stop by the main thread.
|
|
180
|
+
"""
|
|
181
|
+
return self._IsStopRequested
|
|
182
|
+
|
|
183
|
+
@IsStopRequested.setter
|
|
184
|
+
def IsStopRequested(self, value:bool):
|
|
185
|
+
"""
|
|
186
|
+
Sets the IsStopRequested property value.
|
|
187
|
+
"""
|
|
188
|
+
if isinstance(value, bool):
|
|
189
|
+
self._IsStopRequested = value
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def CastAppTasks(self) -> dict[str, SpotifyConnectZeroconfCastAppTask]:
|
|
194
|
+
"""
|
|
195
|
+
Collection of SpotifyConnectZeroconfCastAppTask objects that have been created
|
|
196
|
+
when transferring playback.
|
|
197
|
+
"""
|
|
198
|
+
return self._CastAppTasks
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def CastBrowser(self) -> CastBrowser:
|
|
203
|
+
"""
|
|
204
|
+
Returns the internal CastBrowser object.
|
|
205
|
+
"""
|
|
206
|
+
return self._CastBrowser
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@property
|
|
210
|
+
def IsZeroconDiscoveryEnabled(self) -> bool:
|
|
211
|
+
"""
|
|
212
|
+
Returns True if Zeroconf discovery is enabled; otherwise, False.
|
|
213
|
+
"""
|
|
214
|
+
# is discovery enabled?
|
|
215
|
+
# is zeroconf discovery enabled?
|
|
216
|
+
if (not self.is_alive()):
|
|
217
|
+
return False
|
|
218
|
+
if (self._InitialDiscoveryTimeout == 0):
|
|
219
|
+
return False
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def SpotifyClientInstance(self) -> object: # SpotifyClient:
|
|
225
|
+
"""
|
|
226
|
+
Returns the SpotifyClient instance.
|
|
227
|
+
"""
|
|
228
|
+
return self._SpotifyClientInstance
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def ZeroconfInstance(self) -> Zeroconf:
|
|
233
|
+
"""
|
|
234
|
+
Returns the zeroconf instance.
|
|
235
|
+
"""
|
|
236
|
+
return self._ZeroconfInstance
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def run(self):
|
|
240
|
+
"""
|
|
241
|
+
The task to perform on a seperate thread.
|
|
242
|
+
"""
|
|
243
|
+
try:
|
|
244
|
+
|
|
245
|
+
# trace.
|
|
246
|
+
_logsi.LogVerbose("%s - Starting thread RUN / TASK method" % (self.name))
|
|
247
|
+
_logsi.LogThread(SILevel.Debug, "%s - Thread information" % (self.name), self)
|
|
248
|
+
|
|
249
|
+
# refresh dynamic device list.
|
|
250
|
+
self.RefreshDynamicDevices()
|
|
251
|
+
|
|
252
|
+
# is zeroconf discovery enabled?
|
|
253
|
+
if (not self.IsZeroconDiscoveryEnabled):
|
|
254
|
+
|
|
255
|
+
_logsi.LogVerbose("%s - Spotify Connect Zeroconf discovery is disabled; only Spotify Web API player devices will be recognized" % (self.name), colorValue=SIColors.Coral)
|
|
256
|
+
|
|
257
|
+
else:
|
|
258
|
+
|
|
259
|
+
# create new spotify connect zeroconf discovery browser instance.
|
|
260
|
+
# this will discover spotify connect devices on the local network.
|
|
261
|
+
# when a device is found, cast_listener.add_cast is called.
|
|
262
|
+
# when a deviceost, the cast_listener.remove_cast is called
|
|
263
|
+
# when a devicepdated, cast_listener.update_cast is called.
|
|
264
|
+
_logsi.LogVerbose("%s - Starting Spotify Connect Zeroconf discovery browser" % (self.name))
|
|
265
|
+
handler:SpotifyConnectZeroconfListener = SpotifyConnectZeroconfListener(self, self._Zeroconf_RLock)
|
|
266
|
+
self._SpotifyConnectBrowser = ServiceBrowser(
|
|
267
|
+
self._ZeroconfInstance,
|
|
268
|
+
ZEROCONF_SERVICETYPE_SPOTIFYCONNECT,
|
|
269
|
+
handlers=[handler.OnServiceStateChange]
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# create new chromecast zeroconf discovery browser instance.
|
|
273
|
+
# this will discover Chromecasts on the local network.
|
|
274
|
+
# when a Chromecast is found, cast_listener.add_cast is called.
|
|
275
|
+
# when a Chromecast is lost, the cast_listener.remove_cast is called
|
|
276
|
+
# when a Chromecast is updated, cast_listener.update_cast is called.
|
|
277
|
+
_logsi.LogVerbose("%s - Starting Chromecast Zeroconf discovery browser" % (self.name))
|
|
278
|
+
self._CastBrowser = CastBrowser(
|
|
279
|
+
zeroconf_instance=self._ZeroconfInstance,
|
|
280
|
+
cast_listener=SpotifyConnectZeroconfCastListener(self, self._Zeroconf_RLock),
|
|
281
|
+
known_hosts=None
|
|
282
|
+
)
|
|
283
|
+
self._CastBrowser.start_discovery()
|
|
284
|
+
|
|
285
|
+
# give child Zeroconf discovery threads time to process initial service information discovery.
|
|
286
|
+
time.sleep(self._InitialDiscoveryTimeout)
|
|
287
|
+
|
|
288
|
+
# trace - dump devices discovered initially.
|
|
289
|
+
if (_logsi.IsOn(SILevel.Verbose)):
|
|
290
|
+
|
|
291
|
+
# use lock, as zeroconf tasks could be updating the device list.
|
|
292
|
+
with self._Zeroconf_RLock:
|
|
293
|
+
|
|
294
|
+
# trace.
|
|
295
|
+
_logsi.LogVerbose("%s - Spotify Connect devices discovered at initialization (%s items)" % (self.name, len(self._SpotifyConnectDevices)), colorValue=SIColors.Coral)
|
|
296
|
+
|
|
297
|
+
# trace - log all devices that were discovered initially to SmartInspect console.
|
|
298
|
+
for scDevice in self._SpotifyConnectDevices.Items:
|
|
299
|
+
isActive:str = " (active)" if (scDevice.IsActiveDevice) else ""
|
|
300
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Connect device: %s [%s]%s - object" % (scDevice.Title, scDevice.DiscoveryResult.Description, isActive), scDevice, excludeNonPublic=True, colorValue=SIColors.Coral)
|
|
301
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Connect device: %s [%s]%s - DiscoveryResult" % (scDevice.Title, scDevice.DiscoveryResult.Description, isActive), scDevice.DiscoveryResult, excludeNonPublic=True, colorValue=SIColors.Coral)
|
|
302
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Connect device: %s [%s]%s - DeviceInfo / getInfo" % (scDevice.Title, scDevice.DiscoveryResult.Description, isActive), scDevice.DeviceInfo, excludeNonPublic=True, colorValue=SIColors.Coral)
|
|
303
|
+
|
|
304
|
+
# trace - log all devices that were discovered initially to system logger.
|
|
305
|
+
# (since LogObject does not log to system logger).
|
|
306
|
+
for scDevice in self._SpotifyConnectDevices.Items:
|
|
307
|
+
isActive:str = " (active)" if (scDevice.IsActiveDevice) else ""
|
|
308
|
+
_logsi.LogVerbose("Spotify Connect device: %s [%s]%s" % (scDevice.Title, scDevice.DiscoveryResult.Description, isActive))
|
|
309
|
+
|
|
310
|
+
# indicate we are ready for commands.
|
|
311
|
+
self.WaitForInitComplete.set()
|
|
312
|
+
|
|
313
|
+
# event loop.
|
|
314
|
+
# note that the directory task must be kept running in order to control Chromecast devices!
|
|
315
|
+
while True:
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
|
|
319
|
+
# keep going until we are asked to stop.
|
|
320
|
+
if (self.IsStopRequested):
|
|
321
|
+
_logsi.LogVerbose("%s - Thread task stop requested" % (self.name))
|
|
322
|
+
break
|
|
323
|
+
time.sleep(0.50)
|
|
324
|
+
|
|
325
|
+
except Exception as ex:
|
|
326
|
+
|
|
327
|
+
# trace.
|
|
328
|
+
_logsi.LogException("%s - Exception: %s" % (str(ex)), ex, logToSystemLogger=False)
|
|
329
|
+
# ignore exceptions, as we can't do anything about them.
|
|
330
|
+
|
|
331
|
+
# at this point we have been requested to stop;
|
|
332
|
+
# loop through all active cast app tasks and unregister handlers.
|
|
333
|
+
# note that this will not stop the Spotify App Task that is running
|
|
334
|
+
# on the device; we don't stop the app because it would remove the
|
|
335
|
+
# Spotify Connect registration.
|
|
336
|
+
castAppTask:SpotifyConnectZeroconfCastAppTask = None
|
|
337
|
+
for castAppTask in self._CastAppTasks.values():
|
|
338
|
+
if (castAppTask.is_alive()):
|
|
339
|
+
_logsi.LogVerbose("%s - Stopping %s" % (self.name, castAppTask.name))
|
|
340
|
+
castAppTask.IsStopRequested = True
|
|
341
|
+
castAppTask.join()
|
|
342
|
+
|
|
343
|
+
# trace.
|
|
344
|
+
_logsi.LogVerbose("%s - Thread task was stopped" % (self.name))
|
|
345
|
+
|
|
346
|
+
except Exception as ex:
|
|
347
|
+
|
|
348
|
+
# trace.
|
|
349
|
+
_logsi.LogException("%s - Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
|
|
350
|
+
_logsi.LogVerbose("%s - Thread task is ending due to exception" % (self.name))
|
|
351
|
+
# ignore exceptions, since we are shutting down.
|
|
352
|
+
|
|
353
|
+
# indicate we are ready for commands.
|
|
354
|
+
self.WaitForInitComplete.set()
|
|
355
|
+
|
|
356
|
+
finally:
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
|
|
360
|
+
# unwire event handlers.
|
|
361
|
+
_logsi.LogVerbose("%s - Unwiring event handlers" % (self.name))
|
|
362
|
+
if (self.DeviceAdded is not None):
|
|
363
|
+
self.DeviceAdded -= self.OnDeviceAdded
|
|
364
|
+
if (self.DeviceRemoved is not None):
|
|
365
|
+
self.DeviceRemoved -= self.OnDeviceRemoved
|
|
366
|
+
if (self.DeviceUpdated is not None):
|
|
367
|
+
self.DeviceUpdated -= self.OnDeviceUpdated
|
|
368
|
+
|
|
369
|
+
except:
|
|
370
|
+
|
|
371
|
+
# trace.
|
|
372
|
+
_logsi.LogException("%s - An unhandled exception occured while unwiring event handlers: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
|
|
373
|
+
# ignore exceptions, since we are shutting down.
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
|
|
377
|
+
# stop spotify connect zeroconf discovery browser.
|
|
378
|
+
if (self._SpotifyConnectBrowser is not None):
|
|
379
|
+
_logsi.LogVerbose("%s - Stopping Spotify Connect Zeroconf discovery" % (self.name))
|
|
380
|
+
self._SpotifyConnectBrowser.cancel()
|
|
381
|
+
_logsi.LogObject(SILevel.Verbose, "%s - ZeroconfInstance object after ServiceBrowser resources released" % (self.name), self._ZeroconfInstance)
|
|
382
|
+
|
|
383
|
+
except:
|
|
384
|
+
|
|
385
|
+
# trace.
|
|
386
|
+
_logsi.LogException("%s - An unhandled exception occured while stopping Spotify Connect Zeroconf discovery browser: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
|
|
387
|
+
# ignore exceptions, since we are shutting down.
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
|
|
391
|
+
# stop chromecast zeroconf discovery browser.
|
|
392
|
+
if (self._CastBrowser is not None):
|
|
393
|
+
_logsi.LogVerbose("%s - Stopping Chromecast Zeroconf discovery" % (self.name))
|
|
394
|
+
self._CastBrowser.stop_discovery()
|
|
395
|
+
_logsi.LogObject(SILevel.Verbose, "%s - ZeroconfInstance object after CastBrowser resources released" % (self.name), self._ZeroconfInstance)
|
|
396
|
+
|
|
397
|
+
except:
|
|
398
|
+
|
|
399
|
+
# trace.
|
|
400
|
+
_logsi.LogException("%s - An unhandled exception occured while stopping Chromecast Zeroconf discovery browser: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
|
|
401
|
+
# ignore exceptions, since we are shutting down.
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def ActivateCastAppSpotify(
|
|
405
|
+
self,
|
|
406
|
+
deviceName:str,
|
|
407
|
+
transferPlayback:bool=False,
|
|
408
|
+
timeoutActivation:float=15.0,
|
|
409
|
+
timeoutTransfer:float=10.0,
|
|
410
|
+
) -> str:
|
|
411
|
+
"""
|
|
412
|
+
Activates the Spotify Cast Application on the specified Chromecast device.
|
|
413
|
+
|
|
414
|
+
It will also call the `OnCastGetInfoResponseReceived` method when the Spotify Cast App
|
|
415
|
+
receives a `GetInfoResponse` block that contains Spotify Connect Zeroconf GetInformation
|
|
416
|
+
data about the device.
|
|
417
|
+
|
|
418
|
+
It will also call the `OnCastZeroconfResponseReceived` method when the Spotify Cast App
|
|
419
|
+
receives any of the following message that contain zeroconf response data: `addUserResponse`,
|
|
420
|
+
`addUserError`, `transferError`, `transferSuccess`.
|
|
421
|
+
|
|
422
|
+
Once activated, you have about 20 seconds to transfer playback to the device.
|
|
423
|
+
The Spotify Cast application will automatically end after 20 seconds if playback
|
|
424
|
+
is not transferred to it within the timeout period. Note that any application can
|
|
425
|
+
be used to transfer playback to the device (Spotify Mobile / Desktop / Web, spotipy,
|
|
426
|
+
spotifywebapipython, etc).
|
|
427
|
+
|
|
428
|
+
Args:
|
|
429
|
+
deviceName (str):
|
|
430
|
+
Chromecast device friendly name (or device id) to activate.
|
|
431
|
+
transferPlayback (bool):
|
|
432
|
+
True to transfer playback to the device; otherwise, False to just activate
|
|
433
|
+
the Spotify Cast App on the device.
|
|
434
|
+
timeoutActivation (float):
|
|
435
|
+
Amount of time to wait (in seconds) for the Spotify Cast App to be fully activated,
|
|
436
|
+
the user logged in, and ready for transfer of playback.
|
|
437
|
+
Default is 15 seconds.
|
|
438
|
+
timeoutTransfer (float):
|
|
439
|
+
If `transferPlayback` argument is True, the amount of time to wait (in seconds)
|
|
440
|
+
for the transfer to complete; otherwise, argument is ignored.
|
|
441
|
+
Default is 10 seconds.
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
The deviceId that was activated.
|
|
445
|
+
"""
|
|
446
|
+
apiMethodName:str = "ActivateCastAppSpotify"
|
|
447
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
|
|
451
|
+
# trace.
|
|
452
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
453
|
+
apiMethodParms.AppendKeyValue("deviceName", deviceName)
|
|
454
|
+
apiMethodParms.AppendKeyValue("transferPlayback", transferPlayback)
|
|
455
|
+
apiMethodParms.AppendKeyValue("timeoutActivation", timeoutActivation)
|
|
456
|
+
apiMethodParms.AppendKeyValue("timeoutTransfer", timeoutTransfer)
|
|
457
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Activating Spotify App on Chromecast device: \"%s\"" % (deviceName), apiMethodParms)
|
|
458
|
+
|
|
459
|
+
# validations.
|
|
460
|
+
timeoutActivation = validateDelay(timeoutActivation, 15.0, 30.0)
|
|
461
|
+
timeoutTransfer = validateDelay(timeoutTransfer, 10.0, 20.0)
|
|
462
|
+
if (deviceName is None) or (deviceName.strip() == ""):
|
|
463
|
+
raise SpotifyApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % (apiMethodName, 'deviceName'), logsi=_logsi)
|
|
464
|
+
if (transferPlayback is None):
|
|
465
|
+
transferPlayback = False
|
|
466
|
+
|
|
467
|
+
# validation - Spotify Connect device name / id.
|
|
468
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
469
|
+
with self._SpotifyConnectDevices_RLock:
|
|
470
|
+
|
|
471
|
+
# search for device id first; if no matches, then search for name.
|
|
472
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceById(deviceName)
|
|
473
|
+
if (scDevice is None):
|
|
474
|
+
scDevice = self._SpotifyConnectDevices.GetDeviceByName(deviceName)
|
|
475
|
+
if (scDevice is None):
|
|
476
|
+
raise SpotifyApiError("Chromecast device \"%s\" could not be found in the SpotifyConnectDevices collection." % (deviceName), logsi=_logsi)
|
|
477
|
+
|
|
478
|
+
# match was found; reset device id activated before we activate a device.
|
|
479
|
+
scDevice.DeviceIdActivated = None
|
|
480
|
+
|
|
481
|
+
# is this a chromecast device?
|
|
482
|
+
if (not scDevice.IsChromeCast):
|
|
483
|
+
raise SpotifyApiError("Device %s is not a Chromecast device; use TransferPlayback instead." % (scDevice.Title), logsi=_logsi)
|
|
484
|
+
|
|
485
|
+
# is zeroconf discovery disabled?
|
|
486
|
+
if (not self.IsZeroconDiscoveryEnabled):
|
|
487
|
+
raise SpotifyApiError("Zeroconf discovery is disabled; cannot activate Chromecast devices.", logsi=_logsi)
|
|
488
|
+
|
|
489
|
+
# has discovery browser been started yet?
|
|
490
|
+
if (self._CastBrowser is None):
|
|
491
|
+
raise SpotifyApiError("Chromecast discovery browser reference not set; the Browser must be started prior to calling this method.", logsi=_logsi)
|
|
492
|
+
|
|
493
|
+
# any devices available?
|
|
494
|
+
if (len(self._CastBrowser.devices) == 0):
|
|
495
|
+
raise SpotifyApiError("No available Chromecast devices detected.", logsi=_logsi)
|
|
496
|
+
|
|
497
|
+
# are we waiting on a previous request to finish?
|
|
498
|
+
if (not self.WaitForActivationComplete.is_set):
|
|
499
|
+
raise SpotifyApiError("Waiting for previous Chromecast device activation to complete; try again in a few seconds.", logsi=_logsi)
|
|
500
|
+
if (not self.WaitForTransferComplete.is_set):
|
|
501
|
+
raise SpotifyApiError("Waiting for previous Chromecast device playback transfer to complete; try again in a few seconds.", logsi=_logsi)
|
|
502
|
+
|
|
503
|
+
# clear event notifiers.
|
|
504
|
+
self.WaitForActivationComplete.clear()
|
|
505
|
+
self.WaitForTransferComplete.clear()
|
|
506
|
+
|
|
507
|
+
# if Spotify Cast App is active on a cast device then request that
|
|
508
|
+
# it stop, as we will add a new one below.
|
|
509
|
+
# note that this will not stop the Spotify App Task that is running
|
|
510
|
+
# on the device; we don't stop the app because it would remove the
|
|
511
|
+
# Spotify Connect registration.
|
|
512
|
+
castAppTask:SpotifyConnectZeroconfCastAppTask = self._CastAppTasks.get(scDevice.DiscoveryResult.Key, None)
|
|
513
|
+
if (castAppTask is not None):
|
|
514
|
+
if (not castAppTask.is_alive()):
|
|
515
|
+
_logsi.LogVerbose("%s - Stopping Spotify Cast App task" % (self.name))
|
|
516
|
+
castAppTask.IsStopRequested = True
|
|
517
|
+
castAppTask.join()
|
|
518
|
+
_logsi.LogVerbose("%s - Spotify Cast App task was stopped successfully" % (self.name))
|
|
519
|
+
self._CastAppTasks.pop(scDevice.DiscoveryResult.Key, None)
|
|
520
|
+
|
|
521
|
+
# get CastInfo details of the specified device.
|
|
522
|
+
_logsi.LogVerbose("%s - Getting Chromecast information for device: \"%s\" [%s]" % (self.name, deviceName, scDevice.DiscoveryResult.HostIpTitle))
|
|
523
|
+
castInfo:CastInfo = self._CastBrowser.devices[UUID(scDevice.DiscoveryResult.Key)]
|
|
524
|
+
_logsi.LogObject(SILevel.Verbose, "%s - Chromecast CastInfo details: \"%s\" (ip=%s:%s)" % (self.name, castInfo.friendly_name, castInfo.host, castInfo.port), castInfo)
|
|
525
|
+
|
|
526
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
527
|
+
with self._SpotifyConnectDevices_RLock:
|
|
528
|
+
|
|
529
|
+
# reset device zeroconf response, as we are re-using an existing object.
|
|
530
|
+
scDevice.ZeroconfResponseInfo = ZeroconfResponse()
|
|
531
|
+
|
|
532
|
+
castDevice:Chromecast = None
|
|
533
|
+
|
|
534
|
+
# is this a cast group?
|
|
535
|
+
if (castInfo.cast_type == CAST_TYPE_GROUP):
|
|
536
|
+
|
|
537
|
+
# for groups, the SpotifyAppTask must be activated on the group leader (e.g. coordinator).
|
|
538
|
+
# the cast zeroconf info may not contain the group leader host info, as it is constantly
|
|
539
|
+
# being updated as devices are added / removed from the group. this can lead to the
|
|
540
|
+
# SpotifyAppTask being started on a member of the group that is not the leader, which will
|
|
541
|
+
# cause other devices in the group not to play.
|
|
542
|
+
#
|
|
543
|
+
# to combat this, we will call the `get_multizone_status` function that gives us the
|
|
544
|
+
# current group leader host info for each group. we will then create a cast device to
|
|
545
|
+
# the group leader host address, and start the SpotifyCastApp on that specific host.
|
|
546
|
+
|
|
547
|
+
# get current multizone status for all groups.
|
|
548
|
+
castMultiZoneStatus:MultizoneStatus = get_multizone_status(castInfo.host, castInfo.services, self.ZeroconfInstance, 5)
|
|
549
|
+
_logsi.LogObject(SILevel.Verbose, "%s - Chromecast group multizone status for device: %s [ip=%s:%s]" % (self.name, scDevice.Title, castInfo.host, castInfo.port), castMultiZoneStatus, colorValue=SIColors.Coral)
|
|
550
|
+
|
|
551
|
+
# default host ip and port in case we don't find the group coordinator.
|
|
552
|
+
groupHost:str = scDevice.DiscoveryResult.HostIpAddress
|
|
553
|
+
groupPort:int = scDevice.DiscoveryResult.HostIpPort
|
|
554
|
+
|
|
555
|
+
# find the group coordinator for the selected device.
|
|
556
|
+
zoneInfo:MultizoneInfo = None
|
|
557
|
+
for zoneInfo in castMultiZoneStatus.groups:
|
|
558
|
+
|
|
559
|
+
# is this our group?
|
|
560
|
+
if (str(zoneInfo.uuid) == scDevice.DiscoveryResult.Key):
|
|
561
|
+
|
|
562
|
+
# yes - get host info to use for the device.
|
|
563
|
+
# we will use the device discovery result HostIpAddress instead of the zoneInfo.host
|
|
564
|
+
# value here, as the zoneInfo MAY contain an IPV6 formatted address, which was causing
|
|
565
|
+
# issues if the device was originally activated using an IPV4 address!
|
|
566
|
+
groupHost = scDevice.DiscoveryResult.HostIpAddress
|
|
567
|
+
groupPort = scDevice.DiscoveryResult.HostIpPort
|
|
568
|
+
break
|
|
569
|
+
|
|
570
|
+
# connect to the device and build a Chromecast instance from group coordinator host info.
|
|
571
|
+
castDevice = get_chromecast_from_host(
|
|
572
|
+
host=(groupHost, groupPort, castInfo.uuid, castInfo.model_name, castInfo.friendly_name),
|
|
573
|
+
tries=2,
|
|
574
|
+
retry_wait=0.5,
|
|
575
|
+
timeout=5)
|
|
576
|
+
|
|
577
|
+
# calculate wait timeout for devices to become active, based on number of devices in group.
|
|
578
|
+
groupWaitTimeoutSecs:float = 15.0
|
|
579
|
+
if (castMultiZoneStatus.groups is not None):
|
|
580
|
+
groupCount:int = len(castMultiZoneStatus.groups)
|
|
581
|
+
if (groupCount > 1):
|
|
582
|
+
groupWaitTimeoutSecs = (groupCount * 7.5)
|
|
583
|
+
|
|
584
|
+
# wait for the group device to become ready.
|
|
585
|
+
_logsi.LogVerbose("%s - Waiting %d seconds max for Chromecast group multizone device to activate: %s [ip=%s:%s]" % (self.name, groupWaitTimeoutSecs, scDevice.Title, castInfo.host, castInfo.port), colorValue=SIColors.Coral)
|
|
586
|
+
castDevice.wait(groupWaitTimeoutSecs)
|
|
587
|
+
|
|
588
|
+
else:
|
|
589
|
+
|
|
590
|
+
# not a group; just use the current CastInfo object.
|
|
591
|
+
|
|
592
|
+
# connect to the device and build a Chromecast instance from a CastInfo object.
|
|
593
|
+
castDevice = get_chromecast_from_cast_info(
|
|
594
|
+
cast_info=castInfo,
|
|
595
|
+
zconf=self._ZeroconfInstance,
|
|
596
|
+
tries=2,
|
|
597
|
+
retry_wait=0.5,
|
|
598
|
+
timeout=3)
|
|
599
|
+
|
|
600
|
+
# wait for the device to become ready (10 seconds max).
|
|
601
|
+
castDevice.wait(10)
|
|
602
|
+
|
|
603
|
+
# trace.
|
|
604
|
+
if (_logsi.IsOn(SILevel.Verbose)):
|
|
605
|
+
_logsi.LogObject(SILevel.Verbose, "%s - Chromecast device object: \"%s\" (ip=%s:%s)" % (self.name, castInfo.friendly_name, castDevice.socket_client.host, castDevice.socket_client.port), castDevice)
|
|
606
|
+
_logsi.LogObject(SILevel.Verbose, "%s - Chromecast device status: %s [ip=%s:%s]" % (self.name, scDevice.Title, castDevice.socket_client.host, castDevice.socket_client.port), castDevice.status, colorValue=SIColors.Coral)
|
|
607
|
+
|
|
608
|
+
# start the Spotify Cast application on the Chromecast device.
|
|
609
|
+
# this will start the spotify app on the cast device.
|
|
610
|
+
# it will also call the `OnCastGetInfoResponseReceived` method when the SpotifyConnectZeroconfCastAppTask
|
|
611
|
+
# receives a `GetInfoResponse` block that contains Spotify Connect Zeroconf GetInformation data about the device.
|
|
612
|
+
# it will also call the `OnCastZeroconfResponseReceived` method when the SpotifyConnectZeroconfCastAppTask
|
|
613
|
+
# receives a `ZeroconfResponse` block that contains Spotify Connect Zeroconf Response data about the device.
|
|
614
|
+
_logsi.LogVerbose("%s - Starting Spotify Cast application on Chromecast device: \"%s\" (%s) [%s]" % (self.name, castDevice.name, str(castDevice.uuid), scDevice.DiscoveryResult.HostIpTitle))
|
|
615
|
+
castAppTask:SpotifyConnectZeroconfCastAppTask = SpotifyConnectZeroconfCastAppTask(
|
|
616
|
+
castDevice,
|
|
617
|
+
self.SpotifyClientInstance,
|
|
618
|
+
self.OnCastGetInfoResponseReceived,
|
|
619
|
+
self.OnCastZeroconfResponseReceived,
|
|
620
|
+
transferPlayback
|
|
621
|
+
)
|
|
622
|
+
castAppTask.daemon = True
|
|
623
|
+
castAppTask.start()
|
|
624
|
+
|
|
625
|
+
# add cast app task to active tasks collection.
|
|
626
|
+
self._CastAppTasks[str(castInfo.uuid)] = castAppTask
|
|
627
|
+
|
|
628
|
+
# wait for the launched spotify app to activate the user (or fail).
|
|
629
|
+
# this occurs when we receive any of the following zeroconf response
|
|
630
|
+
# messages: `addUserResponse`, `addUserError`, `getInfoError`, `launchError`.
|
|
631
|
+
|
|
632
|
+
counter = 0
|
|
633
|
+
while counter < (timeoutActivation + 1):
|
|
634
|
+
if (self.WaitForActivationComplete.wait(1)):
|
|
635
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
636
|
+
with self._SpotifyConnectDevices_RLock:
|
|
637
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
|
|
638
|
+
if (scDevice is not None):
|
|
639
|
+
response = scDevice.ZeroconfResponseInfo
|
|
640
|
+
if (response.ResponseSource == TYPE_LAUNCH_ERROR):
|
|
641
|
+
raise SpotifyApiError("Spotify Cast Application could not be activated on Chromecast device \"%s\": %s" % (deviceName, response.StatusString), logsi=_logsi)
|
|
642
|
+
elif (response.ResponseSource == TYPE_ADD_USER_RESPONSE):
|
|
643
|
+
scDevice.WasReConnected = True
|
|
644
|
+
break
|
|
645
|
+
raise SpotifyApiError("Spotify Cast App could not be activated on Chromecast device \"%s\": %s" % (deviceName, response.ToString(False)), logsi=_logsi)
|
|
646
|
+
raise SpotifyApiError("Spotify Cast App could not activated on Chromecast device: unknown error.", logsi=_logsi)
|
|
647
|
+
if (counter >= timeoutActivation):
|
|
648
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
649
|
+
with self._SpotifyConnectDevices_RLock:
|
|
650
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
|
|
651
|
+
if (scDevice is not None):
|
|
652
|
+
raise SpotifyApiError("Spotify Cast App activation timeout (%s seconds) was exceeded while trying to activate Chromecast device \"%s\"." % (timeoutActivation, deviceName), logsi=_logsi)
|
|
653
|
+
counter += 1
|
|
654
|
+
|
|
655
|
+
# at this point we have received an `adduserResponse` from the Chromecast device, indicating
|
|
656
|
+
# that it has launched the Spotify Cast App and that the user is logged in.
|
|
657
|
+
|
|
658
|
+
# was transfer playback specified?
|
|
659
|
+
if (transferPlayback == True):
|
|
660
|
+
|
|
661
|
+
# note that we only wait if we are transferring playback.
|
|
662
|
+
|
|
663
|
+
# wait for the launched spotify app to transfer playback (or fail).
|
|
664
|
+
# this occurs when we receive any of the following zeroconf response
|
|
665
|
+
# messages: `transferSuccess`, `transferError`.
|
|
666
|
+
|
|
667
|
+
counter = 0
|
|
668
|
+
while counter < (timeoutTransfer + 1):
|
|
669
|
+
if (self.WaitForTransferComplete.wait(1)):
|
|
670
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
671
|
+
with self._SpotifyConnectDevices_RLock:
|
|
672
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
|
|
673
|
+
if (scDevice is not None):
|
|
674
|
+
response = scDevice.ZeroconfResponseInfo
|
|
675
|
+
if (response.ResponseSource == TYPE_TRANSFER_SUCCESS):
|
|
676
|
+
break
|
|
677
|
+
raise SpotifyApiError("Spotify Cast App failed to receive playback transfer on Chromecast device \"%s\": %s" % (deviceName, response.ToString(False)), logsi=_logsi)
|
|
678
|
+
raise SpotifyApiError("Spotify Cast App failed to receive playback transfer on Chromecast device: unknown error.", logsi=_logsi)
|
|
679
|
+
if (counter >= timeoutTransfer):
|
|
680
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
681
|
+
with self._SpotifyConnectDevices_RLock:
|
|
682
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
|
|
683
|
+
if (scDevice is not None):
|
|
684
|
+
raise SpotifyApiError("Spotify Cast App transfer playback timeout (%s seconds) was exceeded while waiting for transfer of playback on Chromecast device \"%s\"." % (timeoutTransfer, deviceName), logsi=_logsi)
|
|
685
|
+
counter += 1
|
|
686
|
+
|
|
687
|
+
# at this point we have received an `transferSuccess` from the Chromecast device, indicating
|
|
688
|
+
# that transfer of playback was a success; it should now be playing a track!
|
|
689
|
+
|
|
690
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
691
|
+
with self._SpotifyConnectDevices_RLock:
|
|
692
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(castDevice.uuid))
|
|
693
|
+
if (scDevice is not None):
|
|
694
|
+
|
|
695
|
+
# store the deviceId that was activated, if it's different than the original deviceId.
|
|
696
|
+
if (scDevice.Id != castAppTask.DeviceIdActivated):
|
|
697
|
+
scDevice.DeviceIdActivated = castAppTask.DeviceIdActivated
|
|
698
|
+
|
|
699
|
+
# return the device id that was activated.
|
|
700
|
+
return castAppTask.DeviceIdActivated
|
|
701
|
+
|
|
702
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
703
|
+
except Exception as ex:
|
|
704
|
+
|
|
705
|
+
# format unhandled exception.
|
|
706
|
+
raise SpotifyApiError("Could not activate Spotify Cast application on Chromecast device \"%s\": %s" % (deviceName, str(ex)), ex, logsi=_logsi)
|
|
707
|
+
|
|
708
|
+
finally:
|
|
709
|
+
|
|
710
|
+
# reset activation and transfer wait events to initial state for next time;
|
|
711
|
+
# just in case they were not set in the wait logic above (e.g. error condition).
|
|
712
|
+
self.WaitForActivationComplete.set()
|
|
713
|
+
self.WaitForTransferComplete.set()
|
|
714
|
+
|
|
715
|
+
# trace.
|
|
716
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def AddDynamicDevice(
|
|
720
|
+
self,
|
|
721
|
+
device:Device,
|
|
722
|
+
) -> None:
|
|
723
|
+
"""
|
|
724
|
+
Adds a new dynamic SpotifyConnectDevice entry to the devices collection.
|
|
725
|
+
|
|
726
|
+
Args:
|
|
727
|
+
device (Device):
|
|
728
|
+
Device information to add.
|
|
729
|
+
|
|
730
|
+
Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
|
|
731
|
+
process, but still exist in the player device list. These are usually Spotify Player
|
|
732
|
+
client devices (e.g. mobile / web / desktop players) that utilize temporary device id's.
|
|
733
|
+
|
|
734
|
+
<details>
|
|
735
|
+
<summary>Sample Code</summary>
|
|
736
|
+
```python
|
|
737
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/AddDynamicDevice.py
|
|
738
|
+
```
|
|
739
|
+
</details>
|
|
740
|
+
"""
|
|
741
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
742
|
+
with self._SpotifyConnectDevices_RLock:
|
|
743
|
+
|
|
744
|
+
# validations.
|
|
745
|
+
if (device is None) or (not isinstance(device, Device)):
|
|
746
|
+
return
|
|
747
|
+
|
|
748
|
+
# build zeroconf discovery result with what we can.
|
|
749
|
+
discoverResult:ZeroconfDiscoveryResult = ZeroconfDiscoveryResult()
|
|
750
|
+
discoverResult.DeviceName = device.Name
|
|
751
|
+
discoverResult.HostIpAddresses.append('127.0.0.1')
|
|
752
|
+
discoverResult.Server = '127.0.0.1'
|
|
753
|
+
discoverResult.HostIpPort = 0
|
|
754
|
+
discoverResult.Name = device.Name
|
|
755
|
+
discoverResult.SpotifyConnectCPath = '/zc'
|
|
756
|
+
discoverResult.SpotifyConnectVersion = '1.0'
|
|
757
|
+
|
|
758
|
+
# build zeroconf getinfo result with what we can.
|
|
759
|
+
info:ZeroconfGetInfo = ZeroconfGetInfo()
|
|
760
|
+
info.ActiveUser = self.SpotifyClientInstance.SpotifyConnectLoginId
|
|
761
|
+
info.DeviceId = device.Id
|
|
762
|
+
info.DeviceType = device.Type
|
|
763
|
+
info.RemoteName = device.Name
|
|
764
|
+
info.SpotifyError = 0
|
|
765
|
+
info.Status = 101
|
|
766
|
+
info.StatusString = 'OK'
|
|
767
|
+
|
|
768
|
+
# populate brand info for popular devices.
|
|
769
|
+
if (device.Name == "Web Player (Chrome)"):
|
|
770
|
+
info.BrandDisplayName = 'Google'
|
|
771
|
+
info.ModelDisplayName = 'Chrome'
|
|
772
|
+
info.ProductId = 'Web Player'
|
|
773
|
+
elif (device.Name == "Web Player (Microsoft Edge)"):
|
|
774
|
+
info.BrandDisplayName = 'Microsoft'
|
|
775
|
+
info.ModelDisplayName = 'Edge'
|
|
776
|
+
info.ProductId = 'Web Player'
|
|
777
|
+
elif (device.Name == "iPhone") and (device.Type == "Smartphone"):
|
|
778
|
+
info.BrandDisplayName = 'Apple'
|
|
779
|
+
info.ModelDisplayName = 'iPhone'
|
|
780
|
+
info.ProductId = 'Mobile Player'
|
|
781
|
+
else:
|
|
782
|
+
info.BrandDisplayName = 'unknown'
|
|
783
|
+
info.ModelDisplayName = 'unknown'
|
|
784
|
+
info.ProductId = 'unknown'
|
|
785
|
+
|
|
786
|
+
# add the device.
|
|
787
|
+
scDevice:SpotifyConnectDevice = SpotifyConnectDevice()
|
|
788
|
+
scDevice.Id = info.DeviceId
|
|
789
|
+
scDevice.Name = info.RemoteName
|
|
790
|
+
scDevice.DeviceInfo = info
|
|
791
|
+
scDevice.DiscoveryResult = discoverResult
|
|
792
|
+
self._SpotifyConnectDevices.Items.append(scDevice)
|
|
793
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
794
|
+
|
|
795
|
+
# sort devices collection by device name.
|
|
796
|
+
if (len(self._SpotifyConnectDevices.Items) > 0):
|
|
797
|
+
self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
|
|
798
|
+
|
|
799
|
+
# trace.
|
|
800
|
+
_logsi.LogVerbose("Added SpotifyConnectDevices collection entry: %s - %s" % (scDevice.Title, scDevice.DiscoveryResult.Description), colorValue=SIColors.ForestGreen)
|
|
801
|
+
|
|
802
|
+
# raise event.
|
|
803
|
+
self._RaiseDeviceAdded(scDevice)
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
def GetActiveDevice(
|
|
807
|
+
self,
|
|
808
|
+
refresh:bool=False,
|
|
809
|
+
) -> SpotifyConnectDevice:
|
|
810
|
+
"""
|
|
811
|
+
Returns the device currently marked as active if found; otherwise, null.
|
|
812
|
+
|
|
813
|
+
Args:
|
|
814
|
+
refresh (bool):
|
|
815
|
+
True to retrieve active device from Spotify Web API player state;
|
|
816
|
+
otherwise, False to retrieve active device from devices collection.
|
|
817
|
+
|
|
818
|
+
Returns:
|
|
819
|
+
The device currently marked as active if found; otherwise, null.
|
|
820
|
+
|
|
821
|
+
The object returned is a copy of the internal master device entry; any changes made
|
|
822
|
+
to the copy do not affect the internal master device entry.
|
|
823
|
+
|
|
824
|
+
<details>
|
|
825
|
+
<summary>Sample Code</summary>
|
|
826
|
+
```python
|
|
827
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/GetActiveDevice.py
|
|
828
|
+
```
|
|
829
|
+
</details>
|
|
830
|
+
"""
|
|
831
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
832
|
+
with self._SpotifyConnectDevices_RLock:
|
|
833
|
+
|
|
834
|
+
result:SpotifyConnectDevice = None
|
|
835
|
+
|
|
836
|
+
# if refresh selected then retrieve the active device from playerState.
|
|
837
|
+
if (refresh):
|
|
838
|
+
result = self.UpdateActiveDevice(None)
|
|
839
|
+
if (result is not None):
|
|
840
|
+
return result
|
|
841
|
+
|
|
842
|
+
# find active device in the devices collection.
|
|
843
|
+
for scDevice in self._SpotifyConnectDevices.Items:
|
|
844
|
+
if (scDevice.DeviceInfo is not None):
|
|
845
|
+
if (scDevice.IsActiveDevice):
|
|
846
|
+
_logsi.LogVerbose("Spotify Connect active device detected: %s" % (scDevice.Title))
|
|
847
|
+
result = copy.deepcopy(scDevice)
|
|
848
|
+
break
|
|
849
|
+
|
|
850
|
+
# returns null if no active device found.
|
|
851
|
+
return result
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
def GetDevice(
|
|
855
|
+
self,
|
|
856
|
+
value:str,
|
|
857
|
+
refreshDynamicDevices:bool=True,
|
|
858
|
+
raiseExceptionIfNotFound:bool=True,
|
|
859
|
+
) -> SpotifyConnectDevice:
|
|
860
|
+
"""
|
|
861
|
+
Returns a `SpotifyConnectDevice` object for the specified device name / id if found
|
|
862
|
+
in the devices collection. otherwise, returns the currently active player
|
|
863
|
+
`SpotifyConnectDevice` object is returned. otherwise, null is returned.
|
|
864
|
+
|
|
865
|
+
Args:
|
|
866
|
+
value (str | None):
|
|
867
|
+
The target player device identifier.
|
|
868
|
+
This could be an id, name, a default device indicator (e.g. "*"), or null
|
|
869
|
+
to utilize the active player device.
|
|
870
|
+
Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
|
|
871
|
+
refreshDynamicDevices (bool):
|
|
872
|
+
True to refresh the list of dynamically added devices and the active player
|
|
873
|
+
state from real-time Spotify Web API data; otherwise, false to use the
|
|
874
|
+
current cached state and device list.
|
|
875
|
+
Default is True.
|
|
876
|
+
raiseExceptionIfNotFound (bool):
|
|
877
|
+
True to raise an exception if a device could not be resolved;
|
|
878
|
+
otherwise, False to just return a null value.
|
|
879
|
+
Default is True.
|
|
880
|
+
|
|
881
|
+
Returns:
|
|
882
|
+
A `SpotifyConnectDevice` object for the specified device name / id if found;
|
|
883
|
+
otherwise, null.
|
|
884
|
+
|
|
885
|
+
Raises:
|
|
886
|
+
SpotifyConnectDeviceNotFound:
|
|
887
|
+
If the `raiseExceptionIfNotFound` argument value is True, and the specified
|
|
888
|
+
device value was not found or there was no default device found.
|
|
889
|
+
SpotifyApiError:
|
|
890
|
+
If an error occured while obtaining current Spotify Web API Player device data.
|
|
891
|
+
|
|
892
|
+
If a SpotifyConnectDevice object is returned, it will be a copy of the internal master
|
|
893
|
+
device entry; any changes made to the copy do not affect the internal master device entry.
|
|
894
|
+
|
|
895
|
+
<details>
|
|
896
|
+
<summary>Sample Code</summary>
|
|
897
|
+
```python
|
|
898
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/GetDevice.py
|
|
899
|
+
```
|
|
900
|
+
</details>
|
|
901
|
+
"""
|
|
902
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
903
|
+
with self._SpotifyConnectDevices_RLock:
|
|
904
|
+
|
|
905
|
+
apiMethodName:str = "GetDevice"
|
|
906
|
+
apiMethodParms:SIMethodParmListContext = None
|
|
907
|
+
scDevice:SpotifyConnectDevice = None
|
|
908
|
+
scActiveDevice:SpotifyConnectDevice = None
|
|
909
|
+
|
|
910
|
+
try:
|
|
911
|
+
|
|
912
|
+
# trace.
|
|
913
|
+
apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
|
|
914
|
+
apiMethodParms.AppendKeyValue("value", value)
|
|
915
|
+
apiMethodParms.AppendKeyValue("refreshDynamicDevices", refreshDynamicDevices)
|
|
916
|
+
apiMethodParms.AppendKeyValue("SpotifyClient.DefaultDeviceId", self.SpotifyClientInstance.DefaultDeviceId)
|
|
917
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Getting Spotify Connect device instance for value \"%s\"" % (value), apiMethodParms)
|
|
918
|
+
|
|
919
|
+
# validations.
|
|
920
|
+
if (value is not None) and (not isinstance(value, str)):
|
|
921
|
+
value = None
|
|
922
|
+
if (not isinstance(refreshDynamicDevices, bool)):
|
|
923
|
+
refreshDynamicDevices = True
|
|
924
|
+
if (not isinstance(raiseExceptionIfNotFound, bool)):
|
|
925
|
+
raiseExceptionIfNotFound = True
|
|
926
|
+
valueOriginal:str = value
|
|
927
|
+
|
|
928
|
+
# get currently active player device.
|
|
929
|
+
if (refreshDynamicDevices):
|
|
930
|
+
scActiveDevice = self.RefreshDynamicDevices()
|
|
931
|
+
else:
|
|
932
|
+
scActiveDevice = self.UpdateActiveDevice()
|
|
933
|
+
|
|
934
|
+
# was the active spotify player device value specified (e.g. null / empty string)?
|
|
935
|
+
if (value is None) or (value.strip() == ""):
|
|
936
|
+
|
|
937
|
+
# is there an active player device?
|
|
938
|
+
if (scActiveDevice is not None):
|
|
939
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Player device \"%s\" (%s) was found in the Spotify Connect Devices collection (by Active PlayerState)" % (scActiveDevice.Name, scActiveDevice.Id), scActiveDevice, excludeNonPublic=True)
|
|
940
|
+
return scActiveDevice
|
|
941
|
+
|
|
942
|
+
# if no active player device, then let's use the default device value.
|
|
943
|
+
_logsi.LogVerbose("Spotify Player has no active device; defaulting device selection to use DefaultDeviceId: \"%s\"" % (self.SpotifyClientInstance.DefaultDeviceId))
|
|
944
|
+
value = self.SpotifyClientInstance.DefaultDeviceId
|
|
945
|
+
|
|
946
|
+
# was a default device value specified?
|
|
947
|
+
# if so, then use the `defaultDeviceId` value; if the `defaultDeviceId` value is not in the device list,
|
|
948
|
+
# then we will catch it with the `GetDeviceById` / `GetDeviceByName` logic.
|
|
949
|
+
# if `defaultDeviceId` was not configured, then use active player device (via playerstate).
|
|
950
|
+
# if `defaultDeviceId` value is not in the device list, then use active player device (via playerstate).
|
|
951
|
+
# if no active player device, then it's an exception!
|
|
952
|
+
if (value == "*"):
|
|
953
|
+
|
|
954
|
+
# get default device id value from the client.
|
|
955
|
+
defaultDeviceId:str = self.SpotifyClientInstance.DefaultDeviceId
|
|
956
|
+
|
|
957
|
+
# was a default device specified?
|
|
958
|
+
if (defaultDeviceId is None) or (defaultDeviceId.strip() == ""):
|
|
959
|
+
# is there an active player device?
|
|
960
|
+
if (scActiveDevice is not None):
|
|
961
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Player default device (*) was not configured; using active player device: %s" % (scActiveDevice.Title), scActiveDevice, excludeNonPublic=True)
|
|
962
|
+
return scActiveDevice
|
|
963
|
+
# if no default device and no active player, then it's an error!
|
|
964
|
+
#raise SpotifyApiError("Spotify Player default device (*) was not configured, and there is no active Spotify Player device", logsi=_logsi)
|
|
965
|
+
|
|
966
|
+
# assign default device value.
|
|
967
|
+
_logsi.LogVerbose("Spotify Player device default \"%s\" will be used for selecting a device (by default *)" % (defaultDeviceId))
|
|
968
|
+
value = defaultDeviceId
|
|
969
|
+
|
|
970
|
+
# check for the device id in the devices collection.
|
|
971
|
+
scDevice = self._SpotifyConnectDevices.GetDeviceById(value)
|
|
972
|
+
if (scDevice is not None):
|
|
973
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Player device %s was found in the Spotify Connect Devices collection (by Id)" % (scDevice.Title), scDevice, excludeNonPublic=True)
|
|
974
|
+
return copy.deepcopy(scDevice)
|
|
975
|
+
|
|
976
|
+
# check for the device name in the devices collection.
|
|
977
|
+
scDevice = self._SpotifyConnectDevices.GetDeviceByName(value)
|
|
978
|
+
if (scDevice is not None):
|
|
979
|
+
|
|
980
|
+
# we have a device match by name!
|
|
981
|
+
# is there an active player device?
|
|
982
|
+
if (scActiveDevice is not None):
|
|
983
|
+
|
|
984
|
+
# does the active device have the same name, but a different device id?
|
|
985
|
+
if (scActiveDevice.Name == scDevice.Name) and (scActiveDevice.Id != scDevice.Id):
|
|
986
|
+
# this denotes that we have 2 devices in the device list with the same name
|
|
987
|
+
# but different id's. in this case we will use the active device, since it
|
|
988
|
+
# is already active.
|
|
989
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Player device %s was found in the Spotify Connect Devices collection (by Duplicate Name), which denotes that we have 2 devices in the device list with the same name but different id's; in this case we will use the active device (id=%s), since it is already active" % (scDevice.Title, scActiveDevice.Id), scDevice, excludeNonPublic=True)
|
|
990
|
+
scDevice = scActiveDevice
|
|
991
|
+
|
|
992
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Player device %s was found in the Spotify Connect Devices collection (by Name)" % (scDevice.Title), scDevice, excludeNonPublic=True)
|
|
993
|
+
return copy.deepcopy(scDevice)
|
|
994
|
+
|
|
995
|
+
# is there an active player device?
|
|
996
|
+
if (scActiveDevice is not None):
|
|
997
|
+
|
|
998
|
+
# check if the active device is restricted; if it is, then it may not show up in the
|
|
999
|
+
# available device list of devices (common issue with Sonos devices). in this case,
|
|
1000
|
+
# it WILL show up in the player state device property as the active device.
|
|
1001
|
+
# note that the PlayerPlayState device is a name and not a device id.
|
|
1002
|
+
if (scActiveDevice.IsRestricted):
|
|
1003
|
+
value = ("" + value).lower()
|
|
1004
|
+
if (scActiveDevice.Id.lower() == value) or (scActiveDevice.Name.lower() == value):
|
|
1005
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Player device %s was found to be a restricted device (by Active PlayerState)" % (scDevice.Title), scDevice, excludeNonPublic=True)
|
|
1006
|
+
return scActiveDevice
|
|
1007
|
+
|
|
1008
|
+
# if `defaultDeviceId` value is not in the device list, then use active player device.
|
|
1009
|
+
if (valueOriginal == "*"):
|
|
1010
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Player default device \"%s\" was not found in the Spotify Connect Devices collection; using active player device: %s" % (value, scActiveDevice.Title), scActiveDevice, excludeNonPublic=True)
|
|
1011
|
+
return scActiveDevice
|
|
1012
|
+
|
|
1013
|
+
# at this point we could not resolve the device name / id, and there is no active player
|
|
1014
|
+
# available; raise exception / return null as requested by the caller.
|
|
1015
|
+
if (raiseExceptionIfNotFound):
|
|
1016
|
+
raise SpotifyConnectDeviceNotFound("Spotify Player device \"%s\" was not found, and there is no active Spotify player" % (value), logsi=_logsi)
|
|
1017
|
+
else:
|
|
1018
|
+
_logsi.LogVerbose("Spotify Player device \"%s\" was not found, and there is no active Spotify player" % (value))
|
|
1019
|
+
return None
|
|
1020
|
+
|
|
1021
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
1022
|
+
except Exception as ex:
|
|
1023
|
+
|
|
1024
|
+
# format unhandled exception.
|
|
1025
|
+
raise SpotifyApiError("%s - Could not retrieve dynamic device list: %s" % (self.name, str(ex)), ex, logsi=_logsi)
|
|
1026
|
+
|
|
1027
|
+
finally:
|
|
1028
|
+
|
|
1029
|
+
# trace.
|
|
1030
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
def GetDevices(self,
|
|
1034
|
+
) -> SpotifyConnectDevices:
|
|
1035
|
+
"""
|
|
1036
|
+
Returns a collection of available Spotify Connect devices.
|
|
1037
|
+
|
|
1038
|
+
The object returned is a copy of the internal master device list; any changes made
|
|
1039
|
+
to the copy do not affect the internal master device list.
|
|
1040
|
+
|
|
1041
|
+
<details>
|
|
1042
|
+
<summary>Sample Code</summary>
|
|
1043
|
+
```python
|
|
1044
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/GetDevices.py
|
|
1045
|
+
```
|
|
1046
|
+
</details>
|
|
1047
|
+
"""
|
|
1048
|
+
result:SpotifyConnectDevices
|
|
1049
|
+
|
|
1050
|
+
# DO NOT syncronize access via lock here!
|
|
1051
|
+
# doing so can cause thread deadlocks in Home Assistant state machine.
|
|
1052
|
+
# with self._SpotifyConnectDevices_RLock:
|
|
1053
|
+
|
|
1054
|
+
# creates a completely independent copy of the devices collection (no references),
|
|
1055
|
+
# and return it to the caller.
|
|
1056
|
+
result = copy.deepcopy(self._SpotifyConnectDevices)
|
|
1057
|
+
return result
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def GetPlayerDevice(
|
|
1061
|
+
self,
|
|
1062
|
+
value:str,
|
|
1063
|
+
refresh:bool=False,
|
|
1064
|
+
) -> SpotifyConnectDevice:
|
|
1065
|
+
"""
|
|
1066
|
+
Returns the device instance if it is currently in the Spotify Web API player device list;
|
|
1067
|
+
otherwise, null.
|
|
1068
|
+
|
|
1069
|
+
Args:
|
|
1070
|
+
value (str):
|
|
1071
|
+
Spotify Connect device name or id value used to identify the device.
|
|
1072
|
+
refresh (bool):
|
|
1073
|
+
True to refresh player devices from Spotify Web API devices;
|
|
1074
|
+
otherwise, False to retrieve player device from devices collection.
|
|
1075
|
+
|
|
1076
|
+
Returns:
|
|
1077
|
+
The device instance if it is currently in the Spotify Web API player device list;
|
|
1078
|
+
otherwise, null.
|
|
1079
|
+
|
|
1080
|
+
The object returned is a copy of the internal master device entry; any changes made
|
|
1081
|
+
to the copy do not affect the internal master device entry.
|
|
1082
|
+
|
|
1083
|
+
<details>
|
|
1084
|
+
<summary>Sample Code</summary>
|
|
1085
|
+
```python
|
|
1086
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/GetPlayerDevice.py
|
|
1087
|
+
```
|
|
1088
|
+
</details>
|
|
1089
|
+
"""
|
|
1090
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1091
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1092
|
+
|
|
1093
|
+
result:SpotifyConnectDevice = None
|
|
1094
|
+
|
|
1095
|
+
# validations.
|
|
1096
|
+
if (value is None):
|
|
1097
|
+
return result
|
|
1098
|
+
|
|
1099
|
+
# if refresh selected then update player devices from Spotify Web API.
|
|
1100
|
+
if (refresh):
|
|
1101
|
+
self.UpdatePlayerDevices()
|
|
1102
|
+
|
|
1103
|
+
# prepare for compare.
|
|
1104
|
+
value = "" + value.strip().lower()
|
|
1105
|
+
|
|
1106
|
+
# find specified player device in the devices collection.
|
|
1107
|
+
for scDevice in self._SpotifyConnectDevices._Items:
|
|
1108
|
+
if (scDevice.IsInDeviceList):
|
|
1109
|
+
if (scDevice.Id.lower() == value) or (scDevice.Name.lower() == value) or (scDevice.DiscoveryResult.DeviceName.lower() == value):
|
|
1110
|
+
_logsi.LogVerbose("Spotify Connect player device detected: %s" % (scDevice.Title))
|
|
1111
|
+
result = copy.deepcopy(scDevice)
|
|
1112
|
+
break
|
|
1113
|
+
|
|
1114
|
+
# returns null if no active device found.
|
|
1115
|
+
return result
|
|
1116
|
+
|
|
1117
|
+
|
|
1118
|
+
def GetSonosPlayer(
|
|
1119
|
+
self,
|
|
1120
|
+
device:SpotifyConnectDevice,
|
|
1121
|
+
returnCoordinator:bool=True,
|
|
1122
|
+
) -> SoCo:
|
|
1123
|
+
"""
|
|
1124
|
+
Returns the Sonos Controller instance for the specified Spotify Connect device.
|
|
1125
|
+
Returns the Sonos player instance for the specified Spotify Connect Sonos device.
|
|
1126
|
+
|
|
1127
|
+
Args:
|
|
1128
|
+
device (SpotifyConnectDevice):
|
|
1129
|
+
Spotify Connect device instance used to identify the Sonos player.
|
|
1130
|
+
returnCoordinator (bool):
|
|
1131
|
+
True to return the group coordinator instance if the device is part of a Sonos group;
|
|
1132
|
+
otherwise, False to return the found instance;
|
|
1133
|
+
Default is True.
|
|
1134
|
+
|
|
1135
|
+
Returns:
|
|
1136
|
+
The Sonos Controller instance for the specified Spotify Connect device.
|
|
1137
|
+
|
|
1138
|
+
Raises:
|
|
1139
|
+
SpotifyApiError:
|
|
1140
|
+
If a Sonos Controller instance could not be found for the device id.
|
|
1141
|
+
|
|
1142
|
+
You should invoke "if (scDevice.IsSonos):" logic prior to calling this method, as
|
|
1143
|
+
a Sonos Controller instance will not be created for non-Sonos devices and an exception
|
|
1144
|
+
will be raised!
|
|
1145
|
+
"""
|
|
1146
|
+
apiMethodName:str = "GetSonosPlayer"
|
|
1147
|
+
|
|
1148
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1149
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1150
|
+
|
|
1151
|
+
result:SpotifyConnectDevice = None
|
|
1152
|
+
|
|
1153
|
+
# validations.
|
|
1154
|
+
if (device is None):
|
|
1155
|
+
raise SpotifyApiError(SAAppMessages.ARGUMENT_REQUIRED_ERROR % (apiMethodName, 'device'), logsi=_logsi)
|
|
1156
|
+
if (not isinstance(returnCoordinator, bool)):
|
|
1157
|
+
returnCoordinator = True
|
|
1158
|
+
|
|
1159
|
+
# get Sonos Controller instance for device name.
|
|
1160
|
+
sonosPlayer:SoCo = self._SonosPlayers.get(device.DiscoveryResult.HostIpAddress, None)
|
|
1161
|
+
|
|
1162
|
+
# if not found then it's an error.
|
|
1163
|
+
if (sonosPlayer is None):
|
|
1164
|
+
raise SpotifyApiError("Could not find Sonos Controller instance for device: %s" % (device.Title), None, logsi=_logsi)
|
|
1165
|
+
|
|
1166
|
+
# trace.
|
|
1167
|
+
_logsi.LogDictionary(SILevel.Verbose, "Sonos Controller instance for device: %s (speaker_info)" % (device.Title), sonosPlayer.speaker_info)
|
|
1168
|
+
|
|
1169
|
+
# does caller want the group coordinator?
|
|
1170
|
+
if (returnCoordinator):
|
|
1171
|
+
|
|
1172
|
+
# TEST TODO - use to test Sonos coordinator functionality.
|
|
1173
|
+
# _logsi.LogObject(SILevel.Verbose, "Sonos Controller instance for device: %s" % (device.Title), sonosPlayer, colorValue=SIColors.Red)
|
|
1174
|
+
#sonosPlayer._is_coordinator = False
|
|
1175
|
+
|
|
1176
|
+
# is Sonos Controller instance the group coordinator?
|
|
1177
|
+
if (sonosPlayer.is_coordinator):
|
|
1178
|
+
|
|
1179
|
+
# yes - nothing else to do since it's already a group coordinator.
|
|
1180
|
+
_logsi.LogVerbose("Sonos Controller instance is a group coordinator")
|
|
1181
|
+
|
|
1182
|
+
else:
|
|
1183
|
+
|
|
1184
|
+
# no - try to determine the group coordinator.
|
|
1185
|
+
_logsi.LogVerbose("Sonos Controller instance is NOT a group coordinator for device: %s" % (device.Title))
|
|
1186
|
+
|
|
1187
|
+
# is the device part of a group? if so, then use the `group.coordinator` value (if present).
|
|
1188
|
+
sonosPlayerCoordinator:SoCo = None
|
|
1189
|
+
if (sonosPlayer.group is not None):
|
|
1190
|
+
if (sonosPlayer.group.coordinator is not None):
|
|
1191
|
+
sonosPlayerCoordinator = sonosPlayer.group.coordinator
|
|
1192
|
+
sonosPlayer = sonosPlayerCoordinator
|
|
1193
|
+
_logsi.LogVerbose("Sonos Controller group coordinator \"%s\" (%s) will be used for device: %s" % (sonosPlayerCoordinator.player_name, sonosPlayerCoordinator.ip_address, device.Title))
|
|
1194
|
+
_logsi.LogDictionary(SILevel.Verbose, "Sonos Controller instance for device: %s (speaker_info)" % (device.Title), sonosPlayerCoordinator.speaker_info)
|
|
1195
|
+
|
|
1196
|
+
# if group coordinator could not be found, then log a trace message indicating
|
|
1197
|
+
# this and that subsequent Sonos Controller operations will probably fail.
|
|
1198
|
+
# this only seems to happen for "orphaned" Sonos devices (e.g. uid="")
|
|
1199
|
+
# zone_group_state.groups = [ZoneGroup(uid='RINCON_5CAAFDF4E8DE01400:orphan', coordinator=None, members={SoCo("192.168.50.79")}), etc
|
|
1200
|
+
if (sonosPlayerCoordinator is None):
|
|
1201
|
+
_logsi.LogVerbose("Sonos Controller group coordinator could not be determined for device: %s; subsequent Sonos Controller API functions will probably fail!" % (device.Title), colorValue=SIColors.Red)
|
|
1202
|
+
_logsi.LogObject(SILevel.Verbose, "Sonos Controller instance for device: %s (group)" % (device.Title), sonosPlayer.group)
|
|
1203
|
+
|
|
1204
|
+
# return Sonos Controller instance to caller.
|
|
1205
|
+
return sonosPlayer
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
def GetSpotifyDeviceIDFromName(
|
|
1209
|
+
self,
|
|
1210
|
+
name:str
|
|
1211
|
+
) -> str:
|
|
1212
|
+
"""
|
|
1213
|
+
Retrieve the Spotify deviceID from provided user-friendly name.
|
|
1214
|
+
|
|
1215
|
+
DeviceId is just the md5 hash representation of the user-friendly-name.
|
|
1216
|
+
"""
|
|
1217
|
+
if (name is None):
|
|
1218
|
+
name = ""
|
|
1219
|
+
|
|
1220
|
+
deviceId:str = hashlib.md5(name.encode()).hexdigest()
|
|
1221
|
+
return deviceId
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def RefreshDynamicDevices(
|
|
1225
|
+
self,
|
|
1226
|
+
) -> SpotifyConnectDevice:
|
|
1227
|
+
"""
|
|
1228
|
+
Refreshes dynamic Spotify Connect devices, and also determines the currently
|
|
1229
|
+
active device (if any).
|
|
1230
|
+
|
|
1231
|
+
Returns:
|
|
1232
|
+
A SpotifyConnectDevice object that contains the active device if one was found;
|
|
1233
|
+
otherwise, null to indicate no active device.
|
|
1234
|
+
|
|
1235
|
+
Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
|
|
1236
|
+
process, but exist in the player device list. These are usually Spotify Connect
|
|
1237
|
+
web or mobile players with temporary device id's.
|
|
1238
|
+
|
|
1239
|
+
The object returned is a copy of the internal master device entry; any changes made
|
|
1240
|
+
to the copy do not affect the internal master device entry.
|
|
1241
|
+
|
|
1242
|
+
<details>
|
|
1243
|
+
<summary>Sample Code</summary>
|
|
1244
|
+
```python
|
|
1245
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/RefreshDynamicDevices.py
|
|
1246
|
+
```
|
|
1247
|
+
</details>
|
|
1248
|
+
"""
|
|
1249
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1250
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1251
|
+
|
|
1252
|
+
try:
|
|
1253
|
+
|
|
1254
|
+
# trace.
|
|
1255
|
+
apiMethodParms:SIMethodParmListContext = _logsi.EnterMethodParmList(SILevel.Debug)
|
|
1256
|
+
_logsi.LogMethodParmList(SILevel.Verbose, "Refreshing Spotify Connect dynamic device list", apiMethodParms)
|
|
1257
|
+
|
|
1258
|
+
# trace.
|
|
1259
|
+
if (_logsi.IsOn(SILevel.Verbose)):
|
|
1260
|
+
_logsi.LogDictionary(SILevel.Verbose, "Current Spotify Connect device list (before refresh)", self.GetDevices().ToDictionary(), prettyPrint=True)
|
|
1261
|
+
|
|
1262
|
+
# update player devices known to the Spotify Web API (e.g. dynamic devices).
|
|
1263
|
+
self.UpdatePlayerDevices()
|
|
1264
|
+
|
|
1265
|
+
# update the currently active device (if any).
|
|
1266
|
+
scDevice:SpotifyConnectDevice = self.UpdateActiveDevice()
|
|
1267
|
+
|
|
1268
|
+
# trace.
|
|
1269
|
+
if (_logsi.IsOn(SILevel.Verbose)):
|
|
1270
|
+
_logsi.LogDictionary(SILevel.Verbose, "Current Spotify Connect device list (after refresh)", self.GetDevices().ToDictionary(), prettyPrint=True)
|
|
1271
|
+
if (scDevice is None):
|
|
1272
|
+
_logsi.LogVerbose("Spotify Player playstate device is not present; no active device")
|
|
1273
|
+
else:
|
|
1274
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Player playstate active device instance: %s" % (scDevice.Title), scDevice, excludeNonPublic=True)
|
|
1275
|
+
|
|
1276
|
+
# return real-time active Spotify Player device.
|
|
1277
|
+
return scDevice
|
|
1278
|
+
|
|
1279
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
1280
|
+
except Exception as ex:
|
|
1281
|
+
|
|
1282
|
+
# format unhandled exception.
|
|
1283
|
+
raise SpotifyApiError("%s - Could not retrieve dynamic device list: %s" % (self.name, str(ex)), ex, logsi=_logsi)
|
|
1284
|
+
|
|
1285
|
+
finally:
|
|
1286
|
+
|
|
1287
|
+
# trace.
|
|
1288
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
def RemoveDevice(
|
|
1292
|
+
self,
|
|
1293
|
+
deviceId:str,
|
|
1294
|
+
dynamicDeviceOnly:bool=False
|
|
1295
|
+
) -> SpotifyConnectDevice:
|
|
1296
|
+
"""
|
|
1297
|
+
Removes existing device entry(s) from the devices collection by device id value.
|
|
1298
|
+
|
|
1299
|
+
Args:
|
|
1300
|
+
deviceId (str):
|
|
1301
|
+
Device id to remove.
|
|
1302
|
+
dynamicDeviceOnly (str):
|
|
1303
|
+
True to only remove the device if it's a dynamic device entry;
|
|
1304
|
+
otherwise, False to remove the device id regardless.
|
|
1305
|
+
Default is False.
|
|
1306
|
+
|
|
1307
|
+
Returns:
|
|
1308
|
+
A SpotifyConnectDevice object that contains the removed device if one was found;
|
|
1309
|
+
otherwise, null to indicate no device was removed.
|
|
1310
|
+
|
|
1311
|
+
It's possible to have multiple devices with the same id (e.g. both a dynamic and a
|
|
1312
|
+
zeroconf registered device).
|
|
1313
|
+
|
|
1314
|
+
Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
|
|
1315
|
+
process, but still exist in the player device list. These are usually Spotify Player
|
|
1316
|
+
client devices (e.g. mobile / web / desktop players) that utilize temporary device id's.
|
|
1317
|
+
|
|
1318
|
+
The object returned is a copy of the internal master device entry; any changes made
|
|
1319
|
+
to the copy do not affect the internal master device entry.
|
|
1320
|
+
|
|
1321
|
+
<details>
|
|
1322
|
+
<summary>Sample Code</summary>
|
|
1323
|
+
```python
|
|
1324
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/AddDynamicDevice.py
|
|
1325
|
+
```
|
|
1326
|
+
</details>
|
|
1327
|
+
"""
|
|
1328
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1329
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1330
|
+
|
|
1331
|
+
try:
|
|
1332
|
+
|
|
1333
|
+
# validations.
|
|
1334
|
+
if (not isinstance(dynamicDeviceOnly,bool)):
|
|
1335
|
+
dynamicDeviceOnly = False
|
|
1336
|
+
|
|
1337
|
+
# remove device from devices collection;
|
|
1338
|
+
# the reversed() function creates an iterator that traverses the list in reverse order.
|
|
1339
|
+
# this ensures that removing an element doesn't affect the indices of the subsequent
|
|
1340
|
+
# elements we're going to iterate over.
|
|
1341
|
+
deviceIdCompare:str = (deviceId or "").lower()
|
|
1342
|
+
scDevice:SpotifyConnectDevice
|
|
1343
|
+
for idx in reversed(range(len(self._SpotifyConnectDevices.Items))):
|
|
1344
|
+
|
|
1345
|
+
scDevice = self._SpotifyConnectDevices.Items[idx]
|
|
1346
|
+
|
|
1347
|
+
# did we find a match by device id?
|
|
1348
|
+
if (deviceIdCompare == (scDevice.Id or "").lower()):
|
|
1349
|
+
|
|
1350
|
+
isFound:bool = True
|
|
1351
|
+
|
|
1352
|
+
# get dynamic device status.
|
|
1353
|
+
isDynamic:bool = False
|
|
1354
|
+
if (scDevice.DiscoveryResult is not None):
|
|
1355
|
+
isDynamic = scDevice.DiscoveryResult.IsDynamicDevice
|
|
1356
|
+
|
|
1357
|
+
# are we checking for dynamic device? if so, and it's NOT dynamic,
|
|
1358
|
+
# then don't delete it!
|
|
1359
|
+
if (dynamicDeviceOnly) and (not isDynamic):
|
|
1360
|
+
isFound = False
|
|
1361
|
+
|
|
1362
|
+
# are we deleting this device?
|
|
1363
|
+
if (isFound):
|
|
1364
|
+
|
|
1365
|
+
# remove the device.
|
|
1366
|
+
scDevice:SpotifyConnectDevice = copy.deepcopy(self._SpotifyConnectDevices.Items.pop(idx))
|
|
1367
|
+
_logsi.LogVerbose("Removed SpotifyConnectDevices collection entry: %s - %s" % (scDevice.Title, scDevice.DiscoveryResult.Description), colorValue=SIColors.Orange)
|
|
1368
|
+
|
|
1369
|
+
# raise event.
|
|
1370
|
+
self._RaiseDeviceRemoved(scDevice)
|
|
1371
|
+
|
|
1372
|
+
except SpotifyApiError: raise # pass handled exceptions on thru
|
|
1373
|
+
except Exception as ex:
|
|
1374
|
+
|
|
1375
|
+
# format unhandled exception.
|
|
1376
|
+
raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format("RemoveDevice", str(ex)), ex, logsi=_logsi)
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
def UpdatePlayerDevices(
|
|
1380
|
+
self,
|
|
1381
|
+
playerDevices:list[Device]=None,
|
|
1382
|
+
) -> None:
|
|
1383
|
+
"""
|
|
1384
|
+
Adds a list of dynamic player device entries to the devices collection, and removes any
|
|
1385
|
+
existing dynamic devices from the collection that are not in the `playerDevices` list.
|
|
1386
|
+
|
|
1387
|
+
Args:
|
|
1388
|
+
playerDevices (list[Device]):
|
|
1389
|
+
If specified, the list of current player devices obtained via a previous call to
|
|
1390
|
+
the Spotify Web API; otherwise, null to call the Spotify Web API to retrieve the list.
|
|
1391
|
+
|
|
1392
|
+
Dynamic devices are Spotify Connect devices that are not found in Zeroconf discovery
|
|
1393
|
+
process, but still exist in the player device list. These are usually Spotify Player
|
|
1394
|
+
client devices (e.g. mobile / web / desktop players) that utilize temporary device id's.
|
|
1395
|
+
|
|
1396
|
+
<details>
|
|
1397
|
+
<summary>Sample Code</summary>
|
|
1398
|
+
```python
|
|
1399
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/UpdatePlayerDevices.py
|
|
1400
|
+
```
|
|
1401
|
+
</details>
|
|
1402
|
+
"""
|
|
1403
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1404
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1405
|
+
|
|
1406
|
+
# validations.
|
|
1407
|
+
if (playerDevices is not None) and (not isinstance(playerDevices, list)):
|
|
1408
|
+
return
|
|
1409
|
+
|
|
1410
|
+
# if playerDevices object not supplied, then go get them.
|
|
1411
|
+
if (playerDevices is None):
|
|
1412
|
+
playerDevices:list[Device] = self._SpotifyClientInstance.GetPlayerDevices(refresh=True)
|
|
1413
|
+
|
|
1414
|
+
# reset device list indicators, just in case any were disconnected and are not in the
|
|
1415
|
+
# current player device list.
|
|
1416
|
+
for scDevice in self._SpotifyConnectDevices.Items:
|
|
1417
|
+
scDevice.IsInDeviceList = False
|
|
1418
|
+
|
|
1419
|
+
# add dynamic devices.
|
|
1420
|
+
scDevice:SpotifyConnectDevice
|
|
1421
|
+
device:Device
|
|
1422
|
+
for device in playerDevices:
|
|
1423
|
+
|
|
1424
|
+
# is the player device present in the collection? if not, then add it.
|
|
1425
|
+
if (not self._SpotifyConnectDevices.ContainsDeviceId(device.Id)):
|
|
1426
|
+
self.AddDynamicDevice(device)
|
|
1427
|
+
|
|
1428
|
+
# check all registered Spotify Connect device entries.
|
|
1429
|
+
for scDevice in self._SpotifyConnectDevices.Items:
|
|
1430
|
+
|
|
1431
|
+
# do we have a match on the device id?
|
|
1432
|
+
if (scDevice.DeviceInfo.DeviceId == device.Id) and ((device.Id or "") != ""):
|
|
1433
|
+
|
|
1434
|
+
# indicate the device is in the available device list.
|
|
1435
|
+
scDevice.IsInDeviceList = True
|
|
1436
|
+
|
|
1437
|
+
# is the device an alias entry?
|
|
1438
|
+
# if Spotify Web API device entry is an alias, then the device id and device name
|
|
1439
|
+
# values will USUALLY be the same (e.g. a device id value) and the SpotifyConnectDevices
|
|
1440
|
+
# collection entry's DeviceInfo.RemoteName value will be an empty string (e.g. "").
|
|
1441
|
+
if (scDevice.DeviceInfo.HasAliases) and (device.Id == device.Name):
|
|
1442
|
+
|
|
1443
|
+
# yes - DO NOT change the RemoteName value, as an alias is in use.
|
|
1444
|
+
_logsi.LogVerbose("Detected Spotify Connect Alias in use for SpotifyConnectDevices collection entry %s [%s]; first Alias name is \"%s\"; RemoteName is \"%s\"" % (scDevice.Title, scDevice.DiscoveryResult.Description, scDevice.DeviceInfo.Aliases[0].Name, scDevice.DeviceInfo.RemoteName), colorValue=SIColors.Coral)
|
|
1445
|
+
|
|
1446
|
+
else:
|
|
1447
|
+
|
|
1448
|
+
# did the player device remote name change?
|
|
1449
|
+
# if so, then update the collection RemoteName as well as the Name.
|
|
1450
|
+
# this can happen for devices that are added to / removed from groups
|
|
1451
|
+
# and do not correctly inform interested parties via a zeroconf
|
|
1452
|
+
# OnServiceStateChange event that the name has changed (e.g. Denon HEOS devices, etc).
|
|
1453
|
+
if (scDevice.DeviceInfo.RemoteName != device.Name) and ((device.Name or "") != ""):
|
|
1454
|
+
_logsi.LogVerbose("Detected Spotify Connect RemoteName change for SpotifyConnectDevices collection entry %s [%s]; updated Spotify Web API PlayerDevice Name is \"%s\" - this is usually caused by a Speaker Group membership change" % (scDevice.Title, scDevice.DiscoveryResult.Description, device.Name), colorValue=SIColors.Coral)
|
|
1455
|
+
scDevice.DeviceInfo.RemoteName = device.Name
|
|
1456
|
+
scDevice.Name = device.Name
|
|
1457
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
1458
|
+
# ???? - we should probably call getInfo again to update DeviceInfo since GroupStatus has
|
|
1459
|
+
# probably changed from "NONE" to "GROUP" or vice versa!
|
|
1460
|
+
# we will start with this to see how it goes.
|
|
1461
|
+
self._RaiseDeviceUpdated(scDevice)
|
|
1462
|
+
|
|
1463
|
+
# don't need to search any more devices since we found the device id.
|
|
1464
|
+
break
|
|
1465
|
+
|
|
1466
|
+
# remove stale dynamic devices from results collection;
|
|
1467
|
+
# the reversed() function creates an iterator that traverses the list in reverse order.
|
|
1468
|
+
# this ensures that removing an element doesn't affect the indices of the subsequent
|
|
1469
|
+
# elements we're going to iterate over.
|
|
1470
|
+
for idx in reversed(range(len(self._SpotifyConnectDevices.Items))):
|
|
1471
|
+
|
|
1472
|
+
# only process dynamic devices.
|
|
1473
|
+
scDevice = self._SpotifyConnectDevices.Items[idx]
|
|
1474
|
+
if (scDevice.DiscoveryResult.IsDynamicDevice):
|
|
1475
|
+
|
|
1476
|
+
# process all player devices.
|
|
1477
|
+
wasFound:bool = False
|
|
1478
|
+
playerDevice:Device
|
|
1479
|
+
for playerDevice in playerDevices:
|
|
1480
|
+
if (scDevice.Id == playerDevice.Id):
|
|
1481
|
+
wasFound = True
|
|
1482
|
+
break
|
|
1483
|
+
|
|
1484
|
+
# if collection entry was not found in the player device list then remove it.
|
|
1485
|
+
if (not wasFound):
|
|
1486
|
+
|
|
1487
|
+
# remove the device.
|
|
1488
|
+
scDevice:SpotifyConnectDevice = copy.deepcopy(self._SpotifyConnectDevices.Items.pop(idx))
|
|
1489
|
+
_logsi.LogVerbose("Removed SpotifyConnectDevices collection entry: %s - %s" % (scDevice.Title, scDevice.DiscoveryResult.Description), colorValue=SIColors.Orange)
|
|
1490
|
+
|
|
1491
|
+
# raise event.
|
|
1492
|
+
self._RaiseDeviceRemoved(scDevice)
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
def UpdateActiveDevice(
|
|
1496
|
+
self,
|
|
1497
|
+
playerState:PlayerPlayState=None
|
|
1498
|
+
) -> SpotifyConnectDevice:
|
|
1499
|
+
"""
|
|
1500
|
+
Updates the currently active device based on playerState.
|
|
1501
|
+
|
|
1502
|
+
Args:
|
|
1503
|
+
playerState (PlayerPlayState):
|
|
1504
|
+
Current player state obtained via a call to `GetPlayerPlaybackState` method
|
|
1505
|
+
which will be used to set the active device in the `Items` collection;
|
|
1506
|
+
otherwise, null to bypass active device set.
|
|
1507
|
+
|
|
1508
|
+
Returns:
|
|
1509
|
+
The currently active Spotify Player device instance if one was found; otherwise, null.
|
|
1510
|
+
|
|
1511
|
+
The Spotify Web API GetPlayerPlaybackState method is called to retrieve the active device
|
|
1512
|
+
information.
|
|
1513
|
+
|
|
1514
|
+
The object returned is a copy of the internal master device entry; any changes made
|
|
1515
|
+
to the copy do not affect the internal master device entry.
|
|
1516
|
+
|
|
1517
|
+
<details>
|
|
1518
|
+
<summary>Sample Code</summary>
|
|
1519
|
+
```python
|
|
1520
|
+
.. include:: ../../docs/include/samplecode/SpotifyConnectDirectoryTask/UpdateActiveDevice.py
|
|
1521
|
+
```
|
|
1522
|
+
</details>
|
|
1523
|
+
"""
|
|
1524
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1525
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1526
|
+
|
|
1527
|
+
result:SpotifyConnectDevice = None
|
|
1528
|
+
|
|
1529
|
+
# if playerState object not supplied, then go get it.
|
|
1530
|
+
if (playerState is None):
|
|
1531
|
+
playerState:PlayerPlayState = self._SpotifyClientInstance.GetPlayerPlaybackState(additionalTypes=SpotifyMediaTypes.EPISODE.value)
|
|
1532
|
+
|
|
1533
|
+
# if valid object then process it.
|
|
1534
|
+
if (isinstance(playerState, PlayerPlayState)):
|
|
1535
|
+
|
|
1536
|
+
# we do this in case the active device is a "restricted" device; if it is, then it may
|
|
1537
|
+
# not show up as the active device (common issue with Sonos devices).
|
|
1538
|
+
# in this case, it WILL show up in the player state device property as the active device.
|
|
1539
|
+
# if it IS active, then we will set the active flag.
|
|
1540
|
+
# note that the PlayerPlayState device is a name and not a device id; it's device id
|
|
1541
|
+
# could also be set to null if it's a restricted device (e.g. Sonos).
|
|
1542
|
+
if playerState.Device is not None:
|
|
1543
|
+
|
|
1544
|
+
# reset active device status for all devices; set active device status if found.
|
|
1545
|
+
device = playerState.Device
|
|
1546
|
+
for scDevice in self._SpotifyConnectDevices.Items:
|
|
1547
|
+
scDevice.IsActiveDevice = False
|
|
1548
|
+
|
|
1549
|
+
# is a device id present?
|
|
1550
|
+
if ((device.Id or "") != ""):
|
|
1551
|
+
|
|
1552
|
+
# yes - compare on the device id.
|
|
1553
|
+
if (device.Id == scDevice.Id):
|
|
1554
|
+
_logsi.LogVerbose("Spotify Connect active device detected (By Id): %s" % (scDevice.Title))
|
|
1555
|
+
scDevice.IsActiveDevice = True
|
|
1556
|
+
scDevice.IsRestricted = playerState.Device.IsRestricted
|
|
1557
|
+
result = copy.deepcopy(scDevice)
|
|
1558
|
+
|
|
1559
|
+
# no - compare on the device name.
|
|
1560
|
+
elif (device.Name == scDevice.Name):
|
|
1561
|
+
_logsi.LogVerbose("Spotify Connect active device detected (By Name): %s" % (scDevice.Title))
|
|
1562
|
+
scDevice.IsActiveDevice = True
|
|
1563
|
+
scDevice.IsRestricted = playerState.Device.IsRestricted
|
|
1564
|
+
result = copy.deepcopy(scDevice)
|
|
1565
|
+
|
|
1566
|
+
# returns null if the `playerState` argument was not specified;
|
|
1567
|
+
# otherwise, the currently active Spotify Player device instance as determined by
|
|
1568
|
+
# the Spotify Player PlayState (could be null if no active device).
|
|
1569
|
+
return result
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
def OnServiceInfoAddedUpdatedChromecast(
|
|
1573
|
+
self,
|
|
1574
|
+
zeroconfDiscoveryResult:ZeroconfDiscoveryResult,
|
|
1575
|
+
uuid:UUID,
|
|
1576
|
+
serviceName:str,
|
|
1577
|
+
serviceType:str,
|
|
1578
|
+
) -> None:
|
|
1579
|
+
"""
|
|
1580
|
+
Called when a Chromecast MDNS service record has been discovered
|
|
1581
|
+
or updated for a Chromecast device.
|
|
1582
|
+
|
|
1583
|
+
Args:
|
|
1584
|
+
zeroconfDiscoveryResult (ZeroconfDiscoveryResult):
|
|
1585
|
+
A `ZeroconfDiscoveryResult` object that contains discovery details.
|
|
1586
|
+
uuid (UUID):
|
|
1587
|
+
The cast's uuid, which is the dictionary key to find the chromecast
|
|
1588
|
+
metadata in CastBrowser.devices and CastBrowser.services collection.
|
|
1589
|
+
serviceName (str):
|
|
1590
|
+
First known MDNS service name or host:port.
|
|
1591
|
+
serviceType (str):
|
|
1592
|
+
Service call type (e.g. "add_cast", "update_cast").
|
|
1593
|
+
|
|
1594
|
+
This method will be called in a thread-safe manner, as the caller is using
|
|
1595
|
+
the `_Zeroconf_RLock` object to control access.
|
|
1596
|
+
"""
|
|
1597
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1598
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1599
|
+
|
|
1600
|
+
try:
|
|
1601
|
+
|
|
1602
|
+
# trace.
|
|
1603
|
+
_logsi.EnterMethod(SILevel.Debug)
|
|
1604
|
+
|
|
1605
|
+
# add / update discovered results by serviceinfo key value.
|
|
1606
|
+
# examples:
|
|
1607
|
+
# - Name="SHIELD-Android-TV-72735c92dead1a2d62df0229b1590a65._googlecast._tcp.local.", Key="72735c92-dead-1a2d-62df-0229b1590a65"
|
|
1608
|
+
# - Name="LS50-Wireless-II-5b1bb9c433d5bc1f7c8bf208c0cdda3f._googlecast._tcp.local.", Key="5b1bb9c4-33d5-bc1f-7c8b-f208c0cdda3f"
|
|
1609
|
+
|
|
1610
|
+
# note that for "Google-Cast-Group-x" entries, the name will vary slightly with each member of
|
|
1611
|
+
# the group, but the key value will be the same. this is important because for groups you want
|
|
1612
|
+
# to update the same group KEY as group updates are made (e.g. group / ungroup devices).
|
|
1613
|
+
# examples:
|
|
1614
|
+
# - Name="Google-Cast-Group-631B535F21D3408A8EE2C52BEBDB99E7._googlecast._tcp.local.", Key="631b535f-21d3-408a-8ee2-c52bebdb99e7"
|
|
1615
|
+
# - Name="Google-Cast-Group-631B535F21D3408A8EE2C52BEBDB99E7-1._googlecast._tcp.local.", Key="631b535f-21d3-408a-8ee2-c52bebdb99e7"
|
|
1616
|
+
# - Name="Google-Cast-Group-631B535F21D3408A8EE2C52BEBDB99E7-2._googlecast._tcp.local.", Key="631b535f-21d3-408a-8ee2-c52bebdb99e7"
|
|
1617
|
+
|
|
1618
|
+
# get cast device instance by discovery key.
|
|
1619
|
+
# note that we cannot use discovery name here, as Google Groups use different
|
|
1620
|
+
# names that refer to the same key value!
|
|
1621
|
+
scDevice:SpotifyConnectDevice = None
|
|
1622
|
+
idx:int = self._SpotifyConnectDevices.GetDeviceIndexByDiscoveryKey(zeroconfDiscoveryResult.Key)
|
|
1623
|
+
if (idx == -1):
|
|
1624
|
+
|
|
1625
|
+
# trace.
|
|
1626
|
+
_logsi.LogVerbose("Creating new SpotifyConnectDevice instance from CastInfo data: \"%s\" (%s) [key=%s]" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name, zeroconfDiscoveryResult.Key))
|
|
1627
|
+
|
|
1628
|
+
# connect to the device using CastInfo to get more details.
|
|
1629
|
+
castInfo:CastInfo = self._CastBrowser.services[uuid]
|
|
1630
|
+
_logsi.LogVerbose("Retrieving Chromecast device instance from CastInfo object \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
1631
|
+
castDevice:Chromecast = get_chromecast_from_cast_info(
|
|
1632
|
+
cast_info=castInfo,
|
|
1633
|
+
zconf=self._ZeroconfInstance,
|
|
1634
|
+
tries=2,
|
|
1635
|
+
retry_wait=0.5,
|
|
1636
|
+
timeout=5)
|
|
1637
|
+
|
|
1638
|
+
# wait for the device to become ready (10 seconds max).
|
|
1639
|
+
castDevice.wait(10)
|
|
1640
|
+
_logsi.LogObject(SILevel.Verbose, "Chromecast device instance \"%s\" (%s)" % (castInfo.friendly_name, zeroconfDiscoveryResult.Name), castDevice)
|
|
1641
|
+
|
|
1642
|
+
# create a new Spotify Connect Device instance.
|
|
1643
|
+
scDevice = SpotifyConnectDevice()
|
|
1644
|
+
scDevice.Name = zeroconfDiscoveryResult.DeviceName
|
|
1645
|
+
scDevice.Id = self.GetSpotifyDeviceIDFromName(castInfo.friendly_name)
|
|
1646
|
+
|
|
1647
|
+
# set zeroconf discovery result properties.
|
|
1648
|
+
scDevice.DiscoveryResult = zeroconfDiscoveryResult
|
|
1649
|
+
|
|
1650
|
+
# Spotify Connect GetInformation is called as part of the cast app launch process,
|
|
1651
|
+
# and cannot be performed yet.
|
|
1652
|
+
|
|
1653
|
+
# set device info properties that we know about; the rest will be filled in
|
|
1654
|
+
# when we connect to the device and the cast `getInfoResponse` is returned.
|
|
1655
|
+
scDevice.DeviceInfo = ZeroconfGetInfo()
|
|
1656
|
+
scDevice.DeviceInfo.DeviceId = scDevice.Id
|
|
1657
|
+
scDevice.DeviceInfo.DeviceType = "CastAudio"
|
|
1658
|
+
scDevice.DeviceInfo.BrandDisplayName = castInfo.manufacturer or "ChromeCast"
|
|
1659
|
+
scDevice.DeviceInfo.ModelDisplayName = castInfo.model_name
|
|
1660
|
+
scDevice.DeviceInfo.RemoteName = castInfo.friendly_name
|
|
1661
|
+
|
|
1662
|
+
# did this device change from a dynamic device (e.g. non-zeroconf) to a zeroconf device?
|
|
1663
|
+
# this can happen if the device is initially added as a dynamic device, then the zeroconf
|
|
1664
|
+
# device is discovered. if so, we will remove the dynamic device entry.
|
|
1665
|
+
spDynamicDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceById(scDevice.DeviceInfo.DeviceId)
|
|
1666
|
+
if (spDynamicDevice is not None):
|
|
1667
|
+
|
|
1668
|
+
# trace.
|
|
1669
|
+
_logsi.LogVerbose("Chromecast Zeroconf is converting SpotifyConnectDevice entry %s from dynamic to zeroconf" % (spDynamicDevice.Title))
|
|
1670
|
+
|
|
1671
|
+
# copy real-time status dynamic properties to zerconf object.
|
|
1672
|
+
scDevice.IsActiveDevice = spDynamicDevice.IsActiveDevice
|
|
1673
|
+
scDevice.IsInDeviceList = spDynamicDevice.IsInDeviceList
|
|
1674
|
+
|
|
1675
|
+
# remove the dynamic device from the collection.
|
|
1676
|
+
self.RemoveDevice(spDynamicDevice.Id, dynamicDeviceOnly=True)
|
|
1677
|
+
|
|
1678
|
+
# is this a Google Cast Group?
|
|
1679
|
+
if (castInfo.cast_type == CAST_TYPE_GROUP):
|
|
1680
|
+
|
|
1681
|
+
# if so, then "add_cast" entries will occur for every member of the group.
|
|
1682
|
+
# we only want to keep the LAST group entry zeroconfDiscoveryResult on file,
|
|
1683
|
+
# and ignore all of the others; otherwise, it creates duplicate entries in the
|
|
1684
|
+
# devices collection for each member device of the group!
|
|
1685
|
+
|
|
1686
|
+
# note that we will query the current multi-zone status if it's a group entry, as
|
|
1687
|
+
# we have to start the SpotifyAppTask on the group leader / coordinator device.
|
|
1688
|
+
|
|
1689
|
+
# example: RemoteName = "Kitchen Parlor", DeviceID = 3fb9b12379e4fa1957d065b4c4568939
|
|
1690
|
+
# - (add_cast) "Google-Cast-Group-B85B00FE5F514A40BF6AFBBCCEB7C8DF-1._googlecast._tcp.local."
|
|
1691
|
+
# HostIPAddr = 192.168.1.14:32139
|
|
1692
|
+
# GroupMembr = "Kitchen Speakers", 192.168.1.14:8009
|
|
1693
|
+
# RemoteName = "Kitchen Parlor"
|
|
1694
|
+
# DeviceID = 3fb9b12379e4fa1957d065b4c4568939
|
|
1695
|
+
# - (add_cast) "Google-Cast-Group-B85B00FE5F514A40BF6AFBBCCEB7C8DF._googlecast._tcp.local."
|
|
1696
|
+
# HostIPAddr = 192.168.1.186:32139
|
|
1697
|
+
# GroupMembr = "Parlor Speakers", 192.168.1.186:8009
|
|
1698
|
+
# RemoteName = "Kitchen Parlor"
|
|
1699
|
+
# DeviceID = 3fb9b12379e4fa1957d065b4c4568939
|
|
1700
|
+
|
|
1701
|
+
# ignore "update_cast" entries, as we don't want to create duplicates.
|
|
1702
|
+
if (serviceType == "update_cast"):
|
|
1703
|
+
_logsi.LogVerbose("SpotifyConnectDevice instance will be ignored for Google Cast Group data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
1704
|
+
return
|
|
1705
|
+
|
|
1706
|
+
# does the device name and id already exist?
|
|
1707
|
+
groupDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByNameAndId(scDevice.Name, scDevice.Id)
|
|
1708
|
+
if (groupDevice is not None):
|
|
1709
|
+
_logsi.LogVerbose("SpotifyConnectDevice instance already exists for Google Cast Group data: \"%s\" (%s), Group RemoteName=%s, Host IP=%s" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name, groupDevice.Title, groupDevice.DiscoveryResult.HostIpTitle))
|
|
1710
|
+
|
|
1711
|
+
# set zeroconf discovery result properties.
|
|
1712
|
+
scDevice.DiscoveryResult = zeroconfDiscoveryResult
|
|
1713
|
+
|
|
1714
|
+
# update existing Spotify Connect Device in devices collection.
|
|
1715
|
+
self._SpotifyConnectDevices.Items[idx] = scDevice
|
|
1716
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
1717
|
+
return
|
|
1718
|
+
|
|
1719
|
+
# register a multizone listener to listen for group change events; these
|
|
1720
|
+
# events will (hopefully) help us to determine the leader of a group.
|
|
1721
|
+
try:
|
|
1722
|
+
|
|
1723
|
+
# is this an add_cast event?
|
|
1724
|
+
if (serviceType == "add_cast"):
|
|
1725
|
+
|
|
1726
|
+
# do we have a multizone controller registered for this device yet?
|
|
1727
|
+
castMultizoneController:MultizoneController = self._CastMultiZoneControllers.get(scDevice.DiscoveryResult.Key, None)
|
|
1728
|
+
if (castMultizoneController is None):
|
|
1729
|
+
|
|
1730
|
+
# trace.
|
|
1731
|
+
_logsi.LogVerbose("Chromecast Multizone Controller Listener is being added for Cast device: %s [%s]" % (scDevice.Title, scDevice.DiscoveryResult.HostIpTitle), colorValue=SIColors.Lavender)
|
|
1732
|
+
|
|
1733
|
+
# add cast device to multizone controller instance. this will add a listener
|
|
1734
|
+
# for multizone status events to be processed, which allows us to keep track
|
|
1735
|
+
# of which device is a group leader.
|
|
1736
|
+
castMultizoneController = MultizoneController(castDevice.uuid)
|
|
1737
|
+
castMultizoneController.register_listener(SpotifyConnectZeroconfCastMultiZoneListener(self, self._ZeroconfInstance, self._Zeroconf_RLock, castMultizoneController, castDevice))
|
|
1738
|
+
castDevice.register_handler(castMultizoneController)
|
|
1739
|
+
castMultizoneController.update_members() # Request an initial status
|
|
1740
|
+
|
|
1741
|
+
# add cast multizone controller to active multizone controllers collection.
|
|
1742
|
+
self._CastMultiZoneControllers[str(castDevice.uuid)] = castMultizoneController
|
|
1743
|
+
|
|
1744
|
+
except Exception as ex:
|
|
1745
|
+
|
|
1746
|
+
# trace.
|
|
1747
|
+
_logsi.LogException("%s - MultiZoneController register_listener Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
|
|
1748
|
+
|
|
1749
|
+
# add new Spotify Connect Device to devices collection.
|
|
1750
|
+
self._SpotifyConnectDevices.Items.append(scDevice)
|
|
1751
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
1752
|
+
|
|
1753
|
+
# trace.
|
|
1754
|
+
_logsi.LogObject(SILevel.Verbose, "Chromecast Zeroconf added SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.ForestGreen)
|
|
1755
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: %s (DeviceInfo / getInfo)" % (scDevice.Title), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
1756
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: %s (DiscoveryResult) [%s]" % (scDevice.Title, scDevice.DiscoveryResult.HostIpTitle), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
1757
|
+
|
|
1758
|
+
# sort devices collection by device name.
|
|
1759
|
+
if (len(self._SpotifyConnectDevices.Items) > 0):
|
|
1760
|
+
self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
|
|
1761
|
+
|
|
1762
|
+
# raise event.
|
|
1763
|
+
self._RaiseDeviceAdded(scDevice)
|
|
1764
|
+
|
|
1765
|
+
else:
|
|
1766
|
+
|
|
1767
|
+
# trace.
|
|
1768
|
+
_logsi.LogDebug("Updating existing SpotifyConnectDevice instance from CastInfo data: \"%s\" (%s) [key=%s]" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name, zeroconfDiscoveryResult.Key))
|
|
1769
|
+
|
|
1770
|
+
# get existing Spotify Connect Device instance.
|
|
1771
|
+
scDevice = self._SpotifyConnectDevices.Items[idx]
|
|
1772
|
+
|
|
1773
|
+
# were any changes made to the zeroconf discovery results?
|
|
1774
|
+
if (not scDevice.DiscoveryResult.Equals(zeroconfDiscoveryResult)):
|
|
1775
|
+
|
|
1776
|
+
# trace.
|
|
1777
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (OLD DeviceInfo / getInfo)" % (scDevice.Title), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
1778
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (OLD DiscoveryResult) [%s]" % (scDevice.Title, scDevice.DiscoveryResult.HostIpTitle), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
1779
|
+
|
|
1780
|
+
# did the device name change? if so, then update name and id properties,
|
|
1781
|
+
# as well as the corresponding getInfo properties.
|
|
1782
|
+
deviceNameChanged:bool = False
|
|
1783
|
+
if (scDevice.Name != zeroconfDiscoveryResult.DeviceName):
|
|
1784
|
+
|
|
1785
|
+
# if Spotify Cast App is active on the cast device then request that it stop
|
|
1786
|
+
# as it will need to re-register the new device name and device id.
|
|
1787
|
+
castAppTask:SpotifyConnectZeroconfCastAppTask = self._CastAppTasks.get(zeroconfDiscoveryResult.Key, None)
|
|
1788
|
+
if (castAppTask is not None):
|
|
1789
|
+
if (castAppTask.is_alive()):
|
|
1790
|
+
_logsi.LogVerbose("%s - Stopping Spotify Cast App task (due to device rename)" % (self.name))
|
|
1791
|
+
castAppTask.IsStopRequested = True
|
|
1792
|
+
castAppTask.join()
|
|
1793
|
+
_logsi.LogVerbose("%s - Spotify Cast App task was stopped successfully (due to device rename)" % (self.name))
|
|
1794
|
+
self._CastAppTasks.pop(zeroconfDiscoveryResult.Key, None)
|
|
1795
|
+
|
|
1796
|
+
# update name and id properties, as well as the corresponding getInfo properties.
|
|
1797
|
+
newDeviceName:str = zeroconfDiscoveryResult.DeviceName
|
|
1798
|
+
newDeviceId:str = self.GetSpotifyDeviceIDFromName(zeroconfDiscoveryResult.DeviceName)
|
|
1799
|
+
_logsi.LogVerbose("Chromecast Zeroconf SpotifyConnectDevice entry name and id changed from %s to \"%s\" (%s)" % (scDevice.Title, newDeviceName, newDeviceId))
|
|
1800
|
+
scDevice.Name = newDeviceName
|
|
1801
|
+
scDevice.Id = newDeviceId
|
|
1802
|
+
scDevice.DeviceInfo.DeviceId = newDeviceId
|
|
1803
|
+
scDevice.DeviceInfo.RemoteName = newDeviceName
|
|
1804
|
+
deviceNameChanged = True
|
|
1805
|
+
|
|
1806
|
+
# is this a Google Cast Group?
|
|
1807
|
+
# if so, AND the device name did not change, then ignore the update since
|
|
1808
|
+
# we want the first device discovered in the group to be the coordinator by default.
|
|
1809
|
+
# this might change later, but we will detect that using calls to multizone status.
|
|
1810
|
+
if (zeroconfDiscoveryResult.IsChromeCastGroup) and (not deviceNameChanged):
|
|
1811
|
+
_logsi.LogObject(SILevel.Verbose, "SpotifyConnectDevice info: \"%s\" (ignored; Cast Group device update)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
1812
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: %s (NEW DiscoveryResult ignored) [%s]" % (scDevice.Title, zeroconfDiscoveryResult.HostIpTitle), zeroconfDiscoveryResult, excludeNonPublic=True)
|
|
1813
|
+
return
|
|
1814
|
+
|
|
1815
|
+
# set zeroconf discovery result properties.
|
|
1816
|
+
scDevice.DiscoveryResult = zeroconfDiscoveryResult
|
|
1817
|
+
|
|
1818
|
+
# update existing Spotify Connect Device in devices collection.
|
|
1819
|
+
self._SpotifyConnectDevices.Items[idx] = scDevice
|
|
1820
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
1821
|
+
|
|
1822
|
+
# trace.
|
|
1823
|
+
_logsi.LogObject(SILevel.Verbose, "Chromecast Zeroconf updated SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.ForestGreen)
|
|
1824
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: %s (DeviceInfo / getInfo)" % (scDevice.Title), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
1825
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: %s (DiscoveryResult) [%s]" % (scDevice.Title, scDevice.DiscoveryResult.HostIpTitle), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
1826
|
+
|
|
1827
|
+
# sort devices collection by device name.
|
|
1828
|
+
if (len(self._SpotifyConnectDevices.Items) > 0):
|
|
1829
|
+
self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
|
|
1830
|
+
|
|
1831
|
+
# raise event.
|
|
1832
|
+
self._RaiseDeviceUpdated(scDevice)
|
|
1833
|
+
|
|
1834
|
+
else:
|
|
1835
|
+
|
|
1836
|
+
# trace.
|
|
1837
|
+
_logsi.LogObject(SILevel.Verbose, "SpotifyConnectDevice info: \"%s\" (ignored; DiscoveryResult did not change)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
1838
|
+
|
|
1839
|
+
except Exception as ex:
|
|
1840
|
+
|
|
1841
|
+
# trace.
|
|
1842
|
+
_logsi.LogException("%s - Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
|
|
1843
|
+
|
|
1844
|
+
# ignore exceptions, as there is nothing we can do at this point.
|
|
1845
|
+
|
|
1846
|
+
finally:
|
|
1847
|
+
|
|
1848
|
+
# trace.
|
|
1849
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
1850
|
+
|
|
1851
|
+
|
|
1852
|
+
def OnServiceInfoRemovedChromecast(
|
|
1853
|
+
self,
|
|
1854
|
+
zeroconfDiscoveryResult: ZeroconfDiscoveryResult,
|
|
1855
|
+
uuid:UUID,
|
|
1856
|
+
serviceName:str,
|
|
1857
|
+
castInfo:CastInfo
|
|
1858
|
+
) -> None:
|
|
1859
|
+
"""
|
|
1860
|
+
Called when a cast has been lost (MDNS info expired or host down).
|
|
1861
|
+
|
|
1862
|
+
Args:
|
|
1863
|
+
zeroconfDiscoveryResult (ZeroconfDiscoveryResult):
|
|
1864
|
+
A `ZeroconfDiscoveryResult` object that contains discovery details.
|
|
1865
|
+
uuid (UUID):
|
|
1866
|
+
The cast's uuid, which is the dictionary key to find the chromecast
|
|
1867
|
+
metadata in CastBrowser.devices collection.
|
|
1868
|
+
serviceName (str):
|
|
1869
|
+
Last valid MDNS service name or host:port.
|
|
1870
|
+
castInfo (CastInfo):
|
|
1871
|
+
CastInfo for the service to aid cleanup.
|
|
1872
|
+
|
|
1873
|
+
This could be called under the following conditions:
|
|
1874
|
+
- when a Chromecast device has logged out of zeroconf and left the network.
|
|
1875
|
+
- transfer playback failed (e.g. device restriction, etc).
|
|
1876
|
+
|
|
1877
|
+
This method will be called in a thread-safe manner, as the caller is using
|
|
1878
|
+
the `_Zeroconf_RLock` object to control access.
|
|
1879
|
+
"""
|
|
1880
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1881
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1882
|
+
|
|
1883
|
+
try:
|
|
1884
|
+
|
|
1885
|
+
# trace.
|
|
1886
|
+
_logsi.EnterMethod(SILevel.Debug)
|
|
1887
|
+
|
|
1888
|
+
# get cast device instance by discovery key.
|
|
1889
|
+
# note that we cannot use discovery name here, as Google Groups use different
|
|
1890
|
+
# names that refer to the same key value!
|
|
1891
|
+
idx:int = self._SpotifyConnectDevices.GetDeviceIndexByDiscoveryKey(zeroconfDiscoveryResult.Key)
|
|
1892
|
+
if (idx == -1):
|
|
1893
|
+
|
|
1894
|
+
# trace.
|
|
1895
|
+
_logsi.LogVerbose("SpotifyConnectDevice instance could not be found to remove: \"%s\" (%s) [%s]" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name, zeroconfDiscoveryResult.Key))
|
|
1896
|
+
|
|
1897
|
+
else:
|
|
1898
|
+
|
|
1899
|
+
# unregister the multizone listener, if one was registered.
|
|
1900
|
+
try:
|
|
1901
|
+
|
|
1902
|
+
# do we have a multizone controller registered for this device?
|
|
1903
|
+
castMultizoneController:MultizoneController = self._CastMultiZoneControllers.get(zeroconfDiscoveryResult.Key, None)
|
|
1904
|
+
if (castMultizoneController is not None):
|
|
1905
|
+
|
|
1906
|
+
# trace.
|
|
1907
|
+
_logsi.LogVerbose("Chromecast Multizone Controller Listener is being removed: \"%s\" (%s) [%s]" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name, zeroconfDiscoveryResult.Key), colorValue=SIColors.Lavender)
|
|
1908
|
+
|
|
1909
|
+
# tear down the listener.
|
|
1910
|
+
castMultizoneController.tear_down()
|
|
1911
|
+
|
|
1912
|
+
# remove cast multizone controller from active multizone controllers collection.
|
|
1913
|
+
self._CastMultiZoneControllers.pop(zeroconfDiscoveryResult.Key, None)
|
|
1914
|
+
|
|
1915
|
+
except Exception as ex:
|
|
1916
|
+
|
|
1917
|
+
# trace.
|
|
1918
|
+
_logsi.LogException("%s - MultiZoneController register_listener Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
|
|
1919
|
+
|
|
1920
|
+
# trace.
|
|
1921
|
+
_logsi.LogVerbose("Removing existing SpotifyConnectDevice instance from CastInfo data: \"%s\" (%s) [%s]" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name, zeroconfDiscoveryResult.Key))
|
|
1922
|
+
|
|
1923
|
+
# remove existing Spotify Connect Device instance from devices collection.
|
|
1924
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.Items.pop(idx)
|
|
1925
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
1926
|
+
|
|
1927
|
+
# trace.
|
|
1928
|
+
_logsi.LogObject(SILevel.Verbose, "Chromecast Zeroconf removed SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.DarkOrange)
|
|
1929
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: %s (DeviceInfo / getInfo)" % (scDevice.Title), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
1930
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: %s (DiscoveryResult) [%s]" % (scDevice.Title, scDevice.DiscoveryResult.HostIpTitle), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
1931
|
+
|
|
1932
|
+
# raise event.
|
|
1933
|
+
self._RaiseDeviceRemoved(scDevice)
|
|
1934
|
+
|
|
1935
|
+
except Exception as ex:
|
|
1936
|
+
|
|
1937
|
+
# trace.
|
|
1938
|
+
_logsi.LogException("%s - Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
|
|
1939
|
+
|
|
1940
|
+
# ignore exceptions, as there is nothing we can do at this point.
|
|
1941
|
+
|
|
1942
|
+
finally:
|
|
1943
|
+
|
|
1944
|
+
# trace.
|
|
1945
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
1946
|
+
|
|
1947
|
+
|
|
1948
|
+
def OnCastGetInfoResponseReceived(
|
|
1949
|
+
self,
|
|
1950
|
+
uuid:UUID,
|
|
1951
|
+
info:ZeroconfGetInfo
|
|
1952
|
+
) -> None:
|
|
1953
|
+
"""
|
|
1954
|
+
Called when the cast app task has received a Spotify Connect Zeroconf GetInfoResponse
|
|
1955
|
+
data structure.
|
|
1956
|
+
|
|
1957
|
+
Args:
|
|
1958
|
+
uuid (UUID):
|
|
1959
|
+
Spotify Connect device that was added.
|
|
1960
|
+
info (ZeroconfGetInfo):
|
|
1961
|
+
A `ZeroconfGetInfo` instance that contains the GetInfoResponse data.
|
|
1962
|
+
|
|
1963
|
+
This method allows us to update the SpotifyConnectDevice entry with the Spotify Connect
|
|
1964
|
+
Zeroconf device information.
|
|
1965
|
+
"""
|
|
1966
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
1967
|
+
with self._SpotifyConnectDevices_RLock:
|
|
1968
|
+
|
|
1969
|
+
# get device instance.
|
|
1970
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(uuid))
|
|
1971
|
+
if (scDevice is not None):
|
|
1972
|
+
|
|
1973
|
+
# the actual deviceId that was retrieved may be different than the requested deviceId.
|
|
1974
|
+
# this can sometimes occur when activating a group, as getInfoResponse will return the
|
|
1975
|
+
# deviceId of the group coordinator instead of the deviceId of the group itself.
|
|
1976
|
+
# this only seems to happen in non-Google manufactured devices that have not properly
|
|
1977
|
+
# implemented the Cast protocol for grouped devices (e.g. KEF, etc); it never occurs when
|
|
1978
|
+
# casting to groups of devices manufactured by Google!
|
|
1979
|
+
|
|
1980
|
+
if (scDevice.Id != info.DeviceId):
|
|
1981
|
+
|
|
1982
|
+
# trace.
|
|
1983
|
+
if (_logsi.IsOn(SILevel.Verbose)):
|
|
1984
|
+
_logsi.LogVerbose("Received getInfoResponse from group coordinator device \"%s\" (%s); expected group device \"%s\" (OnCastGetInfoResponseReceived)" % (info.RemoteName or str(info.Aliases), info.DeviceId, scDevice.Title), colorValue=SIColors.Red)
|
|
1985
|
+
_logsi.LogObject(SILevel.Verbose, "SpotifyConnectDevice info: \"%s\" (%s) (Group Coordinator DeviceInfo / getInfo, OnCastGetInfoResponseReceived)" % (info.RemoteName or str(info.Aliases), info.DeviceId), info, excludeNonPublic=True, colorValue=SIColors.Red)
|
|
1986
|
+
|
|
1987
|
+
# if device id's don't match then we will NOT overlay the original group's ZeroconfGetInfo structure
|
|
1988
|
+
# as we want to keep it intact, since the getInfoResponse is from the group coordinator.
|
|
1989
|
+
# overlaying it here would cause duplicate device ID's in the collection!
|
|
1990
|
+
# in this case, just populate the ZeroconfResponse base class properties and other basic properties.
|
|
1991
|
+
scDevice.DeviceInfo.ActiveUser = info.ActiveUser
|
|
1992
|
+
scDevice.DeviceInfo.InteractionIDs = info.InteractionIDs
|
|
1993
|
+
scDevice.DeviceInfo._GroupStatus = info.GroupStatus
|
|
1994
|
+
|
|
1995
|
+
# ZeroconfResponse base class properties.
|
|
1996
|
+
scDevice.DeviceInfo._ResponseSource = info.ResponseSource
|
|
1997
|
+
scDevice.DeviceInfo.SpotifyError = info.SpotifyError
|
|
1998
|
+
scDevice.DeviceInfo.Status = info.Status
|
|
1999
|
+
scDevice.DeviceInfo.StatusString = info.StatusString
|
|
2000
|
+
|
|
2001
|
+
else:
|
|
2002
|
+
|
|
2003
|
+
# ZeroconfGetInfo class properties.
|
|
2004
|
+
# we use underscore property reference for those that don't have a property setter method.
|
|
2005
|
+
scDevice.DeviceInfo._AccountReq = info.AccountReq
|
|
2006
|
+
scDevice.DeviceInfo.ActiveUser = info.ActiveUser
|
|
2007
|
+
scDevice.DeviceInfo._Aliases = info.Aliases
|
|
2008
|
+
scDevice.DeviceInfo._Availability = info.Availability
|
|
2009
|
+
scDevice.DeviceInfo._ClientId = info.ClientId
|
|
2010
|
+
scDevice.DeviceInfo.DeviceId = info.DeviceId # same as zeroconf deviceId
|
|
2011
|
+
scDevice.DeviceInfo._GroupStatus = info.GroupStatus
|
|
2012
|
+
scDevice.DeviceInfo.InteractionIDs = info.InteractionIDs
|
|
2013
|
+
scDevice.DeviceInfo._LibraryVersion = info.LibraryVersion
|
|
2014
|
+
scDevice.DeviceInfo.ProductId = info.ProductId
|
|
2015
|
+
scDevice.DeviceInfo.PublicKey = info.PublicKey
|
|
2016
|
+
scDevice.DeviceInfo.RemoteName = info.RemoteName # same as zeroconf friendly_name
|
|
2017
|
+
scDevice.DeviceInfo._ResolverVersion = info.ResolverVersion
|
|
2018
|
+
scDevice.DeviceInfo._Scope = info.Scope
|
|
2019
|
+
scDevice.DeviceInfo._SupportedCapabilities = info.SupportedCapabilities
|
|
2020
|
+
scDevice.DeviceInfo._SupportedDrmMediaFormats = info.SupportedDrmMediaFormats
|
|
2021
|
+
scDevice.DeviceInfo._TokenType = info.TokenType
|
|
2022
|
+
scDevice.DeviceInfo._Version = info.Version
|
|
2023
|
+
scDevice.DeviceInfo._VoiceSupport = info.VoiceSupport
|
|
2024
|
+
|
|
2025
|
+
# we will leave the following properties with their current values as
|
|
2026
|
+
# set from the CastInfo instance, as they contain more consistent values
|
|
2027
|
+
# that properly describe the device.
|
|
2028
|
+
#scDevice.DeviceInfo.BrandDisplayName = info.BrandDisplayName
|
|
2029
|
+
#scDevice.DeviceInfo.DeviceType = info.DeviceType
|
|
2030
|
+
#scDevice.DeviceInfo.ModelDisplayName = info.ModelDisplayName
|
|
2031
|
+
|
|
2032
|
+
# ZeroconfResponse base class properties.
|
|
2033
|
+
scDevice.DeviceInfo._ResponseSource = info.ResponseSource
|
|
2034
|
+
scDevice.DeviceInfo.SpotifyError = info.SpotifyError
|
|
2035
|
+
scDevice.DeviceInfo.Status = info.Status
|
|
2036
|
+
scDevice.DeviceInfo.StatusString = info.StatusString
|
|
2037
|
+
|
|
2038
|
+
# trace.
|
|
2039
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: %s (DeviceInfo / getInfo, OnCastGetInfoResponseReceived)" % (scDevice.Title), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
2040
|
+
|
|
2041
|
+
|
|
2042
|
+
def OnCastZeroconfResponseReceived(
|
|
2043
|
+
self,
|
|
2044
|
+
uuid:UUID,
|
|
2045
|
+
response:ZeroconfResponse,
|
|
2046
|
+
) -> None:
|
|
2047
|
+
"""
|
|
2048
|
+
Called when the cast app task has received a Spotify Connect ZeroconfResponse
|
|
2049
|
+
data structure.
|
|
2050
|
+
|
|
2051
|
+
Args:
|
|
2052
|
+
uuid (UUID):
|
|
2053
|
+
Spotify Connect device that was added.
|
|
2054
|
+
response (ZeroconfResponse):
|
|
2055
|
+
A `ZeroconfResponse` instance that contains the zeroconf response data.
|
|
2056
|
+
|
|
2057
|
+
This method allows us to update the SpotifyConnectDevice entry with the Spotify Connect
|
|
2058
|
+
Zeroconf response information.
|
|
2059
|
+
"""
|
|
2060
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
2061
|
+
with self._SpotifyConnectDevices_RLock:
|
|
2062
|
+
|
|
2063
|
+
# get device instance.
|
|
2064
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceByDiscoveryKey(str(uuid))
|
|
2065
|
+
if (scDevice is not None):
|
|
2066
|
+
|
|
2067
|
+
# ZeroconfResponse class properties.
|
|
2068
|
+
scDevice.ZeroconfResponseInfo.ResponseSource = response.ResponseSource
|
|
2069
|
+
scDevice.ZeroconfResponseInfo.SpotifyError = response.SpotifyError
|
|
2070
|
+
scDevice.ZeroconfResponseInfo.Status = response.Status
|
|
2071
|
+
scDevice.ZeroconfResponseInfo.StatusString = response.StatusString
|
|
2072
|
+
|
|
2073
|
+
# trace.
|
|
2074
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (ZeroconfResponseInfo)" % (scDevice.Name), scDevice.ZeroconfResponseInfo, excludeNonPublic=True)
|
|
2075
|
+
|
|
2076
|
+
# indicate zeroconf response data was received.
|
|
2077
|
+
if (response.ResponseSource == TYPE_ADD_USER_RESPONSE) or (response.ResponseSource == TYPE_ADD_USER_ERROR) \
|
|
2078
|
+
or (response.ResponseSource == TYPE_GET_INFO_ERROR):
|
|
2079
|
+
self.WaitForActivationComplete.set()
|
|
2080
|
+
elif (response.ResponseSource == TYPE_TRANSFER_SUCCESS) or (response.ResponseSource == TYPE_TRANSFER_ERROR):
|
|
2081
|
+
self.WaitForTransferComplete.set()
|
|
2082
|
+
elif (response.ResponseSource == TYPE_LAUNCH_ERROR):
|
|
2083
|
+
self.WaitForActivationComplete.set()
|
|
2084
|
+
|
|
2085
|
+
|
|
2086
|
+
def OnServiceInfoAddedUpdatedSpotifyConnect(
|
|
2087
|
+
self,
|
|
2088
|
+
zeroconfDiscoveryResult:ZeroconfDiscoveryResult
|
|
2089
|
+
) -> None:
|
|
2090
|
+
"""
|
|
2091
|
+
Called when a new Spotify Connect ServiceInfo record has been discovered
|
|
2092
|
+
or updated for a Spotify Connect device.
|
|
2093
|
+
|
|
2094
|
+
Args:
|
|
2095
|
+
zeroconfDiscoveryResult (ZeroconfDiscoveryResult):
|
|
2096
|
+
A `ZeroconfDiscoveryResult` object that contains discovery details.
|
|
2097
|
+
|
|
2098
|
+
This method will be called in a thread-safe manner, as the caller is using
|
|
2099
|
+
the `_Zeroconf_RLock` object to control access.
|
|
2100
|
+
"""
|
|
2101
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
2102
|
+
with self._SpotifyConnectDevices_RLock:
|
|
2103
|
+
|
|
2104
|
+
try:
|
|
2105
|
+
|
|
2106
|
+
# trace.
|
|
2107
|
+
_logsi.EnterMethod(SILevel.Debug)
|
|
2108
|
+
|
|
2109
|
+
# add / update discovered results by serviceinfo key value.
|
|
2110
|
+
# we use the serviceinfo NAME value (not KEY) since some manufacturers use random ip port
|
|
2111
|
+
# numbers, and the port number could have been updated. in this case, the name SHOULD remain the same.
|
|
2112
|
+
# note that a different serviceinfo name SHOULD be created for speaker groups; at least they are for BOSE devices.
|
|
2113
|
+
scDevice:SpotifyConnectDevice = None
|
|
2114
|
+
idx:int = self._SpotifyConnectDevices.GetDeviceIndexByDiscoveryName(zeroconfDiscoveryResult.Name)
|
|
2115
|
+
if (idx == -1):
|
|
2116
|
+
|
|
2117
|
+
# trace.
|
|
2118
|
+
_logsi.LogVerbose("Creating new SpotifyConnectDevice instance from ServiceInfo data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
2119
|
+
|
|
2120
|
+
# By default, the ZeroConf DeviceName is used for the device name value; this
|
|
2121
|
+
# will change later when the Spotify Connect `getInfo` call is made to retrieve the
|
|
2122
|
+
# RemoteName value, which is what Spotify Connect capable players display as a device name.
|
|
2123
|
+
# For most manufacturers, the ZeroConf DeviceName is an internal-use type of value (e.g. "sonosRINCON_38420B909DC801400").
|
|
2124
|
+
# For other manufacturers, this is a user-friendly type of value (e.g. "Bose-ST10-1").
|
|
2125
|
+
# Here are some examples (by manufacturer):
|
|
2126
|
+
|
|
2127
|
+
# Manufacturer ZeroConf DeviceName getInfo Remotename getInfo Alias
|
|
2128
|
+
# ------------ ------------------------------ ----------------------------------- -----------------------
|
|
2129
|
+
# Sonos sonosRINCON_38420B909DC801400" Office n/a
|
|
2130
|
+
# Bose Bose-ST10-1 Bose-ST10-1 n/a
|
|
2131
|
+
# Bose Bose-ST10-1 + Bose-ST10-2 (L+R) Bose-ST10-1 (if stereo paired) n/a
|
|
2132
|
+
# Bose Bose-ST10-1 Group Bose-ST10-1 (if zoned) n/a
|
|
2133
|
+
# Denon 0005cdb737b2LivingRoom Living Room n/a
|
|
2134
|
+
# Wiim FF98F35902BB6CA851FE34D5 n/a (uses Alias) "Kitchen Speakers"
|
|
2135
|
+
|
|
2136
|
+
# create a new Spotify Connect Device instance.
|
|
2137
|
+
scDevice = SpotifyConnectDevice()
|
|
2138
|
+
scDevice.Name = zeroconfDiscoveryResult.DeviceName
|
|
2139
|
+
|
|
2140
|
+
# set zeroconf discovery result properties.
|
|
2141
|
+
scDevice.DiscoveryResult = zeroconfDiscoveryResult
|
|
2142
|
+
|
|
2143
|
+
# verify CPath property was present; if not, log a warning as the endpoint url will be invalid!
|
|
2144
|
+
if (zeroconfDiscoveryResult.SpotifyConnectCPath is None):
|
|
2145
|
+
_logsi.LogWarning("Spotify Connect device Zeroconf ServiceInfo data did not contain a CPath value: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
2146
|
+
|
|
2147
|
+
try:
|
|
2148
|
+
|
|
2149
|
+
# trace.
|
|
2150
|
+
_logsi.LogVerbose("Retrieving Spotify Connect device information: %s" % (zeroconfDiscoveryResult.Id))
|
|
2151
|
+
|
|
2152
|
+
# create connection object to retrieve spotify connect device information (via direct ip address).
|
|
2153
|
+
zconn:ZeroconfConnect = ZeroconfConnect(
|
|
2154
|
+
zeroconfDiscoveryResult.HostIpAddress,
|
|
2155
|
+
zeroconfDiscoveryResult.HostIpPort,
|
|
2156
|
+
zeroconfDiscoveryResult.SpotifyConnectCPath,
|
|
2157
|
+
zeroconfDiscoveryResult.SpotifyConnectVersion,
|
|
2158
|
+
useSSL=False,
|
|
2159
|
+
tokenStorageDir=self.SpotifyClientInstance.TokenStorageDir,
|
|
2160
|
+
tokenStorageFile=self.SpotifyClientInstance.TokenStorageFile
|
|
2161
|
+
)
|
|
2162
|
+
|
|
2163
|
+
# retrieve initial spotify connect device information (by ip address).
|
|
2164
|
+
scDevice.DeviceInfo = zconn.GetInformation()
|
|
2165
|
+
scDevice.Id = scDevice.DeviceInfo.DeviceId
|
|
2166
|
+
scDevice.Name = scDevice.DeviceInfo.RemoteName
|
|
2167
|
+
|
|
2168
|
+
# if remote name was not specified, then set device name to first alias name.
|
|
2169
|
+
# note that we will not reset the RemoteName, as the "" value indicates an alias is in use.
|
|
2170
|
+
if ((scDevice.DeviceInfo.RemoteName + "").strip() == ""):
|
|
2171
|
+
if (scDevice.DeviceInfo.HasAliases):
|
|
2172
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf GetInformation alias name will be utilized for Zeroconf Discovery Result: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
2173
|
+
scDevice.Name = scDevice.DeviceInfo.Aliases[0].Name
|
|
2174
|
+
|
|
2175
|
+
except Exception as ex:
|
|
2176
|
+
|
|
2177
|
+
# trace.
|
|
2178
|
+
_logsi.LogVerbose("Could not retrieve Spotify Connect device information by ip address; retrying with DNS server alias \"%s\"" % (zeroconfDiscoveryResult.Server))
|
|
2179
|
+
|
|
2180
|
+
try:
|
|
2181
|
+
|
|
2182
|
+
# *IMPORTANT* This call can take up to 20 seconds to timeout because it is using DNS
|
|
2183
|
+
# resolution to find the device. the timeout is a system-level value, so it cannot be
|
|
2184
|
+
# controlled by this api!
|
|
2185
|
+
|
|
2186
|
+
# create connection object to retrieve spotify connect device information (via dns alias).
|
|
2187
|
+
zconn:ZeroconfConnect = ZeroconfConnect(
|
|
2188
|
+
zeroconfDiscoveryResult.Server,
|
|
2189
|
+
zeroconfDiscoveryResult.HostIpPort,
|
|
2190
|
+
zeroconfDiscoveryResult.SpotifyConnectCPath,
|
|
2191
|
+
zeroconfDiscoveryResult.SpotifyConnectVersion,
|
|
2192
|
+
useSSL=False,
|
|
2193
|
+
tokenStorageDir=self.SpotifyClientInstance.TokenStorageDir,
|
|
2194
|
+
tokenStorageFile=self.SpotifyClientInstance.TokenStorageFile
|
|
2195
|
+
)
|
|
2196
|
+
|
|
2197
|
+
# retrieve initial spotify connect device information (by dns alias).
|
|
2198
|
+
scDevice.DeviceInfo = zconn.GetInformation()
|
|
2199
|
+
scDevice.Id = scDevice.DeviceInfo.DeviceId
|
|
2200
|
+
scDevice.Name = scDevice.DeviceInfo.RemoteName
|
|
2201
|
+
|
|
2202
|
+
# if remote name was not specified, then set device name to first alias name.
|
|
2203
|
+
# note that we will not reset the RemoteName, as the "" value indicates an alias is in use.
|
|
2204
|
+
if ((scDevice.DeviceInfo.RemoteName + "").strip() == ""):
|
|
2205
|
+
if (scDevice.DeviceInfo.HasAliases):
|
|
2206
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf GetInformation alias name will be utilized for Zeroconf Discovery Result: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
2207
|
+
scDevice.Name = scDevice.DeviceInfo.Aliases[0].Name
|
|
2208
|
+
|
|
2209
|
+
# update HostIpAddress in discovery result so it knows to use the dns alias
|
|
2210
|
+
# instead of the ip address.
|
|
2211
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf GetInformation call for Instance Name \"%s\" (%s) was resolved using DNS Server alias; HostIpAddress will be updated with the resolved ip address" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Server))
|
|
2212
|
+
resolvedIpAddress = socket.gethostbyname(zeroconfDiscoveryResult.Server)
|
|
2213
|
+
zeroconfDiscoveryResult.HostIpAddress = resolvedIpAddress
|
|
2214
|
+
|
|
2215
|
+
except Exception as ex:
|
|
2216
|
+
|
|
2217
|
+
# trace.
|
|
2218
|
+
_logsi.LogException("Spotify Connect Zeroconf GetInformation call failed for device instance \"%s\" (ip=%s:%s): %s" % (scDevice.Name, zeroconfDiscoveryResult.HostIpAddress, zeroconfDiscoveryResult.HostIpPort, str(ex)), ex, logToSystemLogger=False)
|
|
2219
|
+
|
|
2220
|
+
# if Getinformation call failed, then default device information.
|
|
2221
|
+
scDevice.DeviceInfo = ZeroconfGetInfo()
|
|
2222
|
+
scDevice.DeviceInfo.DeviceId = "getInfoError"
|
|
2223
|
+
scDevice.DeviceInfo.RemoteName = zeroconfDiscoveryResult.DeviceName
|
|
2224
|
+
scDevice.DeviceInfo.ResponseSource = "spotifywebapiPython"
|
|
2225
|
+
scDevice.DeviceInfo.Status = 9999
|
|
2226
|
+
scDevice.DeviceInfo.StatusString = str(ex)
|
|
2227
|
+
scDevice.Id = scDevice.DeviceInfo.DeviceId
|
|
2228
|
+
|
|
2229
|
+
# did this device change from a dynamic device (e.g. non-zeroconf) to a zeroconf device?
|
|
2230
|
+
# this can happen if the device is initially added as a dynamic device, then the zeroconf
|
|
2231
|
+
# device is discovered. if so, we will remove the dynamic device entry.
|
|
2232
|
+
spDynamicDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.GetDeviceById(scDevice.DeviceInfo.DeviceId)
|
|
2233
|
+
if (spDynamicDevice is not None):
|
|
2234
|
+
|
|
2235
|
+
# trace.
|
|
2236
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf is converting SpotifyConnectDevice entry %s from dynamic to zeroconf" % (spDynamicDevice.Title))
|
|
2237
|
+
|
|
2238
|
+
# copy real-time status dynamic properties to zerconf object.
|
|
2239
|
+
scDevice.IsActiveDevice = spDynamicDevice.IsActiveDevice
|
|
2240
|
+
scDevice.IsInDeviceList = spDynamicDevice.IsInDeviceList
|
|
2241
|
+
|
|
2242
|
+
# at this point we will remove the device from the collection so that it can be
|
|
2243
|
+
# replaced with the static device entry; this will also remove any duplicate
|
|
2244
|
+
# device id entries, which can appear for some manufacturers when devices are
|
|
2245
|
+
# grouped, zoned, or stereo paired (e.g. Bose).
|
|
2246
|
+
dynamicOnly:bool = True
|
|
2247
|
+
if (scDevice.DiscoveryResult.IsDynamicDevice == False) and (spDynamicDevice.DiscoveryResult.IsDynamicDevice == False):
|
|
2248
|
+
dynamicOnly = False
|
|
2249
|
+
self.RemoveDevice(spDynamicDevice.Id, dynamicDeviceOnly=dynamicOnly)
|
|
2250
|
+
|
|
2251
|
+
# add new Spotify Connect Device to devices collection.
|
|
2252
|
+
self._SpotifyConnectDevices.Items.append(scDevice)
|
|
2253
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
2254
|
+
|
|
2255
|
+
# trace.
|
|
2256
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf added SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.ForestGreen)
|
|
2257
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: %s (DeviceInfo / getInfo)" % (scDevice.Title), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
2258
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: %s (DiscoveryResult) [%s]" % (scDevice.Title, scDevice.DiscoveryResult.HostIpTitle), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
2259
|
+
|
|
2260
|
+
# sort devices collection by device name.
|
|
2261
|
+
if (len(self._SpotifyConnectDevices.Items) > 0):
|
|
2262
|
+
self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
|
|
2263
|
+
|
|
2264
|
+
try:
|
|
2265
|
+
|
|
2266
|
+
# is this a Sonos device?
|
|
2267
|
+
if (scDevice.IsSonos):
|
|
2268
|
+
|
|
2269
|
+
# create a Sonos Controller instance for the device, retrieve the Sonos speaker information,
|
|
2270
|
+
# and add it to the Sonos players collection. use device ip address as the key, as the
|
|
2271
|
+
# Spotify Web API reports "id=null" for restricted devices in playerstate!
|
|
2272
|
+
_logsi.LogVerbose("Sonos device detected; creating Sonos Controller instance for device: %s (ip=%s)" % (scDevice.Title, zeroconfDiscoveryResult.HostIpAddress))
|
|
2273
|
+
sonosPlayer:SoCo = SoCo(zeroconfDiscoveryResult.HostIpAddress)
|
|
2274
|
+
self._SonosPlayers[zeroconfDiscoveryResult.HostIpAddress] = sonosPlayer
|
|
2275
|
+
|
|
2276
|
+
try:
|
|
2277
|
+
sonosPlayer.get_speaker_info()
|
|
2278
|
+
except Exception as ex:
|
|
2279
|
+
_logsi.LogException("Could not get speaker info for Sonos Controller instance: %s" % (str(ex)), ex, logToSystemLogger=False)
|
|
2280
|
+
|
|
2281
|
+
# trace.
|
|
2282
|
+
if (_logsi.IsOn(SILevel.Verbose)):
|
|
2283
|
+
_logsi.LogObject(SILevel.Verbose, "Sonos Controller instance for device: %s" % (scDevice.Title), sonosPlayer)
|
|
2284
|
+
_logsi.LogDictionary(SILevel.Verbose, "Sonos Controller instance for device: %s (speaker_info)" % (scDevice.Title), sonosPlayer.speaker_info)
|
|
2285
|
+
_logsi.LogObject(SILevel.Verbose, "Sonos Controller instance for device: %s (group)" % (scDevice.Title), sonosPlayer.group)
|
|
2286
|
+
_logsi.LogObject(SILevel.Verbose, "Sonos Controller instance for device: %s (zone_group_state)" % (scDevice.Title), sonosPlayer.zone_group_state)
|
|
2287
|
+
_logsi.LogEnumerable(SILevel.Verbose, "Sonos Controller instance for device: %s (all_zones)" % (scDevice.Title), sonosPlayer.all_zones)
|
|
2288
|
+
|
|
2289
|
+
except:
|
|
2290
|
+
|
|
2291
|
+
# trace.
|
|
2292
|
+
_logsi.LogException("%s - Could not create Sonos Control API reference: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
|
|
2293
|
+
|
|
2294
|
+
# ignore exceptions, as there is nothing we can do at this point.
|
|
2295
|
+
|
|
2296
|
+
# raise event.
|
|
2297
|
+
self._RaiseDeviceAdded(scDevice)
|
|
2298
|
+
|
|
2299
|
+
else:
|
|
2300
|
+
|
|
2301
|
+
# trace.
|
|
2302
|
+
_logsi.LogDebug("Updating existing SpotifyConnectDevice instance from ServiceInfo data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
2303
|
+
|
|
2304
|
+
# get existing Spotify Connect Device instance.
|
|
2305
|
+
scDevice = self._SpotifyConnectDevices.Items[idx]
|
|
2306
|
+
|
|
2307
|
+
# were any changes made to the zeroconf discovery results?
|
|
2308
|
+
if (not scDevice.DiscoveryResult.Equals(zeroconfDiscoveryResult)):
|
|
2309
|
+
|
|
2310
|
+
# trace.
|
|
2311
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (OLD DeviceInfo / getInfo)" % (scDevice.Title), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
2312
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: \"%s\" (OLD DiscoveryResult) [%s]" % (scDevice.Title, scDevice.DiscoveryResult.HostIpTitle), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
2313
|
+
|
|
2314
|
+
# set zeroconf discovery result properties.
|
|
2315
|
+
scDevice.DiscoveryResult = zeroconfDiscoveryResult
|
|
2316
|
+
|
|
2317
|
+
# did the device name change? if so, then update name and id properties,
|
|
2318
|
+
# as well as the corresponding getInfo properties.
|
|
2319
|
+
if (scDevice.Name != zeroconfDiscoveryResult.DeviceName):
|
|
2320
|
+
newDeviceName:str = zeroconfDiscoveryResult.DeviceName
|
|
2321
|
+
newDeviceId:str = self.GetSpotifyDeviceIDFromName(zeroconfDiscoveryResult.DeviceName)
|
|
2322
|
+
_logsi.LogVerbose("Spotify Connect Zeroconf SpotifyConnectDevice entry name and id changed from %s to \"%s\" (%s)" % (scDevice.Title, newDeviceName, newDeviceId))
|
|
2323
|
+
scDevice.Name = newDeviceName
|
|
2324
|
+
scDevice.Id = newDeviceId
|
|
2325
|
+
scDevice.DeviceInfo.DeviceId = newDeviceId
|
|
2326
|
+
scDevice.DeviceInfo.RemoteName = newDeviceName
|
|
2327
|
+
|
|
2328
|
+
# update existing Spotify Connect Device in devices collection.
|
|
2329
|
+
self._SpotifyConnectDevices.Items[idx] = scDevice
|
|
2330
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
2331
|
+
|
|
2332
|
+
# trace.
|
|
2333
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf updated SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.ForestGreen)
|
|
2334
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: %s (DeviceInfo / getInfo)" % (scDevice.Title), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
2335
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: %s (DiscoveryResult) [%s]" % (scDevice.Title, scDevice.DiscoveryResult.HostIpTitle), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
2336
|
+
|
|
2337
|
+
# sort devices collection by device name.
|
|
2338
|
+
if (len(self._SpotifyConnectDevices.Items) > 0):
|
|
2339
|
+
self._SpotifyConnectDevices.Items.sort(key=lambda x: (x.Name or "").lower(), reverse=False)
|
|
2340
|
+
|
|
2341
|
+
# raise event.
|
|
2342
|
+
self._RaiseDeviceUpdated(scDevice)
|
|
2343
|
+
|
|
2344
|
+
else:
|
|
2345
|
+
|
|
2346
|
+
# trace.
|
|
2347
|
+
_logsi.LogObject(SILevel.Verbose, "SpotifyConnectDevice info: \"%s\" (ignored; DiscoveryResult did not change)" % (scDevice.Name), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
2348
|
+
|
|
2349
|
+
except Exception as ex:
|
|
2350
|
+
|
|
2351
|
+
# trace.
|
|
2352
|
+
_logsi.LogException("%s - Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
|
|
2353
|
+
|
|
2354
|
+
# ignore exceptions, as there is nothing we can do at this point.
|
|
2355
|
+
|
|
2356
|
+
finally:
|
|
2357
|
+
|
|
2358
|
+
# trace.
|
|
2359
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
2360
|
+
|
|
2361
|
+
|
|
2362
|
+
def OnServiceInfoRemovedSpotifyConnect(
|
|
2363
|
+
self,
|
|
2364
|
+
zeroconfDiscoveryResult:ZeroconfDiscoveryResult
|
|
2365
|
+
) -> None:
|
|
2366
|
+
"""
|
|
2367
|
+
Called when an mDNS service is lost.
|
|
2368
|
+
|
|
2369
|
+
Args:
|
|
2370
|
+
zeroconfDiscoveryResult (ZeroconfDiscoveryResult):
|
|
2371
|
+
A `ZeroconfDiscoveryResult` object that contains discovery details.
|
|
2372
|
+
|
|
2373
|
+
This method will be called in a thread-safe manner, as the caller is using
|
|
2374
|
+
the `_Zeroconf_RLock` object to control access.
|
|
2375
|
+
"""
|
|
2376
|
+
# syncronize access via lock, as we are accessing the collection.
|
|
2377
|
+
with self._SpotifyConnectDevices_RLock:
|
|
2378
|
+
|
|
2379
|
+
try:
|
|
2380
|
+
|
|
2381
|
+
# trace.
|
|
2382
|
+
_logsi.EnterMethod(SILevel.Debug)
|
|
2383
|
+
|
|
2384
|
+
# remove discovered results by serviceinfo key value.
|
|
2385
|
+
# we use the serviceinfo NAME value (not KEY) since that is the only information available on
|
|
2386
|
+
# a remove serviceinfo request.
|
|
2387
|
+
idx:int = self._SpotifyConnectDevices.GetDeviceIndexByDiscoveryName(zeroconfDiscoveryResult.Name)
|
|
2388
|
+
if (idx == -1):
|
|
2389
|
+
|
|
2390
|
+
# trace.
|
|
2391
|
+
_logsi.LogVerbose("SpotifyConnectDevice instance could not be found to remove: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
2392
|
+
|
|
2393
|
+
else:
|
|
2394
|
+
|
|
2395
|
+
# trace.
|
|
2396
|
+
_logsi.LogVerbose("Removing existing SpotifyConnectDevice instance from ServiceInfo data: \"%s\" (%s)" % (zeroconfDiscoveryResult.DeviceName, zeroconfDiscoveryResult.Name))
|
|
2397
|
+
|
|
2398
|
+
# remove existing Spotify Connect Device instance from devices collection.
|
|
2399
|
+
scDevice:SpotifyConnectDevice = self._SpotifyConnectDevices.Items.pop(idx)
|
|
2400
|
+
self._SpotifyConnectDevices.DateLastRefreshed = datetime.utcnow().timestamp()
|
|
2401
|
+
|
|
2402
|
+
# trace.
|
|
2403
|
+
_logsi.LogObject(SILevel.Verbose, "Spotify Connect Zeroconf removed SpotifyConnectDevices collection entry: \"%s\" (%s)" % (scDevice.Name, scDevice.DiscoveryResult.Name), scDevice, excludeNonPublic=True, colorValue=SIColors.DarkOrange)
|
|
2404
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: %s (DeviceInfo / getInfo)" % (scDevice.Title), scDevice.DeviceInfo, excludeNonPublic=True)
|
|
2405
|
+
_logsi.LogObject(SILevel.Debug, "SpotifyConnectDevice info: %s (DiscoveryResult) [%s]" % (scDevice.Title, scDevice.DiscoveryResult.HostIpTitle), scDevice.DiscoveryResult, excludeNonPublic=True)
|
|
2406
|
+
|
|
2407
|
+
# raise event.
|
|
2408
|
+
self._RaiseDeviceRemoved(scDevice)
|
|
2409
|
+
|
|
2410
|
+
except Exception as ex:
|
|
2411
|
+
|
|
2412
|
+
# trace.
|
|
2413
|
+
_logsi.LogException("%s - Exception: %s" % (self.name, str(ex)), ex, logToSystemLogger=False)
|
|
2414
|
+
|
|
2415
|
+
# ignore exceptions, as there is nothing we can do at this point.
|
|
2416
|
+
|
|
2417
|
+
finally:
|
|
2418
|
+
|
|
2419
|
+
# trace.
|
|
2420
|
+
_logsi.LeaveMethod(SILevel.Debug)
|
|
2421
|
+
|
|
2422
|
+
|
|
2423
|
+
def _RaiseDeviceAdded(
|
|
2424
|
+
self,
|
|
2425
|
+
device:SpotifyConnectDevice
|
|
2426
|
+
) -> None:
|
|
2427
|
+
"""
|
|
2428
|
+
Raises the `DeviceAdded` event.
|
|
2429
|
+
|
|
2430
|
+
Args:
|
|
2431
|
+
device (SpotifyConnectDevice):
|
|
2432
|
+
Spotify Connect device that was added.
|
|
2433
|
+
"""
|
|
2434
|
+
try:
|
|
2435
|
+
|
|
2436
|
+
# raise event.
|
|
2437
|
+
args:SpotifyConnectDeviceEventArgs = SpotifyConnectDeviceEventArgs(device)
|
|
2438
|
+
self.DeviceAdded(self, args)
|
|
2439
|
+
|
|
2440
|
+
except Exception as ex:
|
|
2441
|
+
|
|
2442
|
+
# ignore exceptions.
|
|
2443
|
+
pass
|
|
2444
|
+
|
|
2445
|
+
|
|
2446
|
+
@abstractmethod
|
|
2447
|
+
def OnDeviceAdded(
|
|
2448
|
+
self,
|
|
2449
|
+
sender:object,
|
|
2450
|
+
e:SpotifyConnectDeviceEventArgs
|
|
2451
|
+
) -> None:
|
|
2452
|
+
"""
|
|
2453
|
+
Method that will handle the `DeviceAdded` event.
|
|
2454
|
+
Inheriting classes can override this method to handle the event.
|
|
2455
|
+
|
|
2456
|
+
Args:
|
|
2457
|
+
sender (object):
|
|
2458
|
+
The object which fired the event.
|
|
2459
|
+
e (SpotifyConnectDeviceEventArgs):
|
|
2460
|
+
Arguments that contain detailed information related to the event.
|
|
2461
|
+
|
|
2462
|
+
This event is fired when a Spotify Connect device has been added to the
|
|
2463
|
+
SpotifyConnectDevices collection.
|
|
2464
|
+
|
|
2465
|
+
This method will be called in a thread-safe manner, as the caller is using
|
|
2466
|
+
the `SpotifyConnectDevices_RLock` object to control access.
|
|
2467
|
+
"""
|
|
2468
|
+
pass
|
|
2469
|
+
|
|
2470
|
+
|
|
2471
|
+
def _RaiseDeviceRemoved(
|
|
2472
|
+
self,
|
|
2473
|
+
device:SpotifyConnectDevice
|
|
2474
|
+
) -> None:
|
|
2475
|
+
"""
|
|
2476
|
+
Raises the `DeviceRemoved` event.
|
|
2477
|
+
|
|
2478
|
+
Args:
|
|
2479
|
+
device (SpotifyConnectDevice):
|
|
2480
|
+
Spotify Connect device that was removed.
|
|
2481
|
+
"""
|
|
2482
|
+
try:
|
|
2483
|
+
|
|
2484
|
+
# raise event.
|
|
2485
|
+
args:SpotifyConnectDeviceEventArgs = SpotifyConnectDeviceEventArgs(device)
|
|
2486
|
+
self.DeviceRemoved(self, args)
|
|
2487
|
+
|
|
2488
|
+
except Exception as ex:
|
|
2489
|
+
|
|
2490
|
+
# ignore exceptions.
|
|
2491
|
+
pass
|
|
2492
|
+
|
|
2493
|
+
|
|
2494
|
+
@abstractmethod
|
|
2495
|
+
def OnDeviceRemoved(
|
|
2496
|
+
self,
|
|
2497
|
+
sender:object,
|
|
2498
|
+
e:SpotifyConnectDeviceEventArgs
|
|
2499
|
+
) -> None:
|
|
2500
|
+
"""
|
|
2501
|
+
Method that will handle the `DeviceRemoved` event.
|
|
2502
|
+
Inheriting classes can override this method to handle the event.
|
|
2503
|
+
|
|
2504
|
+
Args:
|
|
2505
|
+
sender (object):
|
|
2506
|
+
The object which fired the event.
|
|
2507
|
+
e (SpotifyConnectDeviceEventArgs):
|
|
2508
|
+
Arguments that contain detailed information related to the event.
|
|
2509
|
+
|
|
2510
|
+
This event is fired when a Spotify Connect device has been removed from the
|
|
2511
|
+
SpotifyConnectDevices collection.
|
|
2512
|
+
|
|
2513
|
+
This method will be called in a thread-safe manner, as the caller is using
|
|
2514
|
+
the `SpotifyConnectDevices_RLock` object to control access.
|
|
2515
|
+
"""
|
|
2516
|
+
pass
|
|
2517
|
+
|
|
2518
|
+
|
|
2519
|
+
def _RaiseDeviceUpdated(
|
|
2520
|
+
self,
|
|
2521
|
+
device:SpotifyConnectDevice
|
|
2522
|
+
) -> None:
|
|
2523
|
+
"""
|
|
2524
|
+
Raises the `DeviceUpdated` event.
|
|
2525
|
+
|
|
2526
|
+
Args:
|
|
2527
|
+
device (SpotifyConnectDevice):
|
|
2528
|
+
Spotify Connect device that was updated.
|
|
2529
|
+
"""
|
|
2530
|
+
try:
|
|
2531
|
+
|
|
2532
|
+
# raise event.
|
|
2533
|
+
args:SpotifyConnectDeviceEventArgs = SpotifyConnectDeviceEventArgs(device)
|
|
2534
|
+
self.DeviceUpdated(self, args)
|
|
2535
|
+
|
|
2536
|
+
except Exception as ex:
|
|
2537
|
+
|
|
2538
|
+
# ignore exceptions.
|
|
2539
|
+
pass
|
|
2540
|
+
|
|
2541
|
+
|
|
2542
|
+
@abstractmethod
|
|
2543
|
+
def OnDeviceUpdated(
|
|
2544
|
+
self,
|
|
2545
|
+
sender:object,
|
|
2546
|
+
e:SpotifyConnectDeviceEventArgs
|
|
2547
|
+
) -> None:
|
|
2548
|
+
"""
|
|
2549
|
+
Method that will handle the `DeviceUpdated` event.
|
|
2550
|
+
Inheriting classes can override this method to handle the event.
|
|
2551
|
+
|
|
2552
|
+
Args:
|
|
2553
|
+
sender (object):
|
|
2554
|
+
The object which fired the event.
|
|
2555
|
+
e (SpotifyConnectDeviceEventArgs):
|
|
2556
|
+
Arguments that contain detailed information related to the event.
|
|
2557
|
+
|
|
2558
|
+
This event is fired when a Spotify Connect device has been updated in the
|
|
2559
|
+
SpotifyConnectDevices collection.
|
|
2560
|
+
|
|
2561
|
+
This method will be called in a thread-safe manner, as the caller is using
|
|
2562
|
+
the `SpotifyConnectDevices_RLock` object to control access.
|
|
2563
|
+
"""
|
|
2564
|
+
pass
|