SpotifyPlus Custom Integration

Unfortunately no :sob:

FYI - just released a new version of the SpotifyPlus integration

[ 1.0.56 ] - 2024/09/19

  • Updated underlying spotifywebapiPython package requirement to version 1.0.96.

FYI - just released a new version of the SpotifyPlus integration

[ 1.0.57 ] - 2024/09/20

  • Added service get_audiobook_favorites to get a list of the audiobooks saved in the current Spotify user’s ‘Your Library’.
  • Added service get_episode_favorites to get a list of the episodes saved in the current Spotify user’s ‘Your Library’.
  • Updated underlying spotifywebapiPython package requirement to version 1.0.97.

This release fixes a bug related to Spotify Connect devices that define aliases (introduced with v1.0.93). This was causing TypeError: unsupported operand type(s) for &: 'str' and 'tuple' exceptions if a device had aliases defined.

1 Like

Hi @thlucas, thanks for working on this integration!

I try to understand how to connect to a dynamic Spotify Connect device (spotifyd) which is not bound to any account (zeroconf authentication, spotifyd calls it “discovery mode” in the log).

You describe here how to solve common Spotify Connect issues.

What is not clear to me is how to fill the the upcoming fields with the values from the response, e.g. if I run the spotifyplus.zeroconf_discover_devices service, how to use the response for the spotifyplus.zeroconf_device_connect service.

May I ask you to elaborate on this a bit? Thanks a lot!!

Regards,
meiser

@meiser
If you just want to re-activate the device, have a look at the Common Spotify Connect Issues wiki page (that you mentioned above), and scroll down to The Solution (for INDIVIDUAL devices) topic heading. It gives specific details on how to take the returned parameters from the zeroconf_discover_devices service and use them in the zeroconf_device_connect service to awaken the specified device.

If you just want to start playing content on the device, you can use the Transfer Playback Service to awaken the device and transfer playback to it. Note that the refresh_device_list: true parameter is required in order to re-awaken the device (if it needs it).

service: spotifyplus.player_transfer_playback
data:
  entity_id: media_player.spotifyplus_john_s
  device_id: "Office"
  play: true
  refresh_device_list: true

Please let me know if this works with spotifyd or not. I have another user with a spotifyd issue, but I think his is related more to a network configuration problem. I have never used spotifyd before, so I am curious to see how it plays with the integration. It should work fine if spotifyd truly supports Spotify Connect Zeroconf discovery and configuration.

The thing is (as I’m not an experienced HA user yet), how do I use the values? How can I use the returned values in the next service? What’s the purpose of the response variable?

I tried the zeroconf_device_connect service in the developer tools, but it just returns “Unknown error”.
Log shows:

024-09-25 18:27:53.870 ERROR (MainThread) [homeassistant.helpers.script.websocket_api_script] websocket_api script: Error executing script. Error for call_service at pos 1:
2024-09-25 18:27:53.871 ERROR (MainThread) [homeassistant.components.websocket_api.http.connection] [140410510929936] Error handling message: Unknown error (home_assistant_error) sysadmin from 192.168.3.3 (Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36.0 (KHTML, like Gecko) Chrome/126.0.6478.185 Safari/537.36.0)
Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/spotifywebapipython/zeroconfapi/zeroconfconnect.py", line 268, in _CheckResponseForErrors
    raise SpotifyWebApiError(response.status_code, errMessage, methodName, response.reason, _logsi)
spotifywebapipython.spotifywebapierror.SpotifyWebApiError: SAM1001E - Spotify Web API returned an error status while processing the "Disconnect" method.
Status: 404 - Not Found
Message: ""

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/config/custom_components/spotifyplus/media_player.py", line 6909, in service_spotify_zeroconf_device_connect
    result = zconn.Disconnect()
             ^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotifywebapipython/zeroconfapi/zeroconfconnect.py", line 908, in Disconnect
    responseData:dict = self._CheckResponseForErrors(response, apiMethodName, endpoint)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/spotifywebapipython/zeroconfapi/zeroconfconnect.py", line 285, in _CheckResponseForErrors
    raise SpotifyZeroconfApiError(response.status_code, responseUTF8, methodName, response.reason, _logsi)
spotifywebapipython.zeroconfapi.spotifyzeroconfapierror.SpotifyZeroconfApiError: SAM1003E - Spotify ZeroConf API returned an error status while processing the "Disconnect" method.
Status: 404 - Not Found
Message: ""

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/websocket_api/decorators.py", line 28, in _handle_async_response
    await func(hass, connection, msg)
  File "/usr/src/homeassistant/homeassistant/components/websocket_api/commands.py", line 816, in handle_execute_script
    script_result = await script_obj.async_run(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 1795, in async_run
    return await asyncio.shield(create_eager_task(run.async_run()))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 463, in async_run
    await self._async_step(log_exceptions=False)
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 527, in _async_step
    self._handle_exception(
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 557, in _handle_exception
    raise exception
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 525, in _async_step
    await getattr(self, handler)()
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 763, in _async_call_service_step
    response_data = await self._async_run_long_action(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 726, in _async_run_long_action
    return await long_task
           ^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/core.py", line 2761, in async_call
    response_data = await coro
                    ^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/core.py", line 2804, in _execute_service
    return await target(service_call)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/config/custom_components/spotifyplus/__init__.py", line 1659, in service_handle_spotify_serviceresponse
    response = await hass.async_add_executor_job(entity.service_spotify_zeroconf_device_connect, username, password, loginid, host_ipv4_address, host_ip_port, cpath, version, use_ssl, pre_disconnect, verify_device_list_entry)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/config/custom_components/spotifyplus/media_player.py", line 6926, in service_spotify_zeroconf_device_connect
    raise HomeAssistantError(ex.Message)
homeassistant.exceptions.HomeAssistantError

And player_transfer_playback always returns “Device not found” when using the device name.

If I try to connect to the spotifyd instance with the mini player and select it as a source, the spotifyd logs shows “Bad credentials”. I entered the credentials in the configuration section. My loginid is identical to my username according to the https://api.spotify.com/v1/me returned values.
As I understand, spotifyd needs the OAuth token for authentication, not username/password, see here.

BTW, spotifyd works perfectly fine with the Spotify Android and desktop (Ubuntu) app.

@meiser
The “Bad Credentials” log entry sounds like spotifyd is not able to connect to the underlying Spotify API. I know there were issues with librespot being able to connect recently, but not sure if they have it sorted yet. The fact that spotifyd works correctly with Spotify Android / ubuntu apps is not relevant, as they (probably ?) use the Spotify Embedded Client to communicate with spotifyd (not Spotify Connect Zeroconf).

The SAM1003E - Spotify ZeroConf API returned an error status while processing the "Disconnect" method. Status: 404 - Not Found indicates that the Zeroconf call to Spotify Connect failed for the given device; specifically that the resetUsers Zeroconf entry-point could not be found (e.g. 404). This is probably related to the spotifyd / Bad Credentials issue, in that spotifyd cannot access the underlying Spotify Web API / Spotify Zeroconf API.

As for passing variables from service responses to another service, check out the Play First Context of Category Playlists script example.

The response variable contains the data returned from a service, IF the service returns data. This is optional, as some services do not return response data. Once the service completes, the service response variable will contain a dictionary of values that can be accessed by scripts and such. In the script example above, the output of the get_category_playlists returns the following dictionary (abbreviated example):

user_profile:
  country: US
  display_name: Todd L
  product: premium
  type: user
result:
  limit: 1
  items:
    - collaborative: false
      description: "New jazz for open minds. Cover: Nubya Garcia"
      external_urls:
        spotify: https://open.spotify.com/playlist/37i9dQZF1DX7YCknf2jT6s
      href: https://api.spotify.com/v1/playlists/37i9dQZF1DX7YCknf2jT6s
      id: 37i9dQZF1DX7YCknf2jT6s
      image_url: https://i.scdn.co/image/ab67706f000000021b3869f2e1d4f52bc5b034f9
      images:
        - url: https://i.scdn.co/image/ab67706f000000021b3869f2e1d4f52bc5b034f9
          height: null
          width: null
      name: State of Jazz
      owner:
        display_name: Spotify
        external_urls:
          spotify: https://open.spotify.com/user/spotify
        followers: {}
        href: https://api.spotify.com/v1/users/spotify
        id: spotify
        type: user
        uri: spotify:user:spotify
      public: true
      snapshotId: ZuzzQAAAAAC+o1HXn4U2iwUyk4m7lkfm
      tracks:
        href: https://api.spotify.com/v1/playlists/37i9dQZF1DX7YCknf2jT6s/tracks
        total: 100
      type: playlist
      uri: spotify:playlist:37i9dQZF1DX7YCknf2jT6s
message: Jazz

The seconds service being executed accesses response data from the first service like so:
context_uri: "{{ search_result.result[\"items\"][0].uri }}"
This says to retrieve the URI value from the first (zero indexed) items collection value (e.g. search_result.result[\"items\"][0].uri) and use it as the value to the context_uri parameter for the player_media_play_context service call.

This gets more into templating and how to write scripts, which is a pretty advanced topic for this thread. You might do some Googling on the HA template scripts, as well as check out the HA Developer Tools \ Template tool. I use it quite frequently when developing scripts / parsing output.

For example, copy / paste the following into the HA Developer Tools \ Template tool:

{## Imitate available variables: ##}
{% set episodes = {
  "items": [
  {
    "is_externally_hosted": false,
    "is_playable": true,
    "name": "Armchair Anonymous: Grocery Store",
    "release_date": "2024-08-23",
    "resume_point": {
      "fully_played": false,
      "resume_position_ms": 21000,
    },
    "type": "episode",
    "uri": "spotify:episode:3jkYh7UbNfdvqHDTJALOlT",
  },
  {
    "is_externally_hosted": false,
    "is_playable": true,
    "name": "Dan Slepian (on The Sing Sing Files)",
    "release_date": "2024-08-22",
    "resume_point": {
      "fully_played": false,
      "resume_position_ms": 0,
    },
    "type": "episode",
    "uri": "spotify:episode:3jkYh7UbNfdvqHDTJALOlT",
  }
  ]
}%}

Episode 1 Info:
  Episode Name = {{ episodes["items"][0].name }}
  Release Date = {{ episodes["items"][0].release_date }}
  Fully Played = {{ episodes["items"][0].resume_point.fully_played }}

Episode 2 Info:
  Episode Name = {{ episodes["items"][1].name }}
  Release Date = {{ episodes["items"][0].release_date }}
  Fully Played = {{ episodes["items"][1].resume_point.fully_played }}

Will look something like this:

Hope it helps!

@thlucas, thanks a lot for your patience and guidance!!

I captured the traffic and checked the addUser request.

From Spotify Android app to spotifyd:

Frame 158: 1096 bytes on wire (8768 bits), 1096 bytes captured (8768 bits)
Linux cooked capture v2
Internet Protocol Version 4, Src: 192.168.1.12, Dst: 192.168.1.4
Transmission Control Protocol, Src Port: 39182, Dst Port: 1234, Seq: 1, Ack: 1, Len: 1024
Hypertext Transfer Protocol
    POST / HTTP/1.1\r\n
    Accept-Encoding: gzip\r\n
    Content-Length: 793\r\n
    Connection: keep-alive\r\n
    Content-Type: application/x-www-form-urlencoded\r\n
    Keep-Alive: 0\r\n
    Host: 192.168.1.4:1234\r\n
    User-Agent: Spotify/8.9.76.538 Android/34 (M2012K11AG)\r\n
    \r\n
    [Response in frame: 160]
    [Full request URI: http://192.168.1.4:1234/]
    File Data: 793 bytes
HTML Form URL Encoded: application/x-www-form-urlencoded
    Form item: "action" = "addUser"
    Form item: "userName" = "username_string"
     […]Form item: "blob" = "blob_string"
    Form item: "clientKey" = "clientKey_string"
    Form item: "loginId" = "loginId_string"
    Form item: "deviceName" = "M2012K11AG"
    Form item: "deviceId" = "deviceId_string"
    Form item: "version" = "2.7.1"

From spotifyplus to spotifyd:

Frame 1278: 449 bytes on wire (3592 bits), 449 bytes captured (3592 bits)
Linux cooked capture v2
Internet Protocol Version 4, Src: 192.168.1.7, Dst: 192.168.1.4
Transmission Control Protocol, Src Port: 42274, Dst Port: 5354, Seq: 218, Ack: 1, Len: 377
[2 Reassembled TCP Segments (594 bytes): #1276(217), #1278(377)]
Hypertext Transfer Protocol
    POST / HTTP/1.1\r\n
    Host: 192.168.1.4:1234\r\n
    User-Agent: python-requests/2.32.3\r\n
    Accept-Encoding: gzip, deflate, br\r\n
    Accept: */*\r\n
    Connection: close\r\n
    Content-Type: application/x-www-form-urlencoded\r\n
    Content-Length: 377\r\n
    \r\n
    [Response in frame: 1280]
    [Full request URI: http://192.168.1.4:1234/]
    File Data: 377 bytes
HTML Form URL Encoded: application/x-www-form-urlencoded
    Form item: "action" = "addUser"
    Form item: "version" = "2.7.1"
    Form item: "tokenType" = "default"
    Form item: "clientKey" = "clientKey_string"
    Form item: "loginId" = "loginId_string"
    Form item: "userName" = "username_string"
    Form item: "blob" = "blob_string"

Could it be related to the User-Agent header that I get Bad Credentials as a user mentioned here something about a proper user-agent?

In my experience with the Spotify Connect Zeroconf addUser call, the user-agent is not relevant. All of the magic happens in the blob parameter, which is encrypted. I also don’t see a tokenType for the Spotify Android app flow, which is odd.

I would also suggest that you open a new issue if you want to pursue this further, so we can track details.

Thanks

I know it’s probably not a spotify plus issue, but can anyone help me out on where to look for the problem?
As often happens, this script doesn’t work and gives me the error you see below if I run it

data:
  entity_id: media_player.spotifyplus_diegocampy
  play: true
  device_id: xxxxxxxxxx 
  refresh_device_list: true
action: spotifyplus.player_transfer_playback
## Errore nell'esecuzione dell'azione

A SpotifyConnectUsername parameter was not supplied; could not reactivate Spotify Connect device 'Bose (xxxxxxxxx)'

@Diegocampy
You need to set your Spotify Connect Username, Password, and LoginId values in the SpotifyPlus configuration options.

This was something that changed with the latest release of the spotifywebapiPython due to changes made by Spotify and their login process.

The Spotify Connect Username and Password values are your Spotify credentials, and the Spotify Connect LoginID is the canonical form of your Spotify UserId. This value can easily be found using the Spotify Developer Web portal, using the Get Current Users profile service. Click on the Try It button, then find the id response value. It will look something like this: 31l77y75hfnhk79f7gk6jkk878mg.

1 Like

I begin to struggle to follow all the changes, every update I have to fix something … how would I do without you? thank you very much problem solved

1 Like

My apologies for all of the changes, but Spotify is an ever-changing enviornment. Not to mention I have to try and accommodate other devices besides Bose.
Thanks for your patience.

1 Like

I apologize, maybe I explained myself badly, I don’t complain to you, in general I was talking about any change in home assistant :sweat_smile:

1 Like

No apologies necessary, I understand. Home Assistant is ever-changing as well. lol

1 Like

Can i use SpotifyPlus with SpotCast instead Spotify Official Integration? Or Official is mandatory?

Thanks

@thlucas, I now switched to librespot directly instead of using spotifyd.

librespot does not support the ResetUsers API, and, if I understand it correctly, therefore player_transfer_playback fails. Could you make the Disconnect call optionally?

Traceback (most recent call last):
  File "/config/custom_components/spotifyplus/media_player.py", line 5197, in service_spotify_player_transfer_playback
    self.data.spotifyClient.PlayerTransferPlayback(deviceId, play, delay, refreshDeviceList=False)
  File "/usr/local/lib/python3.12/site-packages/spotifywebapipython/spotifyclient.py", line 12154, in PlayerTransferPlayback
    raise SpotifyApiError(SAAppMessages.UNHANDLED_EXCEPTION.format(apiMethodName, str(ex)), ex, logsi=_logsi)
spotifywebapipython.spotifyapierror.SpotifyApiError: SpotifyApiError: SAM0001E - An unhandled exception occured while processing method "PlayerTransferPlayback".
SAM1003E - Spotify ZeroConf API returned an error status while processing the "Disconnect" method.
Status: 404 - Not Found
Message: ""
1 Like

@cediqqu
Spotcast only supports the HA Spotify integration, and does not support SpotifyPlus.

1 Like

@meiser
I have come to the same conclusion. I have spent the last 3 days learning about spotifyd, and how it works.

UPDATE - just saw you opened a wiki issue for this. I will respond there with comments. Thanks.

FYI - just released a new version of the SpotifyPlus integration

[ 1.0.59 ] - 2024/09/28

  • Added support for devices that utilize spotifyd / librespot. See the wiki documentation for more information on how to configure support for spotifyD / librespot applications.
  • Corrected the media_content_id attribute value when content is played on a Sonos device. Prior to this fix, the value was loaded from the Sonos Soco API get_current_track_info.uri value that contained the x-sonos-vli value (e.g. x-sonos-vli:RINCON_38420B909DC801400:2,spotify:e934941535d7b182815bf688490ce8a8). The media_content_id attribute value now contains a true Spotify URI value (e.g. spotify:track:6kYyS9g4WJeRzTYqsmcMmM).
  • Added the following extra state attribute: sp_device_is_brand_sonos - denotes the source device is a Sonos brand device (true) or not (false).
  • Added service get_track to get Spotify catalog information for a single track identified by its unique Spotify ID.
  • Added service get_episode to get Spotify catalog information for a single episode identified by its unique Spotify ID.
  • Updated underlying spotifywebapiPython package requirement to version 1.0.101.