Skip to content

Commit

Permalink
[ 1.0.69 ] * Updated code when calling the spotifywebapiPython `GetPl…
Browse files Browse the repository at this point in the history
…aylist` method to not log exception information if the call fails. This was causing exceptions to be logged when trying to retrieve details for Spotify-owned algorithmic playlist details; the call now fails due to the unannounced Spotify Web API changes that were made by the Spotify Developer Team on 2024/11/27. Note that the call works fine for user-defined playlists.

  * The `media_playlist` extended attribute will now display `Unknown` if the currently playing context is a Spotify-owned algorithmic playlist (e.g. "Daily Mix 1", etc). It will display the correct playlist name if the currently playing context is a user-defined playlist.
  * The `sp_playlist_name` extended attribute will now display `Unknown` if the currently playing context is a Spotify-owned algorithmic playlist (e.g. "Daily Mix 1", etc).  It will display the correct playlist name if the currently playing context is a user-defined playlist.
  * Updated underlying `spotifywebapiPython` package requirement to version 1.0.125.
  • Loading branch information
thlucas1 committed Dec 9, 2024
1 parent 3aa0d8a commit 11d3822
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 19 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ Change are listed in reverse chronological order (newest to oldest).

<span class="changelog">

###### [ 1.0.69 ] - 2024/12/09

* Updated code when calling the spotifywebapiPython `GetPlaylist` method to not log exception information if the call fails. This was causing exceptions to be logged when trying to retrieve details for Spotify-owned algorithmic playlist details; the call now fails due to the unannounced Spotify Web API changes that were made by the Spotify Developer Team on 2024/11/27. Note that the call works fine for user-defined playlists.
* The `media_playlist` extended attribute will now display `Unknown` if the currently playing context is a Spotify-owned algorithmic playlist (e.g. "Daily Mix 1", etc). It will display the correct playlist name if the currently playing context is a user-defined playlist.
* The `sp_playlist_name` extended attribute will now display `Unknown` if the currently playing context is a Spotify-owned algorithmic playlist (e.g. "Daily Mix 1", etc). It will display the correct playlist name if the currently playing context is a user-defined playlist.
* Updated underlying `spotifywebapiPython` package requirement to version 1.0.125.

###### [ 1.0.68 ] - 2024/12/06

* Updated service `player_transfer_playback` to resume play if `play=True` and `force_activate_device=True`. If forcefully activating a device, then we need to resume play manually if `play=True` was specified; this is due to the device losing its current status since it was being forcefully activated (e.g. disconnected and reconnected).
Expand Down
4 changes: 2 additions & 2 deletions custom_components/spotifyplus/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
"requests_oauthlib>=1.3.1",
"soco>=0.30.4",
"smartinspectPython>=3.0.33",
"spotifywebapiPython>=1.0.123",
"spotifywebapiPython>=1.0.125",
"urllib3>=1.21.1,<1.27",
"zeroconf>=0.132.2"
],
"version": "1.0.68",
"version": "1.0.69",
"zeroconf": [ "_spotify-connect._tcp.local." ]
}
94 changes: 78 additions & 16 deletions custom_components/spotifyplus/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import time
import urllib.parse
from datetime import timedelta, datetime
from pprint import pformat
from typing import Any, Callable, Concatenate, ParamSpec, TypeVar, Tuple
from yarl import URL

Expand Down Expand Up @@ -328,6 +329,9 @@ def __init__(self, data:InstanceDataSpotifyPlus) -> None:
# initialize instance storage.
self._id = data.spotifyClient.UserProfile.Id
self._playlist:Playlist = None
self._lastMediaPlayedPosition:int = 0
self._lastMediaPlayedContextUri:str = None
self._lastMediaPlayedUri:str = None
self.data = data
self._currentScanInterval:int = 0
self._commandScanInterval:int = 0
Expand Down Expand Up @@ -897,7 +901,6 @@ def select_source(self, source: str) -> None:
# transfer playback to the specified source.
self.service_spotify_player_transfer_playback(
source,
#play=(self._attr_state != MediaPlayerState.PAUSED),
play=True,
refreshDeviceList=True,
forceActivateDevice=True)
Expand Down Expand Up @@ -1105,9 +1108,6 @@ def turn_on(self) -> None:
# get current Spotify Connect player state.
self._playerState, self._spotifyConnectDevice, self._sonosDevice = self._GetPlayerPlaybackState()

# update the source list (spotify connect devices cache).
#self.data.spotifyClient.GetSpotifyConnectDevices(refresh=True)

# try to automatically select a source for play.
# if spotify web api player is not found and a default spotify connect device is configured, then select it;
# otherwise, select the last active device source;
Expand All @@ -1128,13 +1128,25 @@ def turn_on(self) -> None:

# was a source selected?
if source is not None:
# yes - activate the selected source (e.g. transfer playback).
self.select_source(source)
self._isInCommandEvent = True # turn "in a command event" indicator back on.
# yes - is the source currently active?
if (self._playerState) \
and (self._playerState.Device) \
and ((self._playerState.Device.Name == source) or (self._playerState.Device.Id == source)) \
and (self._playerState.Device.IsActive):
# yes - nothing to do since the source is active source.
_logsi.LogVerbose("'%s': Previously active source '%s' is still the active source; bypassing select_source" % (self.name, source))
pass
else:
# no - activate (e.g. transfer playback to) the selected source.
self.select_source(source)
self._isInCommandEvent = True # turn "in a command event" indicator back on.
else:
# no - update the source list (spotify connect devices cache).
self.data.spotifyClient.GetSpotifyConnectDevices(refresh=True)

# trace.
_logsi.LogVerbose("'%s': About to resume play; last known media content: ContextUri=%s, Uri=%s, Position=%d" % (self.name, self._lastMediaPlayedContextUri, self._lastMediaPlayedUri, self._lastMediaPlayedPosition))

# is playing content paused? if so, then resume play.
if (self._playerState.Device.IsActive) \
and (self._playerState.Actions.Pausing):
Expand Down Expand Up @@ -1223,6 +1235,9 @@ def update(self) -> None:
pass
else:
# no - keep waiting to update.
# add scan interval time value to last media played position since it's not a real-time value.
if (self._attr_state == MediaPlayerState.PLAYING):
self._lastMediaPlayedPosition = (self._lastMediaPlayedPosition + SCAN_INTERVAL.seconds)
return

# # TEST TODO - force token expire!!!
Expand Down Expand Up @@ -1263,18 +1278,31 @@ def update(self) -> None:

# yes - if it's a playlist, then we need to update the stored playlist reference.
self._playlist = None
self._lastMediaPlayedContextUri = context.Uri
if context.Type == MediaType.PLAYLIST:

# as of 2024/11/27, Spotify deprecated API support for various Spotify-owned playlists!
# due to that, the following `GetPlaylist` call will fail if the currently playing context
# is a Spotify-owned "algorithmic" playlist (e.g. various "Made For You" content, etc).
try:

_logsi.LogVerbose("Retrieving playlist for context uri '%s'" % context.Uri)
_logsi.LogVerbose("'%s': Retrieving playlist for context uri '%s'" % (self.name, context.Uri))
spotifyId:str = SpotifyClient.GetIdFromUri(context.Uri)
self._playlist = self.data.spotifyClient.GetPlaylist(spotifyId)

except Exception as ex:

_logsi.LogException("Unable to load spotify playlist '%s'. Continuing without playlist data" % context.Uri, ex)
self._playlist = None
#_logsi.LogException("Unable to get playlist data for context '%s'. Continuing without playlist data" % context.Uri, ex, logToSystemLogger=False)
_logsi.LogWarning("'%s': Unable to get playlist data for context '%s'. Continuing without playlist data. GetPlaylist response: %s" % (self.name, context.Uri, str(ex)), logToSystemLogger=False)

# if we could not get the current playlist info, then build a "dummy" playlist so that
# information is still conveyed in the extended attributes.
self._playlist = Playlist()
self._playlist.Uri = context.Uri
self._playlist.Type = self.data.spotifyClient.GetTypeFromUri(context.Uri)
self._playlist.Id = self.data.spotifyClient.GetIdFromUri(context.Uri)
self._playlist.Name = "Unknown"
self._playlist.Description = str(ex)

else:

Expand Down Expand Up @@ -1614,6 +1642,9 @@ def _UpdateHAFromPlayerPlayState(self, playerPlayState:PlayerPlayState) -> None:
self._attr_media_content_type = item.Type
self._attr_media_duration = item.DurationMS / 1000
self._attr_media_title = item.Name

# save currently playing track uri in case we need to restore it later.
self._lastMediaPlayedUri = playerPlayState.Item.Uri

# update media album name attribute.
if item.Type == MediaType.EPISODE.value:
Expand Down Expand Up @@ -1679,9 +1710,11 @@ def _UpdateHAFromPlayerPlayState(self, playerPlayState:PlayerPlayState) -> None:
self.data.spotifyClient.GetSpotifyConnectDevices(refresh=True)

# update seek-related attributes.
# also save currently playing track position in case we need to restore it later.
if playerPlayState.ProgressMS is not None:
self._attr_media_position = playerPlayState.ProgressMS / 1000
self._attr_media_position_updated_at = utcnow()
self._lastMediaPlayedPosition = self._attr_media_position

# update repeat related attributes.
if playerPlayState.RepeatState is not None:
Expand Down Expand Up @@ -7048,12 +7081,24 @@ def service_spotify_player_transfer_playback(

# get current Spotify Connect player state.
self._playerState, self._spotifyConnectDevice, self._sonosDevice = self._GetPlayerPlaybackState()

# get current device name and id values (if any).
currentDeviceName:str = self._playerState.Device.Name or ''
currentDeviceId:str = self._playerState.Device.Id or ''
_logsi.LogVerbose("'%s': Transferring playback from source device name: '%s' (id=%s)" % (self.name, currentDeviceName, currentDeviceId))

# trace.
if (_logsi.IsOn(SILevel.Debug)):
dictText:str = pformat(self._playerState or "* No State *",indent=2,width=132,sort_dicts=False)
_logsi.LogVerbose("'%s': Spotify Player Playback State BEFORE transfer: %s" % (self.name, dictText))
if (self._playerState):
dictText:str = pformat(self._playerState.Device or "* No Device *",indent=2,width=132,sort_dicts=False)
_logsi.LogVerbose("'%s': Spotify Player Playback State BEFORE transfer: %s" % (self.name, dictText))
dictText:str = pformat(self._playerState.Item or "* No Track *",indent=2,width=132,sort_dicts=False)
_logsi.LogVerbose("'%s': Spotify Player Playback State BEFORE transfer: %s" % (self.name, dictText))
dictText:str = pformat(self._playerState.Context or "* No Context *",indent=2,width=132,sort_dicts=False)
_logsi.LogVerbose("'%s': Spotify Player Playback State BEFORE transfer: %s" % (self.name, dictText))

# is there an active spotify connect device?
# if so, then we will pause playback on the device before we transfer control
# to the target device.
Expand Down Expand Up @@ -7237,18 +7282,35 @@ def service_spotify_player_transfer_playback(
# get current Spotify Connect player state.
self._playerState, self._spotifyConnectDevice, self._sonosDevice = self._GetPlayerPlaybackState()

# trace.
if (_logsi.IsOn(SILevel.Debug)):
dictText:str = pformat(self._playerState or "* No State *",indent=2,width=132,sort_dicts=False)
_logsi.LogVerbose("'%s': Spotify Player Playback State AFTER transfer: %s" % (self.name, dictText))
if (self._playerState):
dictText:str = pformat(self._playerState.Device or "* No Device *",indent=2,width=132,sort_dicts=False)
_logsi.LogVerbose("'%s': Spotify Player Playback State AFTER transfer: %s" % (self.name, dictText))
dictText:str = pformat(self._playerState.Item or "* No Track *",indent=2,width=132,sort_dicts=False)
_logsi.LogVerbose("'%s': Spotify Player Playback State AFTER transfer: %s" % (self.name, dictText))
dictText:str = pformat(self._playerState.Context or "* No Context *",indent=2,width=132,sort_dicts=False)
_logsi.LogVerbose("'%s': Spotify Player Playback State AFTER transfer: %s" % (self.name, dictText))

# reset internal SonosDevice to the SonosDevice that we just created (if transferring to Sonos).
self._sonosDevice = sonosDevice

# set the selected source.
# did we resolve a target device?
if scDevice is not None:

# yes - set the selected source.
self._attr_source = scDevice.Name
_logsi.LogVerbose("'%s': Selected source was changed to: '%s'" % (self.name, self._attr_source))

# resume play (if requested and necessary).
if (play) and (self.state == 'paused'):
_logsi.LogVerbose("'%s': Selected source was changed to: '%s'" % (self.name, self._attr_source))
self.media_play()
# trace.
_logsi.LogVerbose("'%s': About to resume play; last known media content: ContextUri=%s, Uri=%s, Position=%d" % (self.name, self._lastMediaPlayedContextUri, self._lastMediaPlayedUri, self._lastMediaPlayedPosition))

# resume play (if requested and necessary).
if (play) and (self.state == 'paused'):
_logsi.LogVerbose("'%s': Current state is paused; resuming play on source: '%s'" % (self.name, self._attr_source))
self.media_play()

# media player command was processed, so force a scan window at the next interval.
_logsi.LogVerbose("'%s': Processed a transfer playback command - forcing a playerState scan window for the next %d updates" % (self.name, SPOTIFY_SCAN_INTERVAL_COMMAND - 1))
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ homeassistant==2024.5.0
ruff==0.1.3
soco>=0.30.4
smartinspectPython>=3.0.33
spotifywebapiPython>=1.0.123
spotifywebapiPython>=1.0.125

0 comments on commit 11d3822

Please sign in to comment.