spotifywebapiPython 1.0.150__py3-none-any.whl → 1.0.151__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of spotifywebapiPython might be problematic. Click here for more details.

Files changed (24) hide show
  1. {spotifywebapiPython-1.0.150.dist-info → spotifywebapiPython-1.0.151.dist-info}/METADATA +2 -1
  2. {spotifywebapiPython-1.0.150.dist-info → spotifywebapiPython-1.0.151.dist-info}/RECORD +24 -24
  3. spotifywebapipython/const.py +6 -1
  4. spotifywebapipython/docs/__init__.html +9 -2
  5. spotifywebapipython/docs/index.html +2 -2
  6. spotifywebapipython/docs/search.js +1 -1
  7. spotifywebapipython/docs/spotifywebapipython/const.html +20 -3
  8. spotifywebapipython/docs/spotifywebapipython/saappmessages.html +2 -2
  9. spotifywebapipython/docs/spotifywebapipython/sautils.html +65 -2
  10. spotifywebapipython/docs/spotifywebapipython/spotifyapierror.html +2 -2
  11. spotifywebapipython/docs/spotifywebapipython/spotifyapimessage.html +2 -2
  12. spotifywebapipython/docs/spotifywebapipython/spotifyauthtoken.html +2 -2
  13. spotifywebapipython/docs/spotifywebapipython/spotifyclient.html +42 -2
  14. spotifywebapipython/docs/spotifywebapipython/spotifydiscovery.html +2 -2
  15. spotifywebapipython/docs/spotifywebapipython/spotifywebapiauthenticationerror.html +2 -2
  16. spotifywebapipython/docs/spotifywebapipython/spotifywebapierror.html +2 -2
  17. spotifywebapipython/docs/spotifywebapipython/spotifywebplayertoken.html +2 -2
  18. spotifywebapipython/models/spotifyconnectdevice.py +12 -0
  19. spotifywebapipython/sautils.py +50 -0
  20. spotifywebapipython/spotifyclient.py +491 -163
  21. spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +61 -0
  22. {spotifywebapiPython-1.0.150.dist-info → spotifywebapiPython-1.0.151.dist-info}/LICENSE +0 -0
  23. {spotifywebapiPython-1.0.150.dist-info → spotifywebapiPython-1.0.151.dist-info}/WHEEL +0 -0
  24. {spotifywebapiPython-1.0.150.dist-info → spotifywebapiPython-1.0.151.dist-info}/top_level.txt +0 -0
@@ -5,10 +5,16 @@ import json
5
5
  from io import BytesIO
6
6
  from oauthlib.oauth2 import BackendApplicationClient, WebApplicationClient
7
7
  import socket
8
+ from soco import SoCo
9
+ from soco.core import (
10
+ PLAY_MODE_BY_MEANING as SONOS_PLAY_MODE_BY_MEANING,
11
+ PLAY_MODES as SONOS_PLAY_MODES,
12
+ )
8
13
  import time
9
14
  from typing import Tuple, Callable, Union
10
15
  from urllib3 import PoolManager, Timeout, HTTPResponse
11
16
  from urllib.parse import urlencode
17
+ import urllib.parse
12
18
  from lxml.etree import fromstring, Element, tostring
13
19
  from lxml import etree
14
20
  from zeroconf import Zeroconf
@@ -18,7 +24,6 @@ from .oauthcli import AuthClient
18
24
  from .models import *
19
25
  from .models import UserProfile as UserProfileCurrentUser
20
26
  from .spotifyconnect import SpotifyConnectDirectoryTask
21
- from .sautils import export, GetUnixTimestampMSFromUtcNow, _xmlGetInnerText, validateDelay
22
27
  from .saappmessages import SAAppMessages
23
28
  from .spotifyapierror import SpotifyApiError
24
29
  from .spotifyapimessage import SpotifyApiMessage
@@ -28,6 +33,14 @@ from .spotifywebapiauthenticationerror import SpotifyWebApiAuthenticationError
28
33
  from .spotifywebapierror import SpotifyWebApiError
29
34
  from .zeroconfapi.zeroconfconnect import ZeroconfConnect, ZeroconfGetInfo, ZeroconfResponse, SpotifyZeroconfApiError
30
35
  from .zeroconfapi.zeroconfgetinfoalias import ZeroconfGetInfoAlias
36
+ from .sautils import (
37
+ _xmlGetInnerText,
38
+ export,
39
+ GetUnixTimestampMSFromUtcNow,
40
+ mediaPositionHMS_fromSeconds,
41
+ mediaPositionHMS_toSeconds,
42
+ validateDelay
43
+ )
31
44
  from .const import (
32
45
  SPOTIFY_API_AUTHORIZE_URL,
33
46
  SPOTIFY_API_TOKEN_URL,
@@ -42,6 +55,7 @@ from .const import (
42
55
  TRACE_MSG_AUTHTOKEN_CREATE,
43
56
  TRACE_MSG_AUTOPAGING_NEXT,
44
57
  TRACE_MSG_DELAY_DEVICE,
58
+ TRACE_MSG_DELAY_DEVICE_SONOS,
45
59
  TRACE_MSG_USERPROFILE,
46
60
  TRACE_WARN_SPOTIFY_SEARCH_NO_MARKET,
47
61
  VERSION
@@ -7573,6 +7587,11 @@ class SpotifyClient:
7573
7587
  if scDevice is not None:
7574
7588
  device.Id = scDevice.DeviceInfo.DeviceId
7575
7589
 
7590
+ # update Spotify Connect Directory with active device details.
7591
+ # IMPORTANT - make sure result contains an object, otherwise it's an endless loop!
7592
+ if result is not None:
7593
+ self.SpotifyConnectDirectory.UpdateActiveDevice(result)
7594
+
7576
7595
  # trace.
7577
7596
  _logsi.LogObject(SILevel.Verbose, TRACE_METHOD_RESULT_TYPE % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
7578
7597
  return result
@@ -9158,6 +9177,219 @@ class SpotifyClient:
9158
9177
  _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
9159
9178
 
9160
9179
 
9180
+ def GetSonosPlayerPlaybackState(
9181
+ self,
9182
+ scDevice:SpotifyConnectDevice,
9183
+ ) -> PlayerPlayState:
9184
+ """
9185
+ Get information about the Sonos user's current playback state, including track or episode, progress,
9186
+ and active device.
9187
+
9188
+ This is not part of the Spotify Web API specification; it is for Sonos devices only!
9189
+
9190
+ Args:
9191
+ scDevice (SpotifyConnectDevice):
9192
+ Spotify Connect device instance used to identify the Sonos player.
9193
+
9194
+ Returns:
9195
+ A `PlayerPlayState` object that contains Sonos player state details as well as
9196
+ currently playing content.
9197
+
9198
+ Raises:
9199
+ SpotifyApiError:
9200
+ If the method fails for any other reason.
9201
+ """
9202
+ apiMethodName:str = 'GetSonosPlayerPlaybackState'
9203
+ apiMethodParms:SIMethodParmListContext = None
9204
+
9205
+ try:
9206
+
9207
+ # trace.
9208
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
9209
+ apiMethodParms.AppendKeyValue("scDevice", scDevice)
9210
+ _logsi.LogMethodParmList(SILevel.Verbose, "Get Sonos user's current playback state", apiMethodParms)
9211
+
9212
+ # create playstate object.
9213
+ playerState = PlayerPlayState()
9214
+
9215
+ # get Sonos Controller instance for the device.
9216
+ sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
9217
+
9218
+ # use the SoCo API to get Sonos playback status.
9219
+ # build a spotify playback status instance with equivalent Sonos state values.
9220
+ _logsi.LogVerbose("Getting Sonos device status via Sonos Control API for device: %s" % (scDevice.Title))
9221
+ sonosTrackInfo:dict = sonosPlayer.get_current_track_info()
9222
+ _logsi.LogDictionary(SILevel.Verbose, "Sonos device current_track_info results for device: %s" % (scDevice.Title), sonosTrackInfo, prettyPrint=True)
9223
+ sonosTransportInfo:dict = sonosPlayer.get_current_transport_info()
9224
+ _logsi.LogDictionary(SILevel.Verbose, "Sonos device get_current_transport_info results for device: %s" % (scDevice.Title), sonosTransportInfo, prettyPrint=True)
9225
+
9226
+ # update Spotify Web API player state with Sonos player state.
9227
+ playerState._Device._Id = scDevice.Id
9228
+ playerState._Device._Name = sonosPlayer.player_name
9229
+ playerState._Device._VolumePercent = sonosPlayer.volume
9230
+ playerState._Device._IsActive = True
9231
+ playerState._Device._IsRestricted = True
9232
+ playerState._Device._SupportsVolume = True
9233
+ playerState._Device._Type = 'SPEAKER'
9234
+
9235
+ # try to get the spotify uri from the "metadata" music library DIDL value, as
9236
+ # the "uri" value (e.g. "x-sonos-vli:RINCON_38420B909DC801400:2,spotify:e934941535d7b182815bf688490ce8a8")
9237
+ # is not a true spotify uri value (e.g. "spotify:track:6kYyS9g4WJeRzTYqsmcMmM")
9238
+ spotifyUri:str = sonosTrackInfo.get('uri','')
9239
+ didl:str = sonosTrackInfo.get('metadata',None)
9240
+ if (didl):
9241
+ METADATA_ID:str = 'x-sonos-spotify:'
9242
+ idx:int = didl.find(METADATA_ID)
9243
+ if (idx > -1):
9244
+ spotifyUri:str = didl[idx + len(METADATA_ID):]
9245
+ spotifyUri = urllib.parse.unquote(spotifyUri)
9246
+ idx = spotifyUri.find('?')
9247
+ if (idx > -1):
9248
+ spotifyUri = spotifyUri[:idx]
9249
+ spotifyType = self.GetTypeFromUri(spotifyUri) or ''
9250
+ spotifyId = self.GetIdFromUri(spotifyUri) or ''
9251
+
9252
+ # set base item properties.
9253
+ playerState.IsEmpty = False
9254
+ playerState.ItemType = spotifyType
9255
+ sTimeValue:str = sonosTrackInfo.get('position',None)
9256
+ playerState._ProgressMS = mediaPositionHMS_toSeconds(sTimeValue) * 1000 # convert h:mm:ss to milliseconds
9257
+
9258
+ # set some item properties based on the playing type (episode or non-episode).
9259
+ if (spotifyType == 'episode'):
9260
+
9261
+ # get the episode data from spotify, as Sonos Soco data is incomplete.
9262
+ playerState._CurrentlyPlayingType = 'episode'
9263
+ episode:Episode = self.GetEpisode(spotifyId)
9264
+ if (episode.Id is not None):
9265
+ playerState._Item = episode
9266
+ if (playerState._Context is None):
9267
+ playerState._Context = Context()
9268
+ playerState._Context._Type = episode.Show.Type
9269
+ playerState._Context._Uri = episode.Show.Uri
9270
+ playerState._Context._Href = episode.Show.Href
9271
+ playerState._Context._ExternalUrls = episode.Show.ExternalUrls
9272
+ else:
9273
+ # if data could not be obtained from spotify, then use what's
9274
+ # available from Sonos Soco api metadata.
9275
+ playerState._Item = Episode()
9276
+ playerState._Item._Name = sonosTrackInfo.get('title','')
9277
+ playerState._Item._Uri = spotifyUri
9278
+ playerState._Item._Description = 'Sonos device does not provide a description'
9279
+ sTimeValue:str = sonosTrackInfo.get('duration',None)
9280
+ playerState._Item._DurationMS = mediaPositionHMS_toSeconds(sTimeValue) * 1000 # convert h:mm:ss to milliseconds
9281
+ playerState._Item._Explicit = False
9282
+ playerState._Item._Href = 'https://api.spotify.com/v1/episodes/' + spotifyId
9283
+ playerState._Item._HtmlDescription = playerState._Item._Description
9284
+ playerState._Item._Id = spotifyId
9285
+ playerState._Item.Images.append(ImageObject())
9286
+ playerState._Item.Images[0]._Url = sonosTrackInfo.get('album_art','')
9287
+ playerState._Item._TrackNumber = sonosTrackInfo.get('playlist_position','')
9288
+ playerState._Item._ReleaseDate = '0000'
9289
+ playerState._Item._ReleaseDatePrecision = 'year'
9290
+ playerState._Item._Type = 'episode'
9291
+ playerState._Item._Show = Show()
9292
+ playerState._Item._Show._Name = sonosTrackInfo.get('album','') # TODO get this from metadata (<r:podcast>The Elfstones of Shannara</r:podcast>)
9293
+ playerState._Item._Show.Images.append(ImageObject())
9294
+ playerState._Item._Show.Images[0]._Url = sonosTrackInfo.get('album_art','')
9295
+ playerState._Item._Show._Publisher = sonosTrackInfo.get('artist','')
9296
+
9297
+ # if show name not set then get it from metadata.
9298
+ if (playerState._Item._Show._Name == ''):
9299
+ if (didl):
9300
+ METADATA_ID:str = '<r:podcast>'
9301
+ METADATA_END:str = '</r:podcast>'
9302
+ idx:int = didl.find(METADATA_ID)
9303
+ if (idx > -1):
9304
+ metaValue:str = didl[idx + len(METADATA_ID):]
9305
+ idx = metaValue.find(METADATA_END)
9306
+ if (idx > -1):
9307
+ metaValue = metaValue[:idx]
9308
+ playerState._Item._Show._Name = metaValue
9309
+
9310
+ # if episode is playing then resolve the underlying type (audiobook / podcast show).
9311
+ if (self.IsChapterEpisode(spotifyId)):
9312
+ playerState.ItemType = 'audiobook'
9313
+ else:
9314
+ playerState.ItemType = 'podcast'
9315
+
9316
+ elif (spotifyType == 'track'):
9317
+
9318
+ # get the track data from spotify, as Sonos Soco data is incomplete.
9319
+ track:Track = self.GetTrack(spotifyId)
9320
+ if (track.Id is not None):
9321
+ playerState._Item = track
9322
+ if (playerState._Context is None):
9323
+ playerState._Context = Context()
9324
+ playerState._Context._Type = track.Album.Type
9325
+ playerState._Context._Uri = track.Album.Uri
9326
+ playerState._Context._Href = track.Album.Href
9327
+ playerState._Context._ExternalUrls = track.Album.ExternalUrls
9328
+ else:
9329
+ # if data could not be obtained from spotify, then use what's
9330
+ # available from Sonos Soco api metadata.
9331
+ playerState._CurrentlyPlayingType = 'track'
9332
+ playerState.ItemType = playerState._CurrentlyPlayingType
9333
+ playerState._Item = Track()
9334
+ playerState._Item._Name = sonosTrackInfo.get('title','')
9335
+ playerState._Item._Uri = spotifyUri
9336
+ playerState._Item._Description = 'Sonos device does not provide a description'
9337
+ sTimeValue:str = sonosTrackInfo.get('duration',None)
9338
+ playerState._Item._DurationMS = mediaPositionHMS_toSeconds(sTimeValue) * 1000 # convert h:mm:ss to milliseconds
9339
+ playerState._Item._Explicit = False
9340
+ playerState._Item._Href = 'https://api.spotify.com/v1/episodes/' + spotifyId
9341
+ playerState._Item._HtmlDescription = playerState._Item._Description
9342
+ playerState._Item._Id = spotifyId
9343
+ playerState._Item._TrackNumber = sonosTrackInfo.get('playlist_position','')
9344
+ playerState._Item._ReleaseDate = '0000'
9345
+ playerState._Item._ReleaseDatePrecision = 'year'
9346
+ playerState._Item._Type = 'track'
9347
+ playerState._Item._Album = Album()
9348
+ playerState._Item._Album._Name = sonosTrackInfo.get('album','')
9349
+ playerState._Item._Album.Images.append(ImageObject())
9350
+ playerState._Item._Album.Images[0]._Url = sonosTrackInfo.get('album_art','')
9351
+ playerState._Item.Artists.append(Artist())
9352
+ playerState._Item.Artists[0]._Name = sonosTrackInfo.get('artist','')
9353
+
9354
+ # set transport actions.
9355
+ currentTransportState:str = sonosTransportInfo.get('current_transport_state','')
9356
+ if currentTransportState == 'PLAYING':
9357
+ playerState._IsPlaying = True
9358
+ elif currentTransportState in ['PAUSED_PLAYBACK','STOPPED']:
9359
+ playerState.Actions._Pausing = True
9360
+
9361
+ # only update the following PlayState attributes from SoCo if they are NOT set.
9362
+ # these values are used to set the Sonos PlayMode value, which is the actual value that determines
9363
+ # what the Repeat and Shuffle settings are (ya, it's weird!). What is odd is that the Spotify
9364
+ # Web API PlayerState values for Shuffle and Repeat can be different than what the Sonos Controller
9365
+ # API (SoCo) report for the device! This is why we use the Spotify Web API PlayerState values.
9366
+ if playerState._ShuffleState is None:
9367
+ playerState._ShuffleState = sonosPlayer.shuffle
9368
+ if playerState._RepeatState is None:
9369
+ sonosRepeat = sonosPlayer.repeat
9370
+ if sonosRepeat == 'ONE':
9371
+ playerState._RepeatState = 'track'
9372
+ elif sonosRepeat == False:
9373
+ playerState._RepeatState = 'off'
9374
+ elif sonosRepeat == True:
9375
+ playerState._RepeatState = 'context'
9376
+
9377
+ # trace.
9378
+ _logsi.LogObject(SILevel.Verbose, "Current Spotify player PlayState (built from Sonos device current state) for device: %s" % (scDevice.Title), playerState, excludeNonPublic=True)
9379
+ return playerState
9380
+
9381
+ except SpotifyApiError: raise # pass handled exceptions on thru
9382
+ except Exception as ex:
9383
+
9384
+ # format unhandled exception.
9385
+ raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
9386
+
9387
+ finally:
9388
+
9389
+ # trace.
9390
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
9391
+
9392
+
9161
9393
  def GetSpotifyConnectDevice(
9162
9394
  self,
9163
9395
  deviceValue:str,
@@ -9244,6 +9476,7 @@ class SpotifyClient:
9244
9476
  apiMethodName:str = 'GetSpotifyConnectDevice'
9245
9477
  apiMethodParms:SIMethodParmListContext = None
9246
9478
  scDevice:SpotifyConnectDevice = None
9479
+ sonosPlayer:Soco = None
9247
9480
 
9248
9481
  try:
9249
9482
 
@@ -9302,7 +9535,7 @@ class SpotifyClient:
9302
9535
  # if this is a Sonos device and there is no Spotify Client Application token then don't bother.
9303
9536
  # in this case, we will exit and let the calling method switch to the Sonos device and play
9304
9537
  # using the Sonos local queue.
9305
- if (info.IsBrandSonos):
9538
+ if (scDevice.IsSonos):
9306
9539
 
9307
9540
  # check if the Spotify Desktop Application Client oauth2 token is defined.
9308
9541
  hasToken:bool = AuthClient.HasTokenForKey(
@@ -9318,6 +9551,10 @@ class SpotifyClient:
9318
9551
  _logsi.LogVerbose("Spotify Desktop Application Client oauth2 token was not found for Spotify LoginId: '%s'" % (self._SpotifyConnectLoginId))
9319
9552
  return scDevice
9320
9553
 
9554
+ # get Sonos Controller instance for the device.
9555
+ _logsi.LogVerbose("Target device is Sonos; getting Sonos Controller instance for device %s" % (scDevice.Title))
9556
+ sonosPlayer = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
9557
+
9321
9558
  # at this point, we will try to activate the device.
9322
9559
 
9323
9560
  # was device activation requested? if not, then we are done.
@@ -9342,87 +9579,20 @@ class SpotifyClient:
9342
9579
  tokenStorageDir=self.TokenStorageDir,
9343
9580
  tokenStorageFile=self.TokenStorageFile)
9344
9581
 
9345
- # what if we DON'T disconnect the device?
9346
- # will `Connect` switch the user context?
9347
-
9348
- # # disconnect the device.
9349
- # # we will bypass disconnects for librespot devices (e.g. spotifyd, etc), as the
9350
- # # Spotify Connect Zeroconf `resetUsers` endpoint is not implemented and will
9351
- # # generate a 404 request response.
9352
- # if (info.BrandDisplayName != 'librespot'):
9353
-
9354
- # _logsi.LogVerbose("Issuing Disconnect to Spotify Connect device \"%s\" for current user context \"%s\" (ip=%s:%s)" % (scDevice.Title, oldActiveUser, zconn.HostIpAddress, zconn.HostIpPort))
9355
- # zcfResult = zconn.Disconnect(delay)
9356
-
9357
- # # delay just a little to give the device time to process the disconnect.
9358
- # # some devices only support a single connection on the spotify connect
9359
- # # zeroconf webserver, so we have to give it a little time to process the
9360
- # # disconnect command before we try to call the Connect method.
9361
- # _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % DELAY_DISCONNECT)
9362
- # time.sleep(DELAY_DISCONNECT)
9363
-
9364
- # # some manufacturers will reset the device host ip port after a disconnect (e.g. "Denon", etc).
9365
- # # due to this, the device will de-register itself via zeroconf and then re-register itself.
9366
- # # we will now wait for the device to be re-discovered by zeroconf.
9367
- # loopTotalDelay:float = 0
9368
- # LOOP_DELAY:float = 0.25
9369
- # while True:
9370
-
9371
- # # wait just a bit between available device list queries.
9372
- # _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE % LOOP_DELAY)
9373
- # time.sleep(LOOP_DELAY)
9374
- # loopTotalDelay = loopTotalDelay + LOOP_DELAY
9375
-
9376
- # with self._SpotifyConnectDirectory._Zeroconf_Lock:
9377
-
9378
- # # check for the device in the available device list.
9379
- # # if we find it, then we are done.
9380
-
9381
- # device:Device = self.GetPlayerDevice(deviceResultId, True)
9382
- # if device is not None:
9383
- # _logsi.LogVerbose("Spotify Connect device \"%s\" is now in the available device list; device found within %f seconds of Connect" % (scDevice.Title, loopTotalDelay))
9384
- # break
9385
-
9582
+ # is this a Sonos device?
9583
+ if (scDevice.IsSonos):
9584
+
9585
+ # trace.
9586
+ sonosMusicSource:str = sonosPlayer.music_source
9587
+ _logsi.LogVerbose("Sonos device %s music source before Connect: \"%s\"" % (scDevice.Title, sonosMusicSource))
9588
+
9589
+ # was the Sonos device music source set to SPOTIFY_CONNECT?
9590
+ # if not, then issue a Disconnect to (hopefully) reset the music source. this should allow
9591
+ # the subsequent Connect to re-establish a SPOTIFY_CONNECT music source on the Sonos device.
9592
+ if sonosMusicSource != "SPOTIFY_CONNECT":
9593
+ _logsi.LogVerbose("Issuing Disconnect to Sonos Spotify Connect device %s" % (scDevice.Title))
9594
+ zcfResult = zconn.Disconnect(ignoreStatusResult=True)
9386
9595
 
9387
- # # re-discover Spotify Connect devices on the network
9388
- # _logsi.LogVerbose("Rediscovering Spotify Connect devices on the local network (due to previous disconnect)")
9389
- # rediscovery:SpotifyDiscovery = SpotifyDiscovery(self._ZeroconfClient, printToConsole=False)
9390
- # rediscovery.DiscoverDevices(timeout=self._SpotifyConnectDiscoveryTimeout)
9391
-
9392
- # # trace.
9393
- # _logsi.LogDictionary(SILevel.Verbose, "Rediscovered Spotify Connect devices on the local network (due to previous disconnect)", rediscovery.DiscoveryResults, prettyPrint=True)
9394
-
9395
- # # process all rediscovered devices, and update selected device.
9396
- # rediscovery:ZeroconfDiscoveryResult
9397
- # for rediscovery in rediscovery.DiscoveryResults:
9398
-
9399
- # # add / update selected device discovery results by serviceinfo key value.
9400
- # if (rediscovery.Key == scDevice.DiscoveryResult.Key):
9401
-
9402
- # # save old discovery result id so we can log it later.
9403
- # oldDiscoveryResultId:str = discovery.Id
9404
-
9405
- # # update selected device discovery result.
9406
- # _logsi.LogObject(SILevel.Verbose, "Spotify Connect device \"%s\" DiscoveryResult instance rediscovered properties" % (rediscovery.Id), rediscovery, excludeNonPublic=True)
9407
- # _logsi.LogVerbose("Updating Spotify Connect device \"%s\" DiscoveryResult instance with rediscovered properties" % (scDevice.Title))
9408
- # scDevice.DiscoveryResult = rediscovery
9409
- # discovery = rediscovery
9410
-
9411
- # # did the host ip address or port change?
9412
- # # if so, then we need to recreate the ZeroconfConnect instance
9413
- # # in order to access the updated ip address and port.
9414
- # if (discovery.HostIpAddress != zconn.HostIpAddress) \
9415
- # or (discovery.HostIpPort != zconn.HostIpPort):
9416
-
9417
- # _logsi.LogVerbose("Spotify Connect device info changed to %s from %s after disconnect; recreating ZeroconfConnect instance" % (discovery.Id, oldDiscoveryResultId))
9418
- # zconn = ZeroconfConnect(discovery.HostIpAddress,
9419
- # discovery.HostIpPort,
9420
- # discovery.SpotifyConnectCPath,
9421
- # useSSL=False,
9422
- # tokenStorageDir=self.TokenStorageDir,
9423
- # tokenStorageFile=self.TokenStorageFile)
9424
- # break
9425
-
9426
9596
  # connect the device to OUR Spotify Connect user context.
9427
9597
  # note that the result here only indicates that the connect was submitted - NOT that it was successful!
9428
9598
  _logsi.LogVerbose("Issuing Connect to Spotify Connect device %s for user context \"%s\" (ip=%s:%s)" % (scDevice.Title, self._SpotifyConnectLoginId, zconn.HostIpAddress, zconn.HostIpPort))
@@ -9434,6 +9604,26 @@ class SpotifyClient:
9434
9604
  # trace.
9435
9605
  _logsi.LogVerbose("User context switched from \"%s\" to \"%s\" for Spotify Connect device %s" % (oldActiveUser, self._SpotifyConnectLoginId, scDevice.Title))
9436
9606
 
9607
+ # is this a Sonos device?
9608
+ if (scDevice.IsSonos):
9609
+
9610
+ # trace.
9611
+ sonosMusicSource:str = sonosPlayer.music_source
9612
+ _logsi.LogVerbose("Sonos device %s music source after Connect: \"%s\"" % (scDevice.Title, sonosMusicSource))
9613
+
9614
+ # was the Sonos device music source set to Spotify Connect?
9615
+ # if not, then it's a lost cause at this point since it's probably in an UNKNOWN state.
9616
+ if sonosMusicSource != "SPOTIFY_CONNECT":
9617
+ _logsi.LogVerbose("Sonos device %s music source after Connect is not SPOTIFY_CONNECT; it will probably fail to play" % (scDevice.Title))
9618
+
9619
+ # at this point the Sonos music source should be set to SPOTIFY_CONNECT, and control
9620
+ # transferred to the device. note that the Sonos device will still NOT appear in the
9621
+ # player device list, but SHOULD be the active (restricted) device in player state.
9622
+
9623
+ # wait for the device to enter the Spotify Web API player device list, or
9624
+ # to become the active device.
9625
+ # Sonos devices are famous for not showing up in the device list, but DO
9626
+ # appear as the active (restricted) device!
9437
9627
  loopTotalDelay:float = 0
9438
9628
  LOOP_DELAY:float = 0.350
9439
9629
  while True:
@@ -9466,7 +9656,7 @@ class SpotifyClient:
9466
9656
  # only check so many times before we give up;
9467
9657
  # we will keep checking until we find the device, or we exceed the verifyTimeout value.
9468
9658
  if (loopTotalDelay > verifyTimeout):
9469
- _logsi.LogWarning("Verification timeout waiting for Spotify Connect device %s to be added to the player device list; device not found within %f seconds of Connect" % (scDevice.Title, verifyTimeout))
9659
+ _logsi.LogWarning("Verification timeout waiting for Spotify Connect device %s to be added to the player device list or become the active device; device not found within %f seconds of Connect" % (scDevice.Title, verifyTimeout))
9470
9660
  break
9471
9661
 
9472
9662
  # return the device instance.
@@ -11217,15 +11407,24 @@ class SpotifyClient:
11217
11407
  # resolve / activate the device object if needed.
11218
11408
  scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
11219
11409
 
11220
- # build spotify web api request parameters.
11221
- urlParms:dict = {}
11222
- urlParms['device_id'] = scDevice.Id
11410
+ # is this an active Sonos device?
11411
+ if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
11223
11412
 
11224
- # execute spotify web api request.
11225
- msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/pause')
11226
- msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
11227
- msg.UrlParameters = urlParms
11228
- self.MakeRequest('PUT', msg)
11413
+ _logsi.LogVerbose("Issuing command to Sonos device %s: PAUSE" % (scDevice.Title))
11414
+ sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
11415
+ sonosPlayer.pause()
11416
+
11417
+ else:
11418
+
11419
+ # build spotify web api request parameters.
11420
+ urlParms:dict = {}
11421
+ urlParms['device_id'] = scDevice.Id
11422
+
11423
+ # execute spotify web api request.
11424
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/pause')
11425
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
11426
+ msg.UrlParameters = urlParms
11427
+ self.MakeRequest('PUT', msg)
11229
11428
 
11230
11429
  # give spotify web api time to process the change.
11231
11430
  if delay > 0:
@@ -11732,15 +11931,24 @@ class SpotifyClient:
11732
11931
  # resolve / activate the device object if needed.
11733
11932
  scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
11734
11933
 
11735
- # build spotify web api request parameters.
11736
- urlParms:dict = {}
11737
- urlParms['device_id'] = scDevice.Id
11934
+ # is this an active Sonos device?
11935
+ if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
11738
11936
 
11739
- # execute spotify web api request.
11740
- msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/play')
11741
- msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
11742
- msg.UrlParameters = urlParms
11743
- self.MakeRequest('PUT', msg)
11937
+ _logsi.LogVerbose("Issuing command to Sonos device %s: PLAY (RESUME)" % (scDevice.Title))
11938
+ sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
11939
+ sonosPlayer.play()
11940
+
11941
+ else:
11942
+
11943
+ # build spotify web api request parameters.
11944
+ urlParms:dict = {}
11945
+ urlParms['device_id'] = scDevice.Id
11946
+
11947
+ # execute spotify web api request.
11948
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/play')
11949
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
11950
+ msg.UrlParameters = urlParms
11951
+ self.MakeRequest('PUT', msg)
11744
11952
 
11745
11953
  # give spotify web api time to process the change.
11746
11954
  if delay > 0:
@@ -11871,18 +12079,28 @@ class SpotifyClient:
11871
12079
  newPositionMS = 0
11872
12080
  positionMS = newPositionMS
11873
12081
 
11874
- # build spotify web api request parameters.
11875
- urlParms:dict = \
11876
- {
11877
- 'position_ms': positionMS
11878
- }
11879
- urlParms['device_id'] = scDevice.Id
12082
+ # is this an active Sonos device?
12083
+ if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
11880
12084
 
11881
- # execute spotify web api request.
11882
- msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/seek')
11883
- msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
11884
- msg.UrlParameters = urlParms
11885
- self.MakeRequest('PUT', msg)
12085
+ sonosPosition:str = mediaPositionHMS_fromSeconds(positionMS / 1000) # convert from milliseconds to Sonos H:MM:SS format
12086
+ _logsi.LogVerbose("Issuing command to Sonos device %s: SEEK (sonosPosition=%s)" % (scDevice.Title, sonosPosition))
12087
+ sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
12088
+ sonosPlayer.seek(str(sonosPosition))
12089
+
12090
+ else:
12091
+
12092
+ # build spotify web api request parameters.
12093
+ urlParms:dict = \
12094
+ {
12095
+ 'position_ms': positionMS
12096
+ }
12097
+ urlParms['device_id'] = scDevice.Id
12098
+
12099
+ # execute spotify web api request.
12100
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/seek')
12101
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12102
+ msg.UrlParameters = urlParms
12103
+ self.MakeRequest('PUT', msg)
11886
12104
 
11887
12105
  # give spotify web api time to process the change.
11888
12106
  if delay > 0:
@@ -11964,15 +12182,24 @@ class SpotifyClient:
11964
12182
  # resolve / activate the device object if needed.
11965
12183
  scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
11966
12184
 
11967
- # build spotify web api request parameters.
11968
- urlParms:dict = {}
11969
- urlParms['device_id'] = scDevice.Id
12185
+ # is this an active Sonos device?
12186
+ if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
11970
12187
 
11971
- # execute spotify web api request.
11972
- msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/next')
11973
- msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
11974
- msg.UrlParameters = urlParms
11975
- self.MakeRequest('POST', msg)
12188
+ _logsi.LogVerbose("Issuing command to Sonos device %s: NEXT" % (scDevice.Title))
12189
+ sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
12190
+ sonosPlayer.next()
12191
+
12192
+ else:
12193
+
12194
+ # build spotify web api request parameters.
12195
+ urlParms:dict = {}
12196
+ urlParms['device_id'] = scDevice.Id
12197
+
12198
+ # execute spotify web api request.
12199
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/next')
12200
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12201
+ msg.UrlParameters = urlParms
12202
+ self.MakeRequest('POST', msg)
11976
12203
 
11977
12204
  # give spotify web api time to process the change.
11978
12205
  if delay > 0:
@@ -12054,15 +12281,24 @@ class SpotifyClient:
12054
12281
  # resolve / activate the device object if needed.
12055
12282
  scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
12056
12283
 
12057
- # build spotify web api request parameters.
12058
- urlParms:dict = {}
12059
- urlParms['device_id'] = scDevice.Id
12284
+ # is this an active Sonos device?
12285
+ if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
12060
12286
 
12061
- # execute spotify web api request.
12062
- msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/previous')
12063
- msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12064
- msg.UrlParameters = urlParms
12065
- self.MakeRequest('POST', msg)
12287
+ _logsi.LogVerbose("Issuing command to Sonos device %s: PREVIOUS" % (scDevice.Title))
12288
+ sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
12289
+ sonosPlayer.previous()
12290
+
12291
+ else:
12292
+
12293
+ # build spotify web api request parameters.
12294
+ urlParms:dict = {}
12295
+ urlParms['device_id'] = scDevice.Id
12296
+
12297
+ # execute spotify web api request.
12298
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/previous')
12299
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12300
+ msg.UrlParameters = urlParms
12301
+ self.MakeRequest('POST', msg)
12066
12302
 
12067
12303
  # give spotify web api time to process the change.
12068
12304
  if delay > 0:
@@ -12152,18 +12388,44 @@ class SpotifyClient:
12152
12388
  # resolve / activate the device object if needed.
12153
12389
  scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
12154
12390
 
12155
- # build spotify web api request parameters.
12156
- urlParms:dict = \
12157
- {
12158
- 'state': state
12159
- }
12160
- urlParms['device_id'] = scDevice.Id
12391
+ # is this an active Sonos device?
12392
+ if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
12393
+
12394
+ # get current Spotify Connect player state.
12395
+ playerState = self.GetSonosPlayerPlaybackState(scDevice)
12396
+
12397
+ # set the Sonos PlayMode directly (instead of setting the Sonos repeat property)!
12398
+ # The Spotify Web API PlayerState `ShuffleState` value can sometimes be different than what the
12399
+ # Sonos device reports for its `shuffle` property value!
12400
+ if state == 'track':
12401
+ sonos_repeat = 'ONE'
12402
+ elif state == 'off':
12403
+ sonos_repeat = False
12404
+ elif state == 'context':
12405
+ sonos_repeat = True
12406
+ else:
12407
+ sonos_repeat = False
12408
+ playMode:str = SONOS_PLAY_MODE_BY_MEANING[(playerState.ShuffleState, sonos_repeat)]
12409
+
12410
+ # execute SoCo api request.
12411
+ _logsi.LogVerbose("Issuing command to Sonos device %s: REPEAT (playmode=%s)" % (scDevice.Title, playMode))
12412
+ sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
12413
+ sonosPlayer.play_mode = playMode
12161
12414
 
12162
- # execute spotify web api request.
12163
- msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/repeat')
12164
- msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12165
- msg.UrlParameters = urlParms
12166
- self.MakeRequest('PUT', msg)
12415
+ else:
12416
+
12417
+ # build spotify web api request parameters.
12418
+ urlParms:dict = \
12419
+ {
12420
+ 'state': state
12421
+ }
12422
+ urlParms['device_id'] = scDevice.Id
12423
+
12424
+ # execute spotify web api request.
12425
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/repeat')
12426
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12427
+ msg.UrlParameters = urlParms
12428
+ self.MakeRequest('PUT', msg)
12167
12429
 
12168
12430
  # give spotify web api time to process the change.
12169
12431
  if delay > 0:
@@ -12252,18 +12514,45 @@ class SpotifyClient:
12252
12514
  # resolve / activate the device object if needed.
12253
12515
  scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
12254
12516
 
12255
- # build spotify web api request parameters.
12256
- urlParms:dict = \
12257
- {
12258
- 'state': state
12259
- }
12260
- urlParms['device_id'] = scDevice.Id
12517
+ # is this an active Sonos device?
12518
+ if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
12519
+
12520
+ # get current Spotify Connect player state.
12521
+ playerState = self.GetSonosPlayerPlaybackState(scDevice)
12522
+
12523
+ # set the Sonos PlayMode directly (instead of setting the Sonos `shuffle` property)!
12524
+ # the Spotify Web API PlayerState `RepeatState` value can sometimes be different than what the
12525
+ # Sonos device reports for its `repeat` property value!
12526
+ sonos_repeat:str
12527
+ if playerState.RepeatState == 'track':
12528
+ sonos_repeat = 'ONE'
12529
+ elif playerState.RepeatState == 'off':
12530
+ sonos_repeat = False
12531
+ elif playerState.RepeatState == 'context':
12532
+ sonos_repeat = True
12533
+ else: # assume off if nothing else.
12534
+ sonos_repeat = False
12535
+ playMode:str = SONOS_PLAY_MODE_BY_MEANING[(state, sonos_repeat)]
12536
+
12537
+ # execute SoCo api request.
12538
+ _logsi.LogVerbose("Issuing command to Sonos device %s: SHUFFLE (playmode=%s)" % (scDevice.Title, playMode))
12539
+ sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
12540
+ sonosPlayer.play_mode = playMode
12261
12541
 
12262
- # execute spotify web api request.
12263
- msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/shuffle')
12264
- msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12265
- msg.UrlParameters = urlParms
12266
- self.MakeRequest('PUT', msg)
12542
+ else:
12543
+
12544
+ # build spotify web api request parameters.
12545
+ urlParms:dict = \
12546
+ {
12547
+ 'state': state
12548
+ }
12549
+ urlParms['device_id'] = scDevice.Id
12550
+
12551
+ # execute spotify web api request.
12552
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/shuffle')
12553
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12554
+ msg.UrlParameters = urlParms
12555
+ self.MakeRequest('PUT', msg)
12267
12556
 
12268
12557
  # give spotify web api time to process the change.
12269
12558
  if delay > 0:
@@ -12351,18 +12640,27 @@ class SpotifyClient:
12351
12640
  # resolve / activate the device object if needed.
12352
12641
  scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
12353
12642
 
12354
- # build spotify web api request parameters.
12355
- urlParms:dict = \
12356
- {
12357
- 'volume_percent': volumePercent
12358
- }
12359
- urlParms['device_id'] = scDevice.Id
12643
+ # is this an active Sonos device?
12644
+ if (scDevice.IsSonos) and (scDevice.IsActiveDevice):
12360
12645
 
12361
- # execute spotify web api request.
12362
- msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/volume')
12363
- msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12364
- msg.UrlParameters = urlParms
12365
- self.MakeRequest('PUT', msg)
12646
+ _logsi.LogVerbose("Issuing command to Sonos device %s: VOLUME = %s" % (scDevice.Title, volumePercent))
12647
+ sonosPlayer:Soco = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
12648
+ sonosPlayer.volume = volumePercent
12649
+
12650
+ else:
12651
+
12652
+ # build spotify web api request parameters.
12653
+ urlParms:dict = \
12654
+ {
12655
+ 'volume_percent': volumePercent
12656
+ }
12657
+ urlParms['device_id'] = scDevice.Id
12658
+
12659
+ # execute spotify web api request.
12660
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/volume')
12661
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12662
+ msg.UrlParameters = urlParms
12663
+ self.MakeRequest('PUT', msg)
12366
12664
 
12367
12665
  # give spotify web api time to process the change.
12368
12666
  if delay > 0:
@@ -12472,10 +12770,40 @@ class SpotifyClient:
12472
12770
 
12473
12771
  # is this a Sonos device?
12474
12772
  # if so, then control was automatically transferred by activating the device.
12475
- # note that we also cannot stop nor start play on the device either, as it's
12476
- # considered a restricted device and the Spotify Web API cannot control it!
12477
- if scDevice.DeviceInfo.IsBrandSonos:
12478
- _logsi.LogVerbose('Sonos device detected; bypassing call to Spotify Web API Transfer Playback endpoint')
12773
+ # note that the Spotify Web API cannot stop nor start play on the device either,
12774
+ # as it's considered a restricted device and the Spotify Web API cannot control it!
12775
+ # all requests to control the device will be made via the Sonos Controller API.
12776
+ if (scDevice.IsSonos):
12777
+
12778
+ # get Sonos Controller instance for the device.
12779
+ sonosPlayer:SoCo = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
12780
+
12781
+ # get current Sonos transport status.
12782
+ sonosTransportInfo:dict = sonosPlayer.get_current_transport_info()
12783
+ currentTransportState:str = sonosTransportInfo.get('current_transport_state', None)
12784
+ currentTransportStatus:str = sonosTransportInfo.get('current_transport_status', None)
12785
+ _logsi.LogVerbose("Sonos device %s current transport state after activation: \"%s\" (Status=%s)" % (scDevice.Title, currentTransportState, currentTransportStatus))
12786
+
12787
+ # stop / start play as requested.
12788
+ wasCmdIssued:bool = False
12789
+ if currentTransportState == 'PLAYING':
12790
+ if play == False:
12791
+ _logsi.LogVerbose("Issuing command to Sonos device %s: PAUSE" % (scDevice.Title))
12792
+ sonosPlayer.pause()
12793
+ wasCmdIssued = True
12794
+ elif currentTransportState in ['PAUSED_PLAYBACK','STOPPED','TRANSITIONING']:
12795
+ if play == True:
12796
+ _logsi.LogVerbose("Issuing command to Sonos device %s: PLAY" % (scDevice.Title))
12797
+ sonosPlayer.play()
12798
+ wasCmdIssued = True
12799
+
12800
+ # give SoCo api time to process the change.
12801
+ if (wasCmdIssued) and (delay > 0):
12802
+ _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE_SONOS % delay)
12803
+ time.sleep(delay)
12804
+
12805
+ # trace.
12806
+ _logsi.LogVerbose("Sonos device detected (IsRestricted); bypassing call to Spotify Web API Transfer Playback endpoint")
12479
12807
  return
12480
12808
 
12481
12809
  # before transferring playback to the device, we first need to check to see if anything