Skip to content

Unable to decode dealer user collection (liked song collection) events #1502

@BenJeau

Description

@BenJeau

Description

I'm creating a Spotify client to listen passively on events from Spotify. It works great for most of the events, except the hm://collection/collection/:username one, which is used whenever a user adds/remove a song from their liked songs.

Version

Using the latest commit, 8b72954

How to reproduce

  1. Create a new project with the following dependencies + code
  2. Execute cargo run
  3. Login to Spotify in the browser window it opens up
  4. Add/Remove a song from your saved library
  5. View error in the logs

Cargo.toml

[dependencies]
futures-util = "0.3.31"
librespot = { git = "https://github.com/librespot-org/librespot", rev = "8b729540f4ad1e7f8c94ff3bba33095878b24d02" }
protobuf = "3.7.2"
tokio = { version = "1.45.1", features = ["full"] }
tracing = "0.1.41"
tracing-subscriber = "0.3.19"

main.rs - the protobuf message used for hm://collection/collection/:username is certainly not the right one, but it would give another error if it reached that part (I'm also unsure which protobuf schema this would relate to):

use futures_util::stream::StreamExt;
use librespot::{
    core::{Error, Session, SessionConfig, dealer::protocol::Message},
    discovery::Credentials,
    oauth::OAuthClientBuilder,
    protocol::connect::{ClusterUpdate, MemberType, PutStateReason, PutStateRequest},
};
use protobuf::EnumOrUnknown;

const HTML: &str = r#"<!DOCTYPE html>
<html>
  <head>
    <script>
      window.close();
    </script>
  </head>
</html>
"#;

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::DEBUG)
        .init();

    let client = OAuthClientBuilder::new(
        "65b708073fc0480ea92a077233ca87bd",
        "http://127.0.0.1:8898/login",
        vec!["streaming"],
    )
    .open_in_browser()
    .with_custom_message(HTML)
    .build()
    .unwrap();

    let access_token = client.get_access_token_async().await.unwrap();
    let refresh_token = client
        .refresh_token_async(&access_token.refresh_token)
        .await
        .unwrap();
    let credentials = Credentials::with_access_token(refresh_token.access_token);
    let session = Session::new(SessionConfig::default(), None);

    fn extract_connection_id(msg: Message) -> Result<String, Error> {
        let connection_id = msg.headers.get("Spotify-Connection-Id").unwrap();
        Ok(connection_id.to_owned())
    }

    let mut connection_id_update = session
        .dealer()
        .listen_for("hm://pusher/v1/connections/", extract_connection_id)
        .unwrap();
    let mut user_collection_update = session
        .dealer()
        .listen_for::<ClusterUpdate>(
            &format!("hm://collection/collection/{}", session.username()),
            |message| {
                println!("user collection update:\n{message:?}");
                Message::from_raw(message)
            },
        )
        .unwrap();

    session.connect(credentials, true).await.unwrap();
    session.dealer().start().await.unwrap();

    println!("---connected!!!---");
    loop {
        tokio::select! {
            user_collection_update = user_collection_update.next() => {
                println!("user collection update:\n{user_collection_update:?}");
            }
            Some(Ok(connection_id)) = connection_id_update.next() => {
                session.set_connection_id(&connection_id);
                let request = PutStateRequest {
                    member_type: EnumOrUnknown::new(MemberType::CONNECT_STATE),
                    put_state_reason: EnumOrUnknown::new(PutStateReason::NEW_DEVICE),
                    ..Default::default()
                };
                session.spclient().put_connect_state_request(&request).await.unwrap();
            }
        }
    }
}

Log

My logs are extremely verbose, but I've captured where the parsing issues are logged. I removed/added/removed/added the same song link=https://open.spotify.com/track/1kuPgsWuwfNVHTPDBGMMj4, uri="spotify:track:1kuPgsWuwfNVHTPDBGMMj4", uid="ea0019b856d6d45d2cc4"

// removed
Message { headers: {"MC-ETag": "64941807882", "Collection-Update-Id": "66dce80f5c4d7c20", "Collection-Source-Revision": "55320748569", "Content-Type": "application/octet-stream"}, payload: Raw([10, 24, 8, 0, 18, 16, 43, 184, 38, 105, 23, 212, 70, 149, 146, 77, 130, 115, 44, 209, 83, 30, 40, 0, 48, 1]), uri: "hm://collection/collection/ha9ha10" }
Error { kind: FailedPrecondition, error: Error(WireError(Utf8Error)) }
2025-05-27T03:04:23.652329Z  WARN librespot_core::dealer: failure during data parsing for hm://collection/collection/ha9ha10/json: Invalid state { base64 decoding failed: Invalid symbol 125, offset 108. }    


// liked
Message { headers: {"Collection-Update-Id": "9ee4a66d1fa26263", "MC-ETag": "46685679324", "Collection-Source-Revision": "64941807882", "Content-Type": "application/octet-stream"}, payload: Raw([10, 28, 8, 0, 18, 16, 43, 184, 38, 105, 23, 212, 70, 149, 146, 77, 130, 115, 44, 209, 83, 30, 40, 191, 215, 212, 193, 6, 48, 0]), uri: "hm://collection/collection/ha9ha10" }
Error { kind: FailedPrecondition, error: Error(WireError(Utf8Error)) }
2025-05-27T03:04:31.498298Z  WARN librespot_core::dealer: failure during data parsing for hm://collection/collection/ha9ha10/json: Invalid state { base64 decoding failed: Invalid symbol 123, offset 0. }    

 
// removed
Message { headers: {"MC-ETag": "47316492062", "Collection-Source-Revision": "46685679324", "Collection-Update-Id": "ee516cc0498e02dd", "Content-Type": "application/octet-stream"}, payload: Raw([10, 24, 8, 0, 18, 16, 43, 184, 38, 105, 23, 212, 70, 149, 146, 77, 130, 115, 44, 209, 83, 30, 40, 0, 48, 1]), uri: "hm://collection/collection/ha9ha10" }
Error { kind: FailedPrecondition, error: Error(WireError(Utf8Error)) }
2025-05-27T03:04:40.601061Z  WARN librespot_core::dealer: failure during data parsing for hm://collection/collection/ha9ha10/json: Invalid state { base64 decoding failed: Invalid symbol 125, offset 108. }   


// liked
Message { headers: {"Collection-Source-Revision": "47316492062", "Content-Type": "application/octet-stream", "MC-ETag": "56467871245", "Collection-Update-Id": "ab9637744956f5fa"}, payload: Raw([10, 28, 8, 0, 18, 16, 43, 184, 38, 105, 23, 212, 70, 149, 146, 77, 130, 115, 44, 209, 83, 30, 40, 202, 215, 212, 193, 6, 48, 0]), uri: "hm://collection/collection/ha9ha10" }
Error { kind: FailedPrecondition, error: Error(WireError(Utf8Error)) }
2025-05-27T03:04:42.219687Z  WARN librespot_core::dealer: failure during data parsing for hm://collection/collection/ha9ha10/json: Invalid state { base64 decoding failed: Invalid symbol 123, offset 0. }  

Host (what you are running librespot on):

  • OS: macOS 15.5 (24F74)
  • Platform: MacBook Pro

Additional context

You can see the raw data is similar throughout the requests:

  • 1st removed: [10, 24, 8, 0, 18, 16, 43, 184, 38, 105, 23, 212, 70, 149, 146, 77, 130, 115, 44, 209, 83, 30, 40, 0, 48, 1]
  • 1st added: [10, 28, 8, 0, 18, 16, 43, 184, 38, 105, 23, 212, 70, 149, 146, 77, 130, 115, 44, 209, 83, 30, 40, 191, 215, 212, 193, 6, 48, 0]
  • 2nd removed: [10, 24, 8, 0, 18, 16, 43, 184, 38, 105, 23, 212, 70, 149, 146, 77, 130, 115, 44, 209, 83, 30, 40, 0, 48, 1] (the same)
  • 2nd added: [10, 28, 8, 0, 18, 16, 43, 184, 38, 105, 23, 212, 70, 149, 146, 77, 130, 115, 44, 209, 83, 30, 40, 202, 215, 212, 193, 6, 48, 0]

I can see the following:

  • 8, 0, 18, 16, 43, 184, 38, 105, 23, 212, 70, 149, 146, 77, 130, 115, 44, 209, 83, 30, 40 is common, I'm guessing that the song ID
  • the last byte seems to be a boolean, 1=removed, 0=added

Ultimately, I'm unfamiliar how this works other from my exploration of this codebase.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions