spotifywebapiPython 1.0.150__py3-none-any.whl → 1.0.152__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 (26) hide show
  1. {spotifywebapiPython-1.0.150.dist-info → spotifywebapiPython-1.0.152.dist-info}/METADATA +2 -1
  2. {spotifywebapiPython-1.0.150.dist-info → spotifywebapiPython-1.0.152.dist-info}/RECORD +26 -26
  3. spotifywebapipython/const.py +6 -1
  4. spotifywebapipython/docs/__init__.html +15 -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 +337 -120
  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/playerplaystate.py +42 -0
  19. spotifywebapipython/models/spotifyconnectdevice.py +12 -0
  20. spotifywebapipython/sautils.py +50 -0
  21. spotifywebapipython/spotifyclient.py +847 -296
  22. spotifywebapipython/spotifyconnect/spotifyconnectdirectorytask.py +98 -16
  23. spotifywebapipython/spotifyconnect/spotifyconnectzeroconfexceptions.py +10 -0
  24. {spotifywebapiPython-1.0.150.dist-info → spotifywebapiPython-1.0.152.dist-info}/LICENSE +0 -0
  25. {spotifywebapiPython-1.0.150.dist-info → spotifywebapiPython-1.0.152.dist-info}/WHEEL +0 -0
  26. {spotifywebapiPython-1.0.150.dist-info → spotifywebapiPython-1.0.152.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
@@ -733,19 +747,28 @@ class SpotifyClient:
733
747
 
734
748
  def _ResolveDeviceObject(
735
749
  self,
736
- device:str | SpotifyConnectDevice
750
+ device:str | SpotifyConnectDevice,
751
+ activateDevice:bool=True,
737
752
  ) -> SpotifyConnectDevice:
738
753
  """
739
754
  Resolves a SpotifyConnectDevice object from a device identifier, name, or
740
- SpotifyConnectDevice object argument value. An exception will be raised
741
- if the `device` argument could not be resolved or activated.
755
+ SpotifyConnectDevice object argument value. A null object will be returned
756
+ if the `device` argument could not be resolved.
742
757
 
743
758
  Args:
744
- device (str|SpotifyConnectDevice):
745
- A device identifier, name, or a SpotifyConnectDevice object.
759
+ device (str | SpotifyConnectDevice) | None):
760
+ The target player device identifier.
761
+ This could be an id, name, a default device indicator (e.g. "*"), a
762
+ SpotifyConnectDevice object, or null to utilize the active player device.
763
+ A device is considered resolved if a SpotifyConnectDevice object is passed
764
+ for this argument.
765
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
766
+ activateDevice (bool):
767
+ True to activate the device if necessary; otherwise, False.
768
+ Default is True.
746
769
 
747
770
  Returns:
748
- A SpotifyConnectDevice object for the specified device.
771
+ A SpotifyConnectDevice object for the specified device if found; otherwise, null.
749
772
 
750
773
  Raises:
751
774
  SpotifyApiError:
@@ -766,13 +789,13 @@ class SpotifyClient:
766
789
 
767
790
  else:
768
791
 
769
- # no - ensure the specified device id / name is active and available,
770
- # and return the SpotifyConnectDevice object.
771
- # an exception will be thrown if the device id could not be activated.
792
+ # no - ensure the specified device id / name is active (optional) and
793
+ # available, and return the SpotifyConnectDevice object.
794
+ # a null object will be returned if the device id could not be found.
772
795
  scDevice = self.GetSpotifyConnectDevice(
773
796
  device,
774
797
  refreshDeviceList=True,
775
- activateDevice=True)
798
+ activateDevice=activateDevice)
776
799
 
777
800
  return scDevice
778
801
 
@@ -1139,12 +1162,16 @@ class SpotifyClient:
1139
1162
  Example: `spotify:track:6zd8T1PBe9JFHmuVnurdRp,spotify:track:1kWUud3vY5ij5r62zxpTRy`.
1140
1163
  An unlimited number of items can be added in one request, but the more items the longer it
1141
1164
  will take.
1142
- deviceId (str):
1143
- The id or name of the device this command is targeting.
1144
- If not supplied, the user's currently active device is the target.
1145
- Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
1146
- Example: `Web Player (Chrome)`
1165
+ deviceId (str | SpotifyConnectDevice) | None):
1166
+ The target player device identifier.
1167
+ This could be an id, name, a default device indicator (e.g. "*"), a
1168
+ SpotifyConnectDevice object, or null to utilize the active player device.
1169
+ A device is considered resolved if a SpotifyConnectDevice object is passed
1170
+ for this argument. An exception will be raised if the argument value could
1171
+ not be resolved or activated.
1172
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
1147
1173
  verifyDeviceId (bool):
1174
+ DEPRECATED - deviceId is automatically verified as of v1.0.149.
1148
1175
  True to verify a device id is active; otherwise, false to assume that a
1149
1176
  device id is already active.
1150
1177
  Default is True.
@@ -1166,7 +1193,7 @@ class SpotifyClient:
1166
1193
 
1167
1194
  The Spotify Web API endpoint is called to add each item individually.
1168
1195
  The Spotify Web API does not currently support adding more than 1 item to the queue at a time.
1169
-
1196
+
1170
1197
  <details>
1171
1198
  <summary>Sample Code</summary>
1172
1199
  ```python
@@ -1183,14 +1210,12 @@ class SpotifyClient:
1183
1210
  apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
1184
1211
  apiMethodParms.AppendKeyValue("uris", uris)
1185
1212
  apiMethodParms.AppendKeyValue("deviceId", deviceId)
1186
- apiMethodParms.AppendKeyValue("verifyDeviceId", verifyDeviceId)
1213
+ apiMethodParms.AppendKeyValue("verifyDeviceId (DEPRECATED)", verifyDeviceId)
1187
1214
  apiMethodParms.AppendKeyValue("delay", delay)
1188
1215
  _logsi.LogMethodParmList(SILevel.Verbose, "Add items to playback queue", apiMethodParms)
1189
1216
 
1190
1217
  # validations.
1191
1218
  delay = validateDelay(delay, 0.15, 10)
1192
- if (verifyDeviceId is None):
1193
- verifyDeviceId = True
1194
1219
 
1195
1220
  # build a list of all item uri's.
1196
1221
  # remove any leading / trailing spaces in case user put a space between the items.
@@ -1203,7 +1228,7 @@ class SpotifyClient:
1203
1228
  arrUris[idx] = arrUris[idx].strip()
1204
1229
 
1205
1230
  # resolve / activate the device object if needed.
1206
- scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
1231
+ scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId, False)
1207
1232
 
1208
1233
  # process all uri's.
1209
1234
  for idx in range(0, len(arrUris)):
@@ -1213,7 +1238,8 @@ class SpotifyClient:
1213
1238
  {
1214
1239
  'uri': arrUris[idx]
1215
1240
  }
1216
- urlParms['device_id'] = scDevice.Id
1241
+ if (scDevice is not None):
1242
+ urlParms['device_id'] = scDevice.Id
1217
1243
 
1218
1244
  # execute spotify web api request.
1219
1245
  msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/queue')
@@ -1966,7 +1992,7 @@ class SpotifyClient:
1966
1992
  A comma-separated list of Spotify User ID's to check.
1967
1993
  Maximum: 5 ID's.
1968
1994
  Example: `1kWUud3vY5ij5r62zxpTRy,2takcwOaAZWiXQijPHIx7B`
1969
- Deprecated - A single item list containing current user's Spotify Username; Maximum of 1 id.
1995
+ DEPRECATED - A single item list containing current user's Spotify Username; Maximum of 1 id.
1970
1996
 
1971
1997
  Returns:
1972
1998
  Array of boolean, containing a single boolean status that indicates
@@ -2000,7 +2026,7 @@ class SpotifyClient:
2000
2026
  # trace.
2001
2027
  apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
2002
2028
  apiMethodParms.AppendKeyValue("playlistId", playlistId)
2003
- apiMethodParms.AppendKeyValue("userIds", userIds)
2029
+ apiMethodParms.AppendKeyValue("userIds (DEPRECATED)", userIds)
2004
2030
  _logsi.LogMethodParmList(SILevel.Verbose, "Check to see if users are following a playlist", apiMethodParms)
2005
2031
 
2006
2032
  # validations.
@@ -5457,7 +5483,8 @@ class SpotifyClient:
5457
5483
  Example: `sv_SE`
5458
5484
  refresh (bool):
5459
5485
  True to return real-time information from the spotify web api and
5460
- update the cache; otherwise, False to just return the cached value.
5486
+ update the cache; otherwise, False to just return the cached value.
5487
+ Default is True.
5461
5488
 
5462
5489
  Returns:
5463
5490
  A `list[Category]` object that contains the list category details.
@@ -6075,6 +6102,94 @@ class SpotifyClient:
6075
6102
  _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
6076
6103
 
6077
6104
 
6105
+ def GetDevicePlaybackState(
6106
+ self,
6107
+ deviceId:str|SpotifyConnectDevice|None=None,
6108
+ ) -> PlayerPlayState:
6109
+ """
6110
+ Get information about the current playback state of the device, including track or episode,
6111
+ and progress. For Sonos devices, this will return the playstate of the device instead of the
6112
+ active Spotify Web API player playstate.
6113
+
6114
+ This method requires the `user-read-playback-state` scope.
6115
+
6116
+ This is not part of the Spotify Web API specification.
6117
+
6118
+ Args:
6119
+ deviceId (str | SpotifyConnectDevice) | None):
6120
+ The target player device identifier.
6121
+ This could be an id, name, a default device indicator (e.g. "*"), a
6122
+ SpotifyConnectDevice object, or null to utilize the active player device.
6123
+ A device is considered resolved if a SpotifyConnectDevice object is passed
6124
+ for this argument. An exception will be raised if the argument value could
6125
+ not be resolved.
6126
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
6127
+
6128
+ Returns:
6129
+ A `PlayerPlayState` object that contains player state details as well as
6130
+ currently playing content.
6131
+
6132
+ Raises:
6133
+ SpotifyWebApiError:
6134
+ If the Spotify Web API request was for a non-authorization service
6135
+ and the response contains error information.
6136
+ SpotifyApiError:
6137
+ If the method fails for any other reason.
6138
+
6139
+ <details>
6140
+ <summary>Sample Code</summary>
6141
+ ```python
6142
+ .. include:: ../docs/include/samplecode/SpotifyClient/GetDevicePlaybackState.py
6143
+ ```
6144
+ </details>
6145
+ """
6146
+ apiMethodName:str = 'GetDevicePlaybackState'
6147
+ apiMethodParms:SIMethodParmListContext = None
6148
+ result:PlayerPlayState = None
6149
+
6150
+ try:
6151
+
6152
+ # trace.
6153
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
6154
+ apiMethodParms.AppendKeyValue("deviceId", deviceId)
6155
+ _logsi.LogMethodParmList(SILevel.Verbose, "Get the current playback state of the device", apiMethodParms)
6156
+
6157
+ # resolve the device object; no need to activate it for what we want here.
6158
+ # this will also set the currently active device in the devices cache, as well as
6159
+ # add the playstate to the configuration cache.
6160
+ scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId, activateDevice=False)
6161
+
6162
+ # get cached Spotify Web API playerstate (_ResolveDeviceObject method just refreshed it).
6163
+ result = self.GetPlayerPlaybackState(refresh=False)
6164
+
6165
+ # is this a Sonos device?
6166
+ if (scDevice is not None) and (scDevice.IsSonos):
6167
+
6168
+ # get Sonos playback state via Sonos Controller instance.
6169
+ playerStateSonos:PlayerPlayState = self.GetPlayerPlaybackStateSonos(scDevice)
6170
+
6171
+ # return the device playstate.
6172
+ _logsi.LogVerbose("Sonos device %s playstate will be returned" % (scDevice.Title))
6173
+ result = playerStateSonos
6174
+
6175
+ # trace.
6176
+ _logsi.LogObject(SILevel.Verbose, TRACE_METHOD_RESULT_TYPE % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
6177
+ return result
6178
+
6179
+ except SpotifyApiError: raise # pass handled exceptions on thru
6180
+ except SpotifyWebApiError: raise # pass handled exceptions on thru
6181
+ except SpotifyWebApiAuthenticationError: raise # pass handled exceptions on thru
6182
+ except Exception as ex:
6183
+
6184
+ # format unhandled exception.
6185
+ raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
6186
+
6187
+ finally:
6188
+
6189
+ # trace.
6190
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
6191
+
6192
+
6078
6193
  def GetEpisode(
6079
6194
  self,
6080
6195
  episodeId:str=None,
@@ -7477,7 +7592,8 @@ class SpotifyClient:
7477
7592
  def GetPlayerPlaybackState(
7478
7593
  self,
7479
7594
  market:str=None,
7480
- additionalTypes:str=None
7595
+ additionalTypes:str=None,
7596
+ refresh:bool=True,
7481
7597
  ) -> PlayerPlayState:
7482
7598
  """
7483
7599
  Get information about the user's current playback state, including track or episode, progress,
@@ -7501,6 +7617,10 @@ class SpotifyClient:
7501
7617
  Note: This parameter was introduced to allow existing clients to maintain their current behaviour
7502
7618
  and might be deprecated in the future. In addition to providing this parameter, make sure that your client
7503
7619
  properly handles cases of new types in the future by checking against the type field of each object.
7620
+ refresh (bool):
7621
+ True to return real-time information from the Spotify Web API and
7622
+ update the cache; otherwise, False to just return the cached value.
7623
+ Default is True.
7504
7624
 
7505
7625
  Returns:
7506
7626
  A `PlayerPlayState` object that contains player state details as well as
@@ -7529,8 +7649,25 @@ class SpotifyClient:
7529
7649
  # trace.
7530
7650
  apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
7531
7651
  apiMethodParms.AppendKeyValue("market", market)
7652
+ apiMethodParms.AppendKeyValue("additionalTypes", additionalTypes)
7653
+ apiMethodParms.AppendKeyValue("refresh", refresh)
7532
7654
  _logsi.LogMethodParmList(SILevel.Verbose, "Get the user's current playback state", apiMethodParms)
7533
-
7655
+
7656
+ # validations.
7657
+ if (refresh is None):
7658
+ refresh = True
7659
+
7660
+ # are we getting the cached value? if so, then just return it.
7661
+ if (not refresh):
7662
+
7663
+ result = self._ConfigurationCache.get(apiMethodName, None)
7664
+ if (result is None):
7665
+ result = PlayerPlayState()
7666
+
7667
+ # trace.
7668
+ _logsi.LogObject(SILevel.Verbose, TRACE_METHOD_RESULT_TYPE % (apiMethodName + " (cached)", type(result).__name__), result, excludeNonPublic=True)
7669
+ return result
7670
+
7534
7671
  # ensure market was either supplied or implied; default if neither.
7535
7672
  market = self._ValidateMarket(market)
7536
7673
 
@@ -7549,9 +7686,12 @@ class SpotifyClient:
7549
7686
 
7550
7687
  # process results.
7551
7688
  result = PlayerPlayState(root=msg.ResponseData)
7552
-
7689
+
7690
+ # update cache with result.
7691
+ self._ConfigurationCache[apiMethodName] = result
7692
+
7553
7693
  # is this an episode? if so, then determine the item type (e.g. podcast or audiobook).
7554
- if (result is not None):
7694
+ if (not result.IsEmpty):
7555
7695
  if (result.Item is not None):
7556
7696
  result.ItemType = SpotifyClient.GetTypeFromUri(result.Item.Uri)
7557
7697
  if (result.CurrentlyPlayingType == 'episode'):
@@ -7563,7 +7703,7 @@ class SpotifyClient:
7563
7703
 
7564
7704
  # check for a restricted device; if found, then grab the device id from the
7565
7705
  # cached list of spotify connect devices (if present).
7566
- if result is not None:
7706
+ if (not result.IsEmpty):
7567
7707
  device:Device = result.Device
7568
7708
  if (device is not None) and (device.Id is None or device.Id == ''):
7569
7709
  cacheKey:str = 'GetSpotifyConnectDevices'
@@ -7573,6 +7713,11 @@ class SpotifyClient:
7573
7713
  if scDevice is not None:
7574
7714
  device.Id = scDevice.DeviceInfo.DeviceId
7575
7715
 
7716
+ # update Spotify Connect Directory with active device details.
7717
+ # IMPORTANT - make sure result contains an object, otherwise it's an endless loop!
7718
+ if (result is not None):
7719
+ self.SpotifyConnectDirectory.UpdateActiveDevice(result)
7720
+
7576
7721
  # trace.
7577
7722
  _logsi.LogObject(SILevel.Verbose, TRACE_METHOD_RESULT_TYPE % (apiMethodName, type(result).__name__), result, excludeNonPublic=True)
7578
7723
  return result
@@ -7591,6 +7736,222 @@ class SpotifyClient:
7591
7736
  _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
7592
7737
 
7593
7738
 
7739
+ def GetPlayerPlaybackStateSonos(
7740
+ self,
7741
+ scDevice:SpotifyConnectDevice,
7742
+ ) -> PlayerPlayState:
7743
+ """
7744
+ Get information about the Sonos user's current playback state, including track or episode, progress,
7745
+ and active device.
7746
+
7747
+ This is not part of the Spotify Web API specification; it is for Sonos devices only!
7748
+
7749
+ Args:
7750
+ scDevice (SpotifyConnectDevice):
7751
+ Spotify Connect device instance used to identify the Sonos player.
7752
+
7753
+ Returns:
7754
+ A `PlayerPlayState` object that contains Sonos player state details as well as
7755
+ currently playing content.
7756
+
7757
+ Raises:
7758
+ SpotifyApiError:
7759
+ If the method fails for any other reason.
7760
+ """
7761
+ apiMethodName:str = 'GetPlayerPlaybackStateSonos'
7762
+ apiMethodParms:SIMethodParmListContext = None
7763
+
7764
+ try:
7765
+
7766
+ # trace.
7767
+ apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
7768
+ apiMethodParms.AppendKeyValue("scDevice", scDevice)
7769
+ _logsi.LogMethodParmList(SILevel.Verbose, "Get Sonos device current playback state", apiMethodParms)
7770
+
7771
+ # create playstate object.
7772
+ playerState = PlayerPlayState()
7773
+
7774
+ # get Sonos Controller instance for the device.
7775
+ _logsi.LogVerbose("Getting Sonos device status via Sonos Controller instance for device: %s" % (scDevice.Title))
7776
+ sonosPlayer:SoCo = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
7777
+
7778
+ # build a spotify playback status instance with equivalent Sonos state values.
7779
+ sonosTrackInfo:dict = sonosPlayer.get_current_track_info()
7780
+ _logsi.LogDictionary(SILevel.Verbose, "Sonos device current_track_info results for device: %s" % (scDevice.Title), sonosTrackInfo, prettyPrint=True)
7781
+ sonosTransportInfo:dict = sonosPlayer.get_current_transport_info()
7782
+ _logsi.LogDictionary(SILevel.Verbose, "Sonos device get_current_transport_info results for device: %s" % (scDevice.Title), sonosTransportInfo, prettyPrint=True)
7783
+
7784
+ # indicate this is a device-specific playstate.
7785
+ playerState.IsDeviceState = True
7786
+ playerState.DeviceMusicSource = "" + str(sonosPlayer.music_source)
7787
+
7788
+ # update Spotify Web API player state with Sonos player state.
7789
+ playerState._Device._Id = scDevice.Id
7790
+ playerState._Device._Name = sonosPlayer.player_name
7791
+ playerState._Device._VolumePercent = sonosPlayer.volume
7792
+ playerState._Device._IsActive = True
7793
+ playerState._Device._IsRestricted = True
7794
+ playerState._Device._SupportsVolume = True
7795
+ playerState._Device._Type = 'SPEAKER'
7796
+
7797
+ # try to get the spotify uri from the "metadata" music library DIDL value, as
7798
+ # the "uri" value (e.g. "x-sonos-vli:RINCON_38420B909DC801400:2,spotify:e934941535d7b182815bf688490ce8a8")
7799
+ # is not a true spotify uri value (e.g. "spotify:track:6kYyS9g4WJeRzTYqsmcMmM")
7800
+ spotifyUri:str = sonosTrackInfo.get('uri','')
7801
+ didl:str = sonosTrackInfo.get('metadata',None)
7802
+ if (didl):
7803
+ METADATA_ID:str = 'x-sonos-spotify:'
7804
+ idx:int = didl.find(METADATA_ID)
7805
+ if (idx > -1):
7806
+ spotifyUri:str = didl[idx + len(METADATA_ID):]
7807
+ spotifyUri = urllib.parse.unquote(spotifyUri)
7808
+ idx = spotifyUri.find('?')
7809
+ if (idx > -1):
7810
+ spotifyUri = spotifyUri[:idx]
7811
+ spotifyType = self.GetTypeFromUri(spotifyUri) or ''
7812
+ spotifyId = self.GetIdFromUri(spotifyUri) or ''
7813
+
7814
+ # set base item properties.
7815
+ playerState.IsEmpty = False
7816
+ playerState.ItemType = spotifyType
7817
+ sTimeValue:str = sonosTrackInfo.get('position',None)
7818
+ playerState._ProgressMS = mediaPositionHMS_toSeconds(sTimeValue) * 1000 # convert h:mm:ss to milliseconds
7819
+
7820
+ # set some item properties based on the playing type (episode or non-episode).
7821
+ if (spotifyType == 'episode'):
7822
+
7823
+ # get the episode data from spotify, as Sonos Soco data is incomplete.
7824
+ playerState._CurrentlyPlayingType = 'episode'
7825
+ episode:Episode = self.GetEpisode(spotifyId)
7826
+ if (episode.Id is not None):
7827
+ playerState._Item = episode
7828
+ if (playerState._Context is None):
7829
+ playerState._Context = Context()
7830
+ playerState._Context._Type = episode.Show.Type
7831
+ playerState._Context._Uri = episode.Show.Uri
7832
+ playerState._Context._Href = episode.Show.Href
7833
+ playerState._Context._ExternalUrls = episode.Show.ExternalUrls
7834
+ else:
7835
+ # if data could not be obtained from spotify, then use what's
7836
+ # available from Sonos Soco api metadata.
7837
+ playerState._Item = Episode()
7838
+ playerState._Item._Name = sonosTrackInfo.get('title','')
7839
+ playerState._Item._Uri = spotifyUri
7840
+ playerState._Item._Description = 'Sonos device does not provide a description'
7841
+ sTimeValue:str = sonosTrackInfo.get('duration',None)
7842
+ playerState._Item._DurationMS = mediaPositionHMS_toSeconds(sTimeValue) * 1000 # convert h:mm:ss to milliseconds
7843
+ playerState._Item._Explicit = False
7844
+ playerState._Item._Href = 'https://api.spotify.com/v1/episodes/' + spotifyId
7845
+ playerState._Item._HtmlDescription = playerState._Item._Description
7846
+ playerState._Item._Id = spotifyId
7847
+ playerState._Item.Images.append(ImageObject())
7848
+ playerState._Item.Images[0]._Url = sonosTrackInfo.get('album_art','')
7849
+ playerState._Item._TrackNumber = sonosTrackInfo.get('playlist_position','')
7850
+ playerState._Item._ReleaseDate = '0000'
7851
+ playerState._Item._ReleaseDatePrecision = 'year'
7852
+ playerState._Item._Type = 'episode'
7853
+ playerState._Item._Show = Show()
7854
+ playerState._Item._Show._Name = sonosTrackInfo.get('album','') # TODO get this from metadata (<r:podcast>The Elfstones of Shannara</r:podcast>)
7855
+ playerState._Item._Show.Images.append(ImageObject())
7856
+ playerState._Item._Show.Images[0]._Url = sonosTrackInfo.get('album_art','')
7857
+ playerState._Item._Show._Publisher = sonosTrackInfo.get('artist','')
7858
+
7859
+ # if show name not set then get it from metadata.
7860
+ if (playerState._Item._Show._Name == ''):
7861
+ if (didl):
7862
+ METADATA_ID:str = '<r:podcast>'
7863
+ METADATA_END:str = '</r:podcast>'
7864
+ idx:int = didl.find(METADATA_ID)
7865
+ if (idx > -1):
7866
+ metaValue:str = didl[idx + len(METADATA_ID):]
7867
+ idx = metaValue.find(METADATA_END)
7868
+ if (idx > -1):
7869
+ metaValue = metaValue[:idx]
7870
+ playerState._Item._Show._Name = metaValue
7871
+
7872
+ # if episode is playing then resolve the underlying type (audiobook / podcast show).
7873
+ if (self.IsChapterEpisode(spotifyId)):
7874
+ playerState.ItemType = 'audiobook'
7875
+ else:
7876
+ playerState.ItemType = 'podcast'
7877
+
7878
+ elif (spotifyType == 'track'):
7879
+
7880
+ # get the track data from spotify, as Sonos Soco data is incomplete.
7881
+ track:Track = self.GetTrack(spotifyId)
7882
+ if (track.Id is not None):
7883
+ playerState._Item = track
7884
+ if (playerState._Context is None):
7885
+ playerState._Context = Context()
7886
+ playerState._Context._Type = track.Album.Type
7887
+ playerState._Context._Uri = track.Album.Uri
7888
+ playerState._Context._Href = track.Album.Href
7889
+ playerState._Context._ExternalUrls = track.Album.ExternalUrls
7890
+ else:
7891
+ # if data could not be obtained from spotify, then use what's
7892
+ # available from Sonos Soco api metadata.
7893
+ playerState._CurrentlyPlayingType = 'track'
7894
+ playerState.ItemType = playerState._CurrentlyPlayingType
7895
+ playerState._Item = Track()
7896
+ playerState._Item._Name = sonosTrackInfo.get('title','')
7897
+ playerState._Item._Uri = spotifyUri
7898
+ playerState._Item._Description = 'Sonos device does not provide a description'
7899
+ sTimeValue:str = sonosTrackInfo.get('duration',None)
7900
+ playerState._Item._DurationMS = mediaPositionHMS_toSeconds(sTimeValue) * 1000 # convert h:mm:ss to milliseconds
7901
+ playerState._Item._Explicit = False
7902
+ playerState._Item._Href = 'https://api.spotify.com/v1/episodes/' + spotifyId
7903
+ playerState._Item._HtmlDescription = playerState._Item._Description
7904
+ playerState._Item._Id = spotifyId
7905
+ playerState._Item._TrackNumber = sonosTrackInfo.get('playlist_position','')
7906
+ playerState._Item._ReleaseDate = '0000'
7907
+ playerState._Item._ReleaseDatePrecision = 'year'
7908
+ playerState._Item._Type = 'track'
7909
+ playerState._Item._Album = Album()
7910
+ playerState._Item._Album._Name = sonosTrackInfo.get('album','')
7911
+ playerState._Item._Album.Images.append(ImageObject())
7912
+ playerState._Item._Album.Images[0]._Url = sonosTrackInfo.get('album_art','')
7913
+ playerState._Item.Artists.append(Artist())
7914
+ playerState._Item.Artists[0]._Name = sonosTrackInfo.get('artist','')
7915
+
7916
+ # set transport actions.
7917
+ currentTransportState:str = sonosTransportInfo.get('current_transport_state','')
7918
+ if currentTransportState == 'PLAYING':
7919
+ playerState._IsPlaying = True
7920
+ elif currentTransportState in ['PAUSED_PLAYBACK','STOPPED']:
7921
+ playerState.Actions._Pausing = True
7922
+
7923
+ # only update the following PlayState attributes from SoCo if they are NOT set.
7924
+ # these values are used to set the Sonos PlayMode value, which is the actual value that determines
7925
+ # what the Repeat and Shuffle settings are (ya, it's weird!). What is odd is that the Spotify
7926
+ # Web API PlayerState values for Shuffle and Repeat can be different than what the Sonos Controller
7927
+ # API (SoCo) report for the device! This is why we use the Spotify Web API PlayerState values.
7928
+ if playerState._ShuffleState is None:
7929
+ playerState._ShuffleState = sonosPlayer.shuffle
7930
+ if playerState._RepeatState is None:
7931
+ sonosRepeat = sonosPlayer.repeat
7932
+ if sonosRepeat == 'ONE':
7933
+ playerState._RepeatState = 'track'
7934
+ elif sonosRepeat == False:
7935
+ playerState._RepeatState = 'off'
7936
+ elif sonosRepeat == True:
7937
+ playerState._RepeatState = 'context'
7938
+
7939
+ # trace.
7940
+ _logsi.LogObject(SILevel.Verbose, "Current Spotify player PlayState (built from Sonos device current state) for device: %s" % (scDevice.Title), playerState, excludeNonPublic=True)
7941
+ return playerState
7942
+
7943
+ except SpotifyApiError: raise # pass handled exceptions on thru
7944
+ except Exception as ex:
7945
+
7946
+ # format unhandled exception.
7947
+ raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
7948
+
7949
+ finally:
7950
+
7951
+ # trace.
7952
+ _logsi.LeaveMethod(SILevel.Debug, apiMethodName)
7953
+
7954
+
7594
7955
  def GetPlayerQueueInfo(self) -> PlayerQueueInfo:
7595
7956
  """
7596
7957
  Get the list of objects that make up the user's playback queue.
@@ -9161,7 +9522,7 @@ class SpotifyClient:
9161
9522
  def GetSpotifyConnectDevice(
9162
9523
  self,
9163
9524
  deviceValue:str,
9164
- verifyUserContext:bool=True, # DEPRECATED
9525
+ verifyUserContext:bool=True,
9165
9526
  verifyTimeout:float=5.0,
9166
9527
  refreshDeviceList:bool=False,
9167
9528
  activateDevice:bool=True,
@@ -9207,7 +9568,7 @@ class SpotifyClient:
9207
9568
  Returns:
9208
9569
  A `SpotifyConnectDevice` object for the deviceValue if one could be resolved.
9209
9570
  otherwise, a null value with the understanding that subsequent operations will probably
9210
- fail since it's not in the Spotify Connect discovery list.
9571
+ fail since it's not in the Spotify Connect device list.
9211
9572
 
9212
9573
  Raises:
9213
9574
  SpotifyWebApiError:
@@ -9244,13 +9605,14 @@ class SpotifyClient:
9244
9605
  apiMethodName:str = 'GetSpotifyConnectDevice'
9245
9606
  apiMethodParms:SIMethodParmListContext = None
9246
9607
  scDevice:SpotifyConnectDevice = None
9608
+ sonosPlayer:SoCo = None
9247
9609
 
9248
9610
  try:
9249
9611
 
9250
9612
  # trace.
9251
9613
  apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
9252
9614
  apiMethodParms.AppendKeyValue("deviceValue", deviceValue)
9253
- apiMethodParms.AppendKeyValue("verifyUserContext", verifyUserContext) # DEPRECATED
9615
+ apiMethodParms.AppendKeyValue("verifyUserContext (DEPRECATED)", verifyUserContext)
9254
9616
  apiMethodParms.AppendKeyValue("verifyTimeout", verifyTimeout)
9255
9617
  apiMethodParms.AppendKeyValue("refreshDeviceList", refreshDeviceList)
9256
9618
  apiMethodParms.AppendKeyValue("activateDevice", activateDevice)
@@ -9266,8 +9628,15 @@ class SpotifyClient:
9266
9628
  refreshDeviceList = False
9267
9629
 
9268
9630
  # resolve the device id from the specified device value.
9269
- # an exception will be raised if the device could not be resolved!
9270
- scDevice = self._SpotifyConnectDirectory.GetDevice(deviceValue, refreshDynamicDevices=True)
9631
+ # returned value will be null if the device could not be resolved!
9632
+ scDevice = self._SpotifyConnectDirectory.GetDevice(deviceValue, refreshDynamicDevices=True, raiseExceptionIfNotFound=False)
9633
+
9634
+ # if device could not be resolved, then go no further as it will result in a
9635
+ # "device not found" error from the Spotify Web API anyway!
9636
+ if (scDevice is None):
9637
+ _logsi.LogVerbose("Spotify Player device was not found, and there is no active Spotify player; no need to re-activate")
9638
+ return None
9639
+
9271
9640
  discovery:ZeroconfDiscoveryResult = scDevice.DiscoveryResult
9272
9641
  info:ZeroconfGetInfo = scDevice.DeviceInfo
9273
9642
  scDevice.WasReConnected = False
@@ -9277,32 +9646,17 @@ class SpotifyClient:
9277
9646
  _logsi.LogVerbose("Spotify Connect device %s is already active; no need to re-activate" % (scDevice.Title))
9278
9647
  return scDevice
9279
9648
 
9280
- # is the device in the player device list? if so, then it's already to be selected. note that
9281
- # if another user is controlling the device, then it will not be in the active device list
9282
- # for the current user.
9649
+ # is the device in the player device list? if so, then it's ready to be selected.
9650
+ # note that if another user is controlling the device, then it will not be in the
9651
+ # active device list for the current user.
9283
9652
  if (scDevice.IsInDeviceList):
9284
9653
  _logsi.LogVerbose("Spotify Connect device %s is already in the available device list; no need to re-activate" % (scDevice.Title))
9285
9654
  return scDevice
9286
9655
 
9287
- # chromecast devices always require activation if they are not the active device.
9288
- # note that `transferPlayback=false`, as transfer should be done after we return to caller.
9289
- if (scDevice.IsChromeCast):
9290
-
9291
- # activate the spotify cast application on the device.
9292
- _logsi.LogVerbose("Activating Chromecast Spotify Connect device: %s" % (scDevice.Title))
9293
- self._SpotifyConnectDirectory.ActivateCastAppSpotify(scDevice.Name, transferPlayback=False)
9294
-
9295
- # re-fetch device instance, as it has updated Zeroconf Response data.
9296
- # do not need to refresh from Spotify Web API, as only zeroconf data was changed.
9297
- scDevice = self._SpotifyConnectDirectory.GetDevice(deviceValue, refreshDynamicDevices=False)
9298
-
9299
- # return device to caller.
9300
- return scDevice
9301
-
9302
9656
  # if this is a Sonos device and there is no Spotify Client Application token then don't bother.
9303
9657
  # in this case, we will exit and let the calling method switch to the Sonos device and play
9304
9658
  # using the Sonos local queue.
9305
- if (info.IsBrandSonos):
9659
+ if (scDevice.IsSonos):
9306
9660
 
9307
9661
  # check if the Spotify Desktop Application Client oauth2 token is defined.
9308
9662
  hasToken:bool = AuthClient.HasTokenForKey(
@@ -9318,13 +9672,32 @@ class SpotifyClient:
9318
9672
  _logsi.LogVerbose("Spotify Desktop Application Client oauth2 token was not found for Spotify LoginId: '%s'" % (self._SpotifyConnectLoginId))
9319
9673
  return scDevice
9320
9674
 
9321
- # at this point, we will try to activate the device.
9322
-
9675
+ # get Sonos Controller instance for the device.
9676
+ _logsi.LogVerbose("Target device is Sonos; getting Sonos Controller instance for device %s" % (scDevice.Title))
9677
+ sonosPlayer = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
9678
+
9323
9679
  # was device activation requested? if not, then we are done.
9324
9680
  if (not activateDevice):
9325
9681
  _logsi.LogVerbose("Activation not requested for Spotify Connect device: %s" % (scDevice.Title))
9326
9682
  return scDevice
9327
9683
 
9684
+ # at this point, we will try to activate the device if needed.
9685
+
9686
+ # chromecast devices always require activation if they are not the active device.
9687
+ # note that `transferPlayback=false`, as transfer should be done after we return to caller.
9688
+ if (scDevice.IsChromeCast):
9689
+
9690
+ # activate the spotify cast application on the device.
9691
+ _logsi.LogVerbose("Activating Chromecast Spotify Connect device: %s" % (scDevice.Title))
9692
+ self._SpotifyConnectDirectory.ActivateCastAppSpotify(scDevice.Name, transferPlayback=False)
9693
+
9694
+ # re-fetch device instance, as it has updated Zeroconf Response data.
9695
+ # do not need to refresh from Spotify Web API, as only zeroconf data was changed.
9696
+ scDevice = self._SpotifyConnectDirectory.GetDevice(deviceValue, refreshDynamicDevices=False)
9697
+
9698
+ # return device to caller.
9699
+ return scDevice
9700
+
9328
9701
  zcfResult:ZeroconfResponse
9329
9702
 
9330
9703
  # store the currently active user of the device, in case we need to switch users later on.
@@ -9342,87 +9715,20 @@ class SpotifyClient:
9342
9715
  tokenStorageDir=self.TokenStorageDir,
9343
9716
  tokenStorageFile=self.TokenStorageFile)
9344
9717
 
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
-
9718
+ # is this a Sonos device?
9719
+ if (scDevice.IsSonos):
9720
+
9721
+ # trace.
9722
+ sonosMusicSource:str = sonosPlayer.music_source
9723
+ _logsi.LogVerbose("Sonos device %s music source before Connect: \"%s\"" % (scDevice.Title, sonosMusicSource))
9724
+
9725
+ # was the Sonos device music source set to SPOTIFY_CONNECT?
9726
+ # if not, then issue a Disconnect to (hopefully) reset the music source. this should allow
9727
+ # the subsequent Connect to re-establish a SPOTIFY_CONNECT music source on the Sonos device.
9728
+ if sonosMusicSource != "SPOTIFY_CONNECT":
9729
+ _logsi.LogVerbose("Issuing Disconnect to Sonos Spotify Connect device %s" % (scDevice.Title))
9730
+ zcfResult = zconn.Disconnect(ignoreStatusResult=True)
9386
9731
 
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
9732
  # connect the device to OUR Spotify Connect user context.
9427
9733
  # note that the result here only indicates that the connect was submitted - NOT that it was successful!
9428
9734
  _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 +9740,26 @@ class SpotifyClient:
9434
9740
  # trace.
9435
9741
  _logsi.LogVerbose("User context switched from \"%s\" to \"%s\" for Spotify Connect device %s" % (oldActiveUser, self._SpotifyConnectLoginId, scDevice.Title))
9436
9742
 
9743
+ # is this a Sonos device?
9744
+ if (scDevice.IsSonos):
9745
+
9746
+ # trace.
9747
+ sonosMusicSource:str = sonosPlayer.music_source
9748
+ _logsi.LogVerbose("Sonos device %s music source after Connect: \"%s\"" % (scDevice.Title, sonosMusicSource))
9749
+
9750
+ # was the Sonos device music source set to Spotify Connect?
9751
+ # if not, then it's a lost cause at this point since it's probably in an UNKNOWN state.
9752
+ if sonosMusicSource != "SPOTIFY_CONNECT":
9753
+ _logsi.LogVerbose("Sonos device %s music source after Connect is not SPOTIFY_CONNECT; it will probably fail to play" % (scDevice.Title))
9754
+
9755
+ # at this point the Sonos music source should be set to SPOTIFY_CONNECT, and control
9756
+ # transferred to the device. note that the Sonos device will still NOT appear in the
9757
+ # player device list, but SHOULD be the active (restricted) device in player state.
9758
+
9759
+ # wait for the device to enter the Spotify Web API player device list, or
9760
+ # to become the active device.
9761
+ # Sonos devices are famous for not showing up in the device list, but DO
9762
+ # appear as the active (restricted) device!
9437
9763
  loopTotalDelay:float = 0
9438
9764
  LOOP_DELAY:float = 0.350
9439
9765
  while True:
@@ -9466,7 +9792,7 @@ class SpotifyClient:
9466
9792
  # only check so many times before we give up;
9467
9793
  # we will keep checking until we find the device, or we exceed the verifyTimeout value.
9468
9794
  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))
9795
+ _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
9796
  break
9471
9797
 
9472
9798
  # return the device instance.
@@ -9544,7 +9870,7 @@ class SpotifyClient:
9544
9870
  # trace.
9545
9871
  apiMethodParms = _logsi.EnterMethodParmList(SILevel.Debug, apiMethodName)
9546
9872
  apiMethodParms.AppendKeyValue("refresh", refresh)
9547
- apiMethodParms.AppendKeyValue("sortResult", sortResult) # DEPRECATED
9873
+ apiMethodParms.AppendKeyValue("sortResult (DEPRECATED)", sortResult)
9548
9874
  _logsi.LogMethodParmList(SILevel.Verbose, "Get available Spotify Connect devices", apiMethodParms)
9549
9875
 
9550
9876
  # validations.
@@ -11171,11 +11497,14 @@ class SpotifyClient:
11171
11497
  This method requires the `user-modify-playback-state` scope.
11172
11498
 
11173
11499
  Args:
11174
- deviceId (str):
11175
- The id or name of the device this command is targeting.
11176
- If not supplied, the user's currently active device is the target.
11177
- Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
11178
- Example: `Web Player (Chrome)`
11500
+ deviceId (str | SpotifyConnectDevice) | None):
11501
+ The target player device identifier.
11502
+ This could be an id, name, a default device indicator (e.g. "*"), a
11503
+ SpotifyConnectDevice object, or null to utilize the active player device.
11504
+ A device is considered resolved if a SpotifyConnectDevice object is passed
11505
+ for this argument. An exception will be raised if the argument value could
11506
+ not be resolved or activated.
11507
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
11179
11508
  delay (float):
11180
11509
  Time delay (in seconds) to wait AFTER issuing the command to the player.
11181
11510
  This delay will give the spotify web api time to process the change before
@@ -11215,17 +11544,27 @@ class SpotifyClient:
11215
11544
  delay = validateDelay(delay, 0.50, 10)
11216
11545
 
11217
11546
  # resolve / activate the device object if needed.
11218
- scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
11547
+ scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId, False)
11219
11548
 
11220
- # build spotify web api request parameters.
11221
- urlParms:dict = {}
11222
- urlParms['device_id'] = scDevice.Id
11549
+ # is this an active Sonos device?
11550
+ if (scDevice is not None) and (scDevice.IsSonos):
11223
11551
 
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)
11552
+ _logsi.LogVerbose("Issuing command to Sonos device %s: PAUSE" % (scDevice.Title))
11553
+ sonosPlayer:SoCo = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
11554
+ sonosPlayer.pause()
11555
+
11556
+ else:
11557
+
11558
+ # build spotify web api request parameters.
11559
+ urlParms:dict = {}
11560
+ if (scDevice is not None):
11561
+ urlParms['device_id'] = scDevice.Id
11562
+
11563
+ # execute spotify web api request.
11564
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/pause')
11565
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
11566
+ msg.UrlParameters = urlParms
11567
+ self.MakeRequest('PUT', msg)
11229
11568
 
11230
11569
  # give spotify web api time to process the change.
11231
11570
  if delay > 0:
@@ -11288,11 +11627,14 @@ class SpotifyClient:
11288
11627
  player to start playing the next track.
11289
11628
  Default is `0`.
11290
11629
  Example: `25000`
11291
- deviceId (str):
11292
- The id or name of the device this command is targeting.
11293
- If not supplied, the user's currently active device is the target.
11294
- Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
11295
- Example: `Web Player (Chrome)`
11630
+ deviceId (str | SpotifyConnectDevice) | None):
11631
+ The target player device identifier.
11632
+ This could be an id, name, a default device indicator (e.g. "*"), a
11633
+ SpotifyConnectDevice object, or null to utilize the active player device.
11634
+ A device is considered resolved if a SpotifyConnectDevice object is passed
11635
+ for this argument. An exception will be raised if the argument value could
11636
+ not be resolved or activated.
11637
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
11296
11638
  delay (float):
11297
11639
  Time delay (in seconds) to wait AFTER issuing the command to the player.
11298
11640
  This delay will give the spotify web api time to process the change before
@@ -11357,7 +11699,7 @@ class SpotifyClient:
11357
11699
  delay = validateDelay(delay, 0.50, 10)
11358
11700
 
11359
11701
  # resolve / activate the device object if needed.
11360
- scDevice = self._ResolveDeviceObject(deviceId)
11702
+ scDevice = self._ResolveDeviceObject(deviceId, False)
11361
11703
 
11362
11704
  # build spotify web api request parameters.
11363
11705
  reqData:dict = \
@@ -11371,7 +11713,9 @@ class SpotifyClient:
11371
11713
  reqData['offset'] = { 'position': offsetPosition }
11372
11714
 
11373
11715
  # formulate url parms; will manually add them since we are adding json body as well.
11374
- urlParms:str = '?device_id=%s' % scDevice.Id
11716
+ urlParms:str = ''
11717
+ if (scDevice is not None):
11718
+ urlParms = '?device_id=%s' % scDevice.Id
11375
11719
 
11376
11720
  # execute spotify web api request.
11377
11721
  msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/play{url_parms}'.format(url_parms=urlParms))
@@ -11415,11 +11759,14 @@ class SpotifyClient:
11415
11759
  and starts playing them.
11416
11760
 
11417
11761
  Args:
11418
- deviceId (str):
11419
- The id or name of the device this command is targeting.
11420
- If not supplied, the user's currently active device is the target.
11421
- Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
11422
- Example: `Web Player (Chrome)`
11762
+ deviceId (str | SpotifyConnectDevice) | None):
11763
+ The target player device identifier.
11764
+ This could be an id, name, a default device indicator (e.g. "*"), a
11765
+ SpotifyConnectDevice object, or null to utilize the active player device.
11766
+ A device is considered resolved if a SpotifyConnectDevice object is passed
11767
+ for this argument. An exception will be raised if the argument value could
11768
+ not be resolved or activated.
11769
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
11423
11770
  shuffle (bool):
11424
11771
  True to set player shuffle mode to on; otherwise, False for no shuffle.
11425
11772
  delay (float):
@@ -11497,11 +11844,11 @@ class SpotifyClient:
11497
11844
  arrUris.append(trackSaved.Track.Uri)
11498
11845
 
11499
11846
  # resolve / activate the device object if needed.
11500
- scDevice = self._ResolveDeviceObject(deviceId)
11847
+ scDevice = self._ResolveDeviceObject(deviceId, False)
11501
11848
 
11502
11849
  # set desired shuffle mode.
11503
11850
  if (shuffle == True):
11504
- self.PlayerSetShuffleMode(shuffle, scDevice.Id, delay)
11851
+ self.PlayerSetShuffleMode(shuffle, scDevice, delay)
11505
11852
 
11506
11853
  # play the tracks.
11507
11854
  # indicate device id has already been resolved.
@@ -11551,11 +11898,14 @@ class SpotifyClient:
11551
11898
  player to start playing the next track.
11552
11899
  Default is `0`.
11553
11900
  Example: `25000`
11554
- deviceId (str):
11555
- The id or name of the device this command is targeting.
11556
- If not supplied, the user's currently active device is the target.
11557
- Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
11558
- Example: `Web Player (Chrome)`
11901
+ deviceId (str | SpotifyConnectDevice) | None):
11902
+ The target player device identifier.
11903
+ This could be an id, name, a default device indicator (e.g. "*"), a
11904
+ SpotifyConnectDevice object, or null to utilize the active player device.
11905
+ A device is considered resolved if a SpotifyConnectDevice object is passed
11906
+ for this argument. An exception will be raised if the argument value could
11907
+ not be resolved or activated.
11908
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
11559
11909
  delay (float):
11560
11910
  Time delay (in seconds) to wait AFTER issuing the command to the player.
11561
11911
  This delay will give the spotify web api time to process the change before
@@ -11614,7 +11964,7 @@ class SpotifyClient:
11614
11964
  arrUris[idx] = arrUris[idx].strip()
11615
11965
 
11616
11966
  # resolve / activate the device object if needed.
11617
- scDevice = self._ResolveDeviceObject(deviceId)
11967
+ scDevice = self._ResolveDeviceObject(deviceId, False)
11618
11968
 
11619
11969
  # left this here in case we want to implement it in the future.
11620
11970
 
@@ -11644,7 +11994,9 @@ class SpotifyClient:
11644
11994
  reqData['uris'] = arrUris
11645
11995
 
11646
11996
  # formulate url parms; will manually add them since we are adding json body as well.
11647
- urlParms:str = '?device_id=%s' % scDevice.Id
11997
+ urlParms:str = ''
11998
+ if (scDevice is not None):
11999
+ urlParms = '?device_id=%s' % scDevice.Id
11648
12000
 
11649
12001
  # execute spotify web api request.
11650
12002
  msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/play{url_parms}'.format(url_parms=urlParms))
@@ -11686,11 +12038,14 @@ class SpotifyClient:
11686
12038
  This method requires the `user-modify-playback-state` scope.
11687
12039
 
11688
12040
  Args:
11689
- deviceId (str):
11690
- The id or name of the device this command is targeting.
11691
- If not supplied, the user's currently active device is the target.
11692
- Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
11693
- Example: `Web Player (Chrome)`
12041
+ deviceId (str | SpotifyConnectDevice) | None):
12042
+ The target player device identifier.
12043
+ This could be an id, name, a default device indicator (e.g. "*"), a
12044
+ SpotifyConnectDevice object, or null to utilize the active player device.
12045
+ A device is considered resolved if a SpotifyConnectDevice object is passed
12046
+ for this argument. An exception will be raised if the argument value could
12047
+ not be resolved or activated.
12048
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
11694
12049
  delay (float):
11695
12050
  Time delay (in seconds) to wait AFTER issuing the command to the player.
11696
12051
  This delay will give the spotify web api time to process the change before
@@ -11707,7 +12062,7 @@ class SpotifyClient:
11707
12062
  This API only works for users who have Spotify Premium.
11708
12063
 
11709
12064
  The order of execution is not guaranteed when you use this API with other Player API endpoints.
11710
-
12065
+
11711
12066
  <details>
11712
12067
  <summary>Sample Code</summary>
11713
12068
  ```python
@@ -11730,17 +12085,27 @@ class SpotifyClient:
11730
12085
  delay = validateDelay(delay, 0.50, 10)
11731
12086
 
11732
12087
  # resolve / activate the device object if needed.
11733
- scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
12088
+ scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId, False)
11734
12089
 
11735
- # build spotify web api request parameters.
11736
- urlParms:dict = {}
11737
- urlParms['device_id'] = scDevice.Id
12090
+ # is this an active Sonos device?
12091
+ if (scDevice is not None) and (scDevice.IsSonos):
11738
12092
 
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)
12093
+ _logsi.LogVerbose("Issuing command to Sonos device %s: PLAY (RESUME)" % (scDevice.Title))
12094
+ sonosPlayer:SoCo = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
12095
+ sonosPlayer.play()
12096
+
12097
+ else:
12098
+
12099
+ # build spotify web api request parameters.
12100
+ urlParms:dict = {}
12101
+ if (scDevice is not None):
12102
+ urlParms['device_id'] = scDevice.Id
12103
+
12104
+ # execute spotify web api request.
12105
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/play')
12106
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12107
+ msg.UrlParameters = urlParms
12108
+ self.MakeRequest('PUT', msg)
11744
12109
 
11745
12110
  # give spotify web api time to process the change.
11746
12111
  if delay > 0:
@@ -11785,11 +12150,14 @@ class SpotifyClient:
11785
12150
  Passing in a position that is greater than the length of the track will cause the
11786
12151
  player to start playing the next song.
11787
12152
  Example = `25000` to start playing at the 25 second mark.
11788
- deviceId (str):
11789
- The id or name of the device this command is targeting.
11790
- If not supplied, the user's currently active device is the target.
11791
- Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
11792
- Example: `Web Player (Chrome)`
12153
+ deviceId (str | SpotifyConnectDevice) | None):
12154
+ The target player device identifier.
12155
+ This could be an id, name, a default device indicator (e.g. "*"), a
12156
+ SpotifyConnectDevice object, or null to utilize the active player device.
12157
+ A device is considered resolved if a SpotifyConnectDevice object is passed
12158
+ for this argument. An exception will be raised if the argument value could
12159
+ not be resolved or activated.
12160
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
11793
12161
  delay (float):
11794
12162
  Time delay (in seconds) to wait AFTER issuing the command to the player.
11795
12163
  This delay will give the spotify web api time to process the change before
@@ -11855,13 +12223,13 @@ class SpotifyClient:
11855
12223
  relativePositionMS = 0
11856
12224
 
11857
12225
  # resolve / activate the device object if needed.
11858
- scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
12226
+ scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId, False)
11859
12227
 
11860
12228
  # was relative seeking specified?
11861
- if (relativePositionMS != 0) and ((positionMS is None) or (positionMS <= 0)):
12229
+ if (scDevice is not None) and (relativePositionMS != 0) and ((positionMS is None) or (positionMS <= 0)):
11862
12230
 
11863
- # get current track position.
11864
- playState:PlayerPlayState = self.GetPlayerPlaybackState()
12231
+ # get current track position (from cache, since we called _ResolveDeviceObject method above).
12232
+ playState:PlayerPlayState = self.GetDevicePlaybackState(scDevice)
11865
12233
  newPositionMS:int = playState.ProgressMS
11866
12234
  if (newPositionMS is not None) and (newPositionMS > 0):
11867
12235
 
@@ -11871,18 +12239,29 @@ class SpotifyClient:
11871
12239
  newPositionMS = 0
11872
12240
  positionMS = newPositionMS
11873
12241
 
11874
- # build spotify web api request parameters.
11875
- urlParms:dict = \
11876
- {
11877
- 'position_ms': positionMS
11878
- }
11879
- urlParms['device_id'] = scDevice.Id
12242
+ # is this an active Sonos device?
12243
+ if (scDevice is not None) and (scDevice.IsSonos):
11880
12244
 
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)
12245
+ sonosPosition:str = mediaPositionHMS_fromSeconds(positionMS / 1000) # convert from milliseconds to Sonos H:MM:SS format
12246
+ _logsi.LogVerbose("Issuing command to Sonos device %s: SEEK (sonosPosition=%s)" % (scDevice.Title, sonosPosition))
12247
+ sonosPlayer:SoCo = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
12248
+ sonosPlayer.seek(str(sonosPosition))
12249
+
12250
+ else:
12251
+
12252
+ # build spotify web api request parameters.
12253
+ urlParms:dict = \
12254
+ {
12255
+ 'position_ms': positionMS
12256
+ }
12257
+ if (scDevice is not None):
12258
+ urlParms['device_id'] = scDevice.Id
12259
+
12260
+ # execute spotify web api request.
12261
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/seek')
12262
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12263
+ msg.UrlParameters = urlParms
12264
+ self.MakeRequest('PUT', msg)
11886
12265
 
11887
12266
  # give spotify web api time to process the change.
11888
12267
  if delay > 0:
@@ -11918,11 +12297,14 @@ class SpotifyClient:
11918
12297
  This method requires the `user-modify-playback-state` scope.
11919
12298
 
11920
12299
  Args:
11921
- deviceId (str):
11922
- The id or name of the device this command is targeting.
11923
- If not supplied, the user's currently active device is the target.
11924
- Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
11925
- Example: `Web Player (Chrome)`
12300
+ deviceId (str | SpotifyConnectDevice) | None):
12301
+ The target player device identifier.
12302
+ This could be an id, name, a default device indicator (e.g. "*"), a
12303
+ SpotifyConnectDevice object, or null to utilize the active player device.
12304
+ A device is considered resolved if a SpotifyConnectDevice object is passed
12305
+ for this argument. An exception will be raised if the argument value could
12306
+ not be resolved or activated.
12307
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
11926
12308
  delay (float):
11927
12309
  Time delay (in seconds) to wait AFTER issuing the command to the player.
11928
12310
  This delay will give the spotify web api time to process the change before
@@ -11962,17 +12344,27 @@ class SpotifyClient:
11962
12344
  delay = validateDelay(delay, 0.50, 10)
11963
12345
 
11964
12346
  # resolve / activate the device object if needed.
11965
- scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
12347
+ scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId, False)
11966
12348
 
11967
- # build spotify web api request parameters.
11968
- urlParms:dict = {}
11969
- urlParms['device_id'] = scDevice.Id
12349
+ # is this an active Sonos device?
12350
+ if (scDevice is not None) and (scDevice.IsSonos):
11970
12351
 
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)
12352
+ _logsi.LogVerbose("Issuing command to Sonos device %s: NEXT" % (scDevice.Title))
12353
+ sonosPlayer:SoCo = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
12354
+ sonosPlayer.next()
12355
+
12356
+ else:
12357
+
12358
+ # build spotify web api request parameters.
12359
+ urlParms:dict = {}
12360
+ if (scDevice is not None):
12361
+ urlParms['device_id'] = scDevice.Id
12362
+
12363
+ # execute spotify web api request.
12364
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/next')
12365
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12366
+ msg.UrlParameters = urlParms
12367
+ self.MakeRequest('POST', msg)
11976
12368
 
11977
12369
  # give spotify web api time to process the change.
11978
12370
  if delay > 0:
@@ -12008,11 +12400,14 @@ class SpotifyClient:
12008
12400
  This method requires the `user-modify-playback-state` scope.
12009
12401
 
12010
12402
  Args:
12011
- deviceId (str):
12012
- The id or name of the device this command is targeting.
12013
- If not supplied, the user's currently active device is the target.
12014
- Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
12015
- Example: `Web Player (Chrome)`
12403
+ deviceId (str | SpotifyConnectDevice) | None):
12404
+ The target player device identifier.
12405
+ This could be an id, name, a default device indicator (e.g. "*"), a
12406
+ SpotifyConnectDevice object, or null to utilize the active player device.
12407
+ A device is considered resolved if a SpotifyConnectDevice object is passed
12408
+ for this argument. An exception will be raised if the argument value could
12409
+ not be resolved or activated.
12410
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
12016
12411
  delay (float):
12017
12412
  Time delay (in seconds) to wait AFTER issuing the command to the player.
12018
12413
  This delay will give the spotify web api time to process the change before
@@ -12052,17 +12447,27 @@ class SpotifyClient:
12052
12447
  delay = validateDelay(delay, 0.50, 10)
12053
12448
 
12054
12449
  # resolve / activate the device object if needed.
12055
- scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
12450
+ scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId, False)
12056
12451
 
12057
- # build spotify web api request parameters.
12058
- urlParms:dict = {}
12059
- urlParms['device_id'] = scDevice.Id
12452
+ # is this an active Sonos device?
12453
+ if (scDevice is not None) and (scDevice.IsSonos):
12060
12454
 
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)
12455
+ _logsi.LogVerbose("Issuing command to Sonos device %s: PREVIOUS" % (scDevice.Title))
12456
+ sonosPlayer:SoCo = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
12457
+ sonosPlayer.previous()
12458
+
12459
+ else:
12460
+
12461
+ # build spotify web api request parameters.
12462
+ urlParms:dict = {}
12463
+ if (scDevice is not None):
12464
+ urlParms['device_id'] = scDevice.Id
12465
+
12466
+ # execute spotify web api request.
12467
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/previous')
12468
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12469
+ msg.UrlParameters = urlParms
12470
+ self.MakeRequest('POST', msg)
12066
12471
 
12067
12472
  # give spotify web api time to process the change.
12068
12473
  if delay > 0:
@@ -12105,11 +12510,14 @@ class SpotifyClient:
12105
12510
  - `context` - will repeat the current context.
12106
12511
  - `off` - will turn repeat off.
12107
12512
  Default: `off`
12108
- deviceId (str):
12109
- The id or name of the device this command is targeting.
12110
- If not supplied, the user's currently active device is the target.
12111
- Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
12112
- Example: `Web Player (Chrome)`
12513
+ deviceId (str | SpotifyConnectDevice) | None):
12514
+ The target player device identifier.
12515
+ This could be an id, name, a default device indicator (e.g. "*"), a
12516
+ SpotifyConnectDevice object, or null to utilize the active player device.
12517
+ A device is considered resolved if a SpotifyConnectDevice object is passed
12518
+ for this argument. An exception will be raised if the argument value could
12519
+ not be resolved or activated.
12520
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
12113
12521
  delay (float):
12114
12522
  Time delay (in seconds) to wait AFTER issuing the command to the player.
12115
12523
  This delay will give the spotify web api time to process the change before
@@ -12150,20 +12558,47 @@ class SpotifyClient:
12150
12558
  delay = validateDelay(delay, 0.50, 10)
12151
12559
 
12152
12560
  # resolve / activate the device object if needed.
12153
- scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
12561
+ scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId, False)
12562
+
12563
+ # is this an active Sonos device?
12564
+ if (scDevice is not None) and (scDevice.IsSonos):
12565
+
12566
+ # get current Spotify Connect player state.
12567
+ playerState = self.GetPlayerPlaybackStateSonos(scDevice)
12568
+
12569
+ # set the Sonos PlayMode directly (instead of setting the Sonos repeat property)!
12570
+ # The Spotify Web API PlayerState `ShuffleState` value can sometimes be different than what the
12571
+ # Sonos device reports for its `shuffle` property value!
12572
+ if state == 'track':
12573
+ sonos_repeat = 'ONE'
12574
+ elif state == 'off':
12575
+ sonos_repeat = False
12576
+ elif state == 'context':
12577
+ sonos_repeat = True
12578
+ else:
12579
+ sonos_repeat = False
12580
+ playMode:str = SONOS_PLAY_MODE_BY_MEANING[(playerState.ShuffleState, sonos_repeat)]
12581
+
12582
+ # execute SoCo api request.
12583
+ _logsi.LogVerbose("Issuing command to Sonos device %s: REPEAT (playmode=%s)" % (scDevice.Title, playMode))
12584
+ sonosPlayer:SoCo = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
12585
+ sonosPlayer.play_mode = playMode
12154
12586
 
12155
- # build spotify web api request parameters.
12156
- urlParms:dict = \
12157
- {
12158
- 'state': state
12159
- }
12160
- urlParms['device_id'] = scDevice.Id
12587
+ else:
12161
12588
 
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)
12589
+ # build spotify web api request parameters.
12590
+ urlParms:dict = \
12591
+ {
12592
+ 'state': state
12593
+ }
12594
+ if (scDevice is not None):
12595
+ urlParms['device_id'] = scDevice.Id
12596
+
12597
+ # execute spotify web api request.
12598
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/repeat')
12599
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12600
+ msg.UrlParameters = urlParms
12601
+ self.MakeRequest('PUT', msg)
12167
12602
 
12168
12603
  # give spotify web api time to process the change.
12169
12604
  if delay > 0:
@@ -12205,11 +12640,14 @@ class SpotifyClient:
12205
12640
  - `True` - Shuffle user's playback.
12206
12641
  - `False` - Do not shuffle user's playback.
12207
12642
  Default is False
12208
- deviceId (str):
12209
- The id or name of the device this command is targeting.
12210
- If not supplied, the user's currently active device is the target.
12211
- Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
12212
- Example: `Web Player (Chrome)`
12643
+ deviceId (str | SpotifyConnectDevice) | None):
12644
+ The target player device identifier.
12645
+ This could be an id, name, a default device indicator (e.g. "*"), a
12646
+ SpotifyConnectDevice object, or null to utilize the active player device.
12647
+ A device is considered resolved if a SpotifyConnectDevice object is passed
12648
+ for this argument. An exception will be raised if the argument value could
12649
+ not be resolved or activated.
12650
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
12213
12651
  delay (float):
12214
12652
  Time delay (in seconds) to wait AFTER issuing the command to the player.
12215
12653
  This delay will give the spotify web api time to process the change before
@@ -12250,20 +12688,48 @@ class SpotifyClient:
12250
12688
  delay = validateDelay(delay, 0.50, 10)
12251
12689
 
12252
12690
  # resolve / activate the device object if needed.
12253
- scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
12691
+ scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId, False)
12692
+
12693
+ # is this an active Sonos device?
12694
+ if (scDevice is not None) and (scDevice.IsSonos):
12695
+
12696
+ # get current Spotify Connect player state.
12697
+ playerState = self.GetPlayerPlaybackStateSonos(scDevice)
12698
+
12699
+ # set the Sonos PlayMode directly (instead of setting the Sonos `shuffle` property)!
12700
+ # the Spotify Web API PlayerState `RepeatState` value can sometimes be different than what the
12701
+ # Sonos device reports for its `repeat` property value!
12702
+ sonos_repeat:str
12703
+ if playerState.RepeatState == 'track':
12704
+ sonos_repeat = 'ONE'
12705
+ elif playerState.RepeatState == 'off':
12706
+ sonos_repeat = False
12707
+ elif playerState.RepeatState == 'context':
12708
+ sonos_repeat = True
12709
+ else: # assume off if nothing else.
12710
+ sonos_repeat = False
12711
+ playMode:str = SONOS_PLAY_MODE_BY_MEANING[(state, sonos_repeat)]
12712
+
12713
+ # execute SoCo api request.
12714
+ _logsi.LogVerbose("Issuing command to Sonos device %s: SHUFFLE (playmode=%s)" % (scDevice.Title, playMode))
12715
+ sonosPlayer:SoCo = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
12716
+ sonosPlayer.play_mode = playMode
12254
12717
 
12255
- # build spotify web api request parameters.
12256
- urlParms:dict = \
12257
- {
12258
- 'state': state
12259
- }
12260
- urlParms['device_id'] = scDevice.Id
12718
+ else:
12261
12719
 
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)
12720
+ # build spotify web api request parameters.
12721
+ urlParms:dict = \
12722
+ {
12723
+ 'state': state
12724
+ }
12725
+ if (scDevice is not None):
12726
+ urlParms['device_id'] = scDevice.Id
12727
+
12728
+ # execute spotify web api request.
12729
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/shuffle')
12730
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12731
+ msg.UrlParameters = urlParms
12732
+ self.MakeRequest('PUT', msg)
12267
12733
 
12268
12734
  # give spotify web api time to process the change.
12269
12735
  if delay > 0:
@@ -12304,11 +12770,14 @@ class SpotifyClient:
12304
12770
  The volume to set.
12305
12771
  Must be a value from 0 to 100 inclusive.
12306
12772
  Example: `50`
12307
- deviceId (str):
12308
- The id or name of the device this command is targeting.
12309
- If not supplied, the user's currently active device is the target.
12310
- Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
12311
- Example: `Web Player (Chrome)`
12773
+ deviceId (str | SpotifyConnectDevice) | None):
12774
+ The target player device identifier.
12775
+ This could be an id, name, a default device indicator (e.g. "*"), a
12776
+ SpotifyConnectDevice object, or null to utilize the active player device.
12777
+ A device is considered resolved if a SpotifyConnectDevice object is passed
12778
+ for this argument. An exception will be raised if the argument value could
12779
+ not be resolved or activated.
12780
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
12312
12781
  delay (float):
12313
12782
  Time delay (in seconds) to wait AFTER issuing the command to the player.
12314
12783
  This delay will give the spotify web api time to process the change before
@@ -12349,20 +12818,30 @@ class SpotifyClient:
12349
12818
  delay = validateDelay(delay, 0.50, 10)
12350
12819
 
12351
12820
  # resolve / activate the device object if needed.
12352
- scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId)
12821
+ scDevice:SpotifyConnectDevice = self._ResolveDeviceObject(deviceId, False)
12353
12822
 
12354
- # build spotify web api request parameters.
12355
- urlParms:dict = \
12356
- {
12357
- 'volume_percent': volumePercent
12358
- }
12359
- urlParms['device_id'] = scDevice.Id
12823
+ # is this an active Sonos device?
12824
+ if (scDevice is not None) and (scDevice.IsSonos):
12360
12825
 
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)
12826
+ _logsi.LogVerbose("Issuing command to Sonos device %s: VOLUME = %s" % (scDevice.Title, volumePercent))
12827
+ sonosPlayer:SoCo = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
12828
+ sonosPlayer.volume = volumePercent
12829
+
12830
+ else:
12831
+
12832
+ # build spotify web api request parameters.
12833
+ urlParms:dict = \
12834
+ {
12835
+ 'volume_percent': volumePercent
12836
+ }
12837
+ if (scDevice is not None):
12838
+ urlParms['device_id'] = scDevice.Id
12839
+
12840
+ # execute spotify web api request.
12841
+ msg:SpotifyApiMessage = SpotifyApiMessage(apiMethodName, '/me/player/volume')
12842
+ msg.RequestHeaders[self.AuthToken.HeaderKey] = self.AuthToken.HeaderValue
12843
+ msg.UrlParameters = urlParms
12844
+ self.MakeRequest('PUT', msg)
12366
12845
 
12367
12846
  # give spotify web api time to process the change.
12368
12847
  if delay > 0:
@@ -12394,6 +12873,7 @@ class SpotifyClient:
12394
12873
  delay:float=0.50,
12395
12874
  refreshDeviceList:bool=True,
12396
12875
  forceActivateDevice:bool=True,
12876
+ deviceIdFrom:str=None,
12397
12877
  ) -> None:
12398
12878
  """
12399
12879
  Transfer playback to a new Spotify Connect device and optionally begin playback.
@@ -12401,10 +12881,14 @@ class SpotifyClient:
12401
12881
  This method requires the `user-modify-playback-state` scope.
12402
12882
 
12403
12883
  Args:
12404
- deviceId (str):
12405
- The id or name of the device on which playback should be started/transferred.
12406
- Example: `0d1841b0976bae2a3a310dd74c0f3df354899bc8`
12407
- Example: `Web Player (Chrome)`
12884
+ deviceId (str | SpotifyConnectDevice) | None):
12885
+ The target player device identifier.
12886
+ This could be an id, name, a default device indicator (e.g. "*"), a
12887
+ SpotifyConnectDevice object, or null to utilize the active player device.
12888
+ A device is considered resolved if a SpotifyConnectDevice object is passed
12889
+ for this argument. An exception will be raised if the argument value could
12890
+ not be resolved or activated.
12891
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
12408
12892
  play (bool):
12409
12893
  The transfer method:
12410
12894
  - `True` - ensure playback happens on new device.
@@ -12424,6 +12908,13 @@ class SpotifyClient:
12424
12908
  force the device to reconnect to Spotify Connect; otherwise, False to not
12425
12909
  disconnect.
12426
12910
  Default is True.
12911
+ deviceIdFrom (str | SpotifyConnectDevice) | None):
12912
+ The player device identifier where play is being transferred from.
12913
+ This could be an id, name, a default device indicator (e.g. "*"), a
12914
+ SpotifyConnectDevice object, or null.
12915
+ A device is considered resolved if a SpotifyConnectDevice object is passed
12916
+ for this argument.
12917
+ Examples are `0d1841b0976bae2a3a310dd74c0f3df354899bc8`, `Office`, `*`, None.
12427
12918
 
12428
12919
  Raises:
12429
12920
  SpotifyWebApiError:
@@ -12435,6 +12926,11 @@ class SpotifyClient:
12435
12926
  This API only works for users who have Spotify Premium.
12436
12927
 
12437
12928
  The order of execution is not guaranteed when you use this API with other Player API endpoints.
12929
+
12930
+ The `deviceIdFrom` argument is not part of the Spotify Web API specification. If a value
12931
+ was supplied, then we will attempt to stop play on the device if it's needed. This is required
12932
+ if transferring playback from a Sonos device that is playing content on its local queue; if you
12933
+ don't stop playback on it, then it and the target device will both be playing!
12438
12934
 
12439
12935
  <details>
12440
12936
  <summary>Sample Code</summary>
@@ -12456,6 +12952,7 @@ class SpotifyClient:
12456
12952
  apiMethodParms.AppendKeyValue("delay", delay)
12457
12953
  apiMethodParms.AppendKeyValue("refreshDeviceList", refreshDeviceList)
12458
12954
  apiMethodParms.AppendKeyValue("forceActivateDevice", forceActivateDevice)
12955
+ apiMethodParms.AppendKeyValue("deviceIdFrom", deviceIdFrom)
12459
12956
  _logsi.LogMethodParmList(SILevel.Verbose, "Transfer playback to a new Spotify Connect device", apiMethodParms)
12460
12957
 
12461
12958
  # validations.
@@ -12467,15 +12964,69 @@ class SpotifyClient:
12467
12964
  if (forceActivateDevice is None):
12468
12965
  forceActivateDevice = True
12469
12966
 
12967
+ # was a from deviceId specified? if so, then we will attempt to stop play on the device
12968
+ # if it's needed. this is required if transferring playback from a Sonos device that is
12969
+ # playing content on its local queue; if you don't stop playback on it, then it and the
12970
+ # target device will both be playing!
12971
+ if (deviceIdFrom is not None):
12972
+
12973
+ # trace.
12974
+ _logsi.LogVerbose("FROM deviceId supplied; checking to see if we need to pause play on device \"%s\" prior to transferring playback to device \"%s\"" % (deviceIdFrom, deviceId))
12975
+
12976
+ # resolve from device object; no need to activate it for what we want here.
12977
+ scDeviceFrom:SpotifyConnectDevice = self._ResolveDeviceObject(deviceIdFrom, activateDevice=False)
12978
+
12979
+ # if it's a Sonos, then pause the player.
12980
+ # note that for other brands, the Spotify Web API transfer playback takes care of
12981
+ # pausing the player since everything is under the Spotify players control.
12982
+ if (scDeviceFrom is not None) and (scDeviceFrom.IsSonos):
12983
+ self.PlayerMediaPause(scDeviceFrom)
12984
+
12470
12985
  # resolve / activate the device object if needed.
12471
- scDevice = self._ResolveDeviceObject(deviceId)
12986
+ # for transfer playback, we will always activate the device.
12987
+ scDevice = self._ResolveDeviceObject(deviceId, True)
12988
+
12989
+ # if device could not be resolved, then go no further as it will result in a
12990
+ # "device not found" error from the Spotify Web API anyway!
12991
+ if (scDevice is None):
12992
+ raise SpotifyConnectDeviceNotFound("Spotify Player device was not found, and there is no active Spotify player", logsi=_logsi)
12472
12993
 
12473
12994
  # is this a Sonos device?
12474
12995
  # 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')
12996
+ # note that the Spotify Web API cannot stop nor start play on the device either,
12997
+ # as it's considered a restricted device and the Spotify Web API cannot control it!
12998
+ # all requests to control the device will be made via the Sonos Controller API.
12999
+ if (scDevice.IsSonos):
13000
+
13001
+ # get Sonos Controller instance for the device.
13002
+ sonosPlayer:SoCo = self.SpotifyConnectDirectory.GetSonosPlayer(scDevice)
13003
+
13004
+ # get current Sonos transport status.
13005
+ sonosTransportInfo:dict = sonosPlayer.get_current_transport_info()
13006
+ currentTransportState:str = sonosTransportInfo.get('current_transport_state', None)
13007
+ currentTransportStatus:str = sonosTransportInfo.get('current_transport_status', None)
13008
+ _logsi.LogVerbose("Sonos device %s current transport state after activation: \"%s\" (Status=%s)" % (scDevice.Title, currentTransportState, currentTransportStatus))
13009
+
13010
+ # stop / start play as requested.
13011
+ wasCmdIssued:bool = False
13012
+ if currentTransportState == 'PLAYING':
13013
+ if play == False:
13014
+ _logsi.LogVerbose("Issuing command to Sonos device %s: PAUSE" % (scDevice.Title))
13015
+ sonosPlayer.pause()
13016
+ wasCmdIssued = True
13017
+ elif currentTransportState in ['PAUSED_PLAYBACK','STOPPED','TRANSITIONING']:
13018
+ if play == True:
13019
+ _logsi.LogVerbose("Issuing command to Sonos device %s: PLAY" % (scDevice.Title))
13020
+ sonosPlayer.play()
13021
+ wasCmdIssued = True
13022
+
13023
+ # give SoCo api time to process the change.
13024
+ if (wasCmdIssued) and (delay > 0):
13025
+ _logsi.LogVerbose(TRACE_MSG_DELAY_DEVICE_SONOS % delay)
13026
+ time.sleep(delay)
13027
+
13028
+ # trace.
13029
+ _logsi.LogVerbose("Sonos device detected (IsRestricted); bypassing call to Spotify Web API Transfer Playback endpoint")
12479
13030
  return
12480
13031
 
12481
13032
  # before transferring playback to the device, we first need to check to see if anything