Closed ShayBox closed 1 year ago
Thank you for the post, I didn't think my library would attract attention any time soon :sweat_smile:
I haven't given the blocking API much though at all since, like the readme says, right now I'm developing this library for my own needs. I have a rough idea of how the blocking API could work alongside the async API, but it's going to require further thought and experimentation. I can start working on it, no problems there! Expect breaking changes :smile:
As for the web browser and HTTP server inclusion, I don't think they fall into the scope of the library. As much as the library could do it, so can you in your own application. I don't want to mandate a certain method for the authorization flow; the library simply gives you an authorization URL and expects back the authorization code and state. It's up to you to handle the in-between.
I've just published version 0.2.0 that contains the first blocking API, available through the crate feature sync
. The build
methods on the Spotify client builders have been renamed to build_async
, and they've gained build_sync
methods that return synchronous clients. All the clients have new type signatures that lets them be generic over the underlying HTTP client, so for convenience there are type aliases for each kind of client. From there on their usage is (hopefully) unchanged.
There should be documentation for everything, soon as docs.rs updates. The synchronous endpoints are essentially copypastes, and preliminary tests show they work identically to the asynchronous ones. I could've of course missed something, so if there's any issues, do let me know!
So far the library works as intended with blocking, but I'm not sure what "State" is in the examples for incomplete_auth_code_client.finalize
, there's only a code
query from the callback
I'm also struggling to optionally use PKCE at runtime, but that's likely a lack of Rust knowledge,
let spotify_client;
if config.spotify.pkce {
spotify_client = SpotifyClientBuilder::new(&config.spotify.client_id).build_sync()
} else {
spotify_client = SpotifyClientBuilder::new(&config.spotify.client_id)
.client_secret(&config.spotify.client_secret)
.build_sync()
.into_report()
.change_context(VRCError::Spotify)
.attach_printable("Failed to build Spotify client")?;
}
but SpotifyClient
and SpotifyClientWithSecret
aren't compatible, any ideas/insights?
When you retrieve the authorization URL from an incomplete authorization code client, you can check that the state parameter is in the query parameters: https://accounts.spotify.com/authorize?response_type=code&redirect_uri=http%3A%2F%2Flocalhost%2Fcallback&client_id=...&state=i8njcOCODJkPs0IE
. The callback URL will contain that same state parameter: https://localhost/callback?code=...&state=i8njcOCODJkPs0IE
. The only scenario when Spotify doesn't return the state is when it wasn't given one in the first place, but the library should always include it. Can you show if the state is missing from the authorization URL for some reason?
SpotifyClient
and SpotifyClientWithSecret
are purposefully two distinct types since the latter implements the unscoped endpoint traits and the former doesn't. If you only need one client in your application, you can go straight to an authorization code client:
let incomplete_auth_code_client = if config.spotify.pkce {
let spotify_client = SpotifyClientBuilder::new(&config.spotify.client_id).build_sync();
spotify_client
.authorization_code_client_with_pkce("callback")
.build()
} else {
let spotify_client = SpotifyClientBuilder::new(&config.spotify.client_id)
.client_secret(&config.spotify.client_secret)
.build_sync();
spotify_client
.authorization_code_client("callback")
.build()
};
// the Spotify clients inside the if-expression are dropped but that's okay, the final auth
// code client can access all unscoped and scoped endpoints
let auth_url = incomplete_auth_code_client.get_authorize_url();
...
let auth_code_client = incomplete_auth_code_pkce_client
.finalize(code, state);
If you do need to keep the original Spotify clients (which is only if you need to create more simultaneous auth code clients for different users), then I suggest grouping them in one common enum type, for example:
enum SpotifyClients {
NoSecret(SyncSpotifyClient),
Secret(SyncSpotifyClientWithSecret),
}
Today I'm getting a state query parameter, 10 hours ago I wasn't... I don't know why.
That code works, thank you!
EDIT: This is my final method using tiny_http
and webbrowser
, should anyone want to use it
fn get_query_from_user(url: &str, uri: &str) -> Result<(String, String), VRCError> {
match webbrowser::open(url) {
Ok(ok) => ok,
Err(why) => eprintln!(
"Error when trying to open an URL in your browser: {:?}. \
Please navigate here manually: {}",
why, url
),
}
let addr = uri.replace("http://", "").replace("https://", "");
let server = Server::http(addr).expect("Failed to bind server");
let request = match server.recv() {
Ok(rq) => rq,
Err(e) => panic!("Failed to get request: {e}"),
};
let request_url = uri.to_owned() + request.url();
let parsed_url = Url::parse(&request_url)
.into_report()
.change_context(VRCError::Url)?;
let header = Header::from_bytes(&b"Content-Type"[..], &b"text/html"[..]).unwrap();
let mut response;
if parsed_url.query_pairs().count() == 2 {
response = Response::from_string(
"<h1>You may close this tab</h1> \
<script>window.close()</script>",
);
} else {
response = Response::from_string("<h1>An error has occured</h1>");
}
response.add_header(header);
request.respond(response).expect("Failed to send response");
let Some(code) = parsed_url.query_pairs().next() else {
bail!(VRCError::None)
};
let Some(state) = parsed_url.query_pairs().nth(1) else {
bail!(VRCError::None)
};
Ok((code.1.into(), state.1.into()))
}
Hmm, It seems using PKCE refresh_access_token
blocks forever, it never finishes or panics
let authorize_url = incomplete_auth_code_client.get_authorize_url();
println!("AUTH: {}", authorize_url);
let (code, state) = get_query_from_user(&authorize_url, &config.spotify.callback_uri)?;
println!("CODE: {}", code);
println!("STATE: {}", state);
let user_client = incomplete_auth_code_client
.finalize(code.trim(), state.trim())
.into_report()
.change_context(VRCError::Spotify)?;
println!("Pre refresh_access_token");
user_client.refresh_access_token().unwrap();
println!("Post refresh_access_token");
AUTH: https://accounts.spotify.com/authorize?response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A2345&client_id=5e240a5b68bf49ad80163f9877b82f97&state=HmMoJLbncOflZ9Je&show_dialog=false&code_challenge_method=S256&code_challenge=2lJ7eMoGyCU95NFIx11Aj_gG5RC5ykLsWBhSG4GpEA4
CODE: AQCpz35Ui-ZWWg_3BCXg9JcpfyliBBss3PTpSOwZjTk9Es5osmx2mgPmk5K8W2wd6Yr6t2fPttnYRlp4PELbxyVxp-IBFnPEHR7n8yVvcBR1Z1CW36-Y-cEe8Xx3Tc8vrD119dvcEZrSIM83-wA7C79bq6WwfjNiMVD1SMuTIljUxXtIF2GyttjdK9sIfqG2FUZpaDlw0U7r53B9rn4rMPyx0_3s-S2loXZf7Q
STATE: HmMoJLbncOflZ9Je
Pre refresh_access_token
But it works without PKCE
EDIT: I found where to provide scopes, my bad
but even with .scopes([Scope::UserReadPlaybackState])
,
playback_state
.currently_playing_item()
.public_playing_item()
still returns None
I've confirmed that the client freezing when refreshing an access token with PKCE is a bug, and have pushed an update. I'll release a patch release soon to crates.io. 0.2.1 is now up.
If .currently_playing_item()
returns None, there is nothing playing in the user's account. If .public_playing_item()
returns None, it most likely means there is something playing but the user has a private session enabled, so the Spotify API won't tell what the playing item is (hence the name public_playing_item
).
Although, it's always a possibility my model doesn't match Spotify's response so the public playing item isn't deserialized properly, so it is left as None during the deserialization. I'd have to see what Spotify exactly responds to confirm. The library logs the API call response and deserialized body at the trace level.
Here's the tracing output, hopefully it helps output.txt
EDIT: The first currently_playing_item
isn't None but public_playing_item
and public_playing_track
are None
Seems Spotify just doesn't respond with a public playing item. I'll have to see if something similar crops up elsewhere to try and figure out if my model is incorrect.
If it helps, this is how I get it with rspotify and it successfully gets the song https://github.com/ramsayleung/rspotify/blob/2fe50355312d90d9b93d3731ecdc1dda3c569a03/src/clients/oauth.rs#L499
EDIT: This makes no sense, I re-ran the code today and it's working again, I literally haven't changed anything, I tested it last night and it wasn't working, this morning it's working...
EDIT: OH I figured it out! It only works if it's a public PLAYLIST, my liked songs are private and don't work, a different playlist works
I'm adding refresh token storing and I can't access SyncClient because it's private, to send an incomplete_auth_code_client to a function since both pkce/non-pkce result in the same code, is there a better way to do this or can Sync/AsyncClient be made public?
https://pastebin.com/bP2JikJj
The internal AsyncClient
and SyncClient
traits are hidden on purpose, since their functionality isn't relevant to the library consumer. To alleviate it, each client (including their builders and incomplete clients) have corresponding type aliases that let you use the structs without the generic type parameter. The type alias for AuthorizationCodeUserClient<SyncClient>
is SyncAuthorizationCodeUserClient
.
That worked, thanks 👍
I found the endpoint I've been using me/player/currently-playing
in Ferrispot
https://github.com/Spanfile/Ferrispot/blob/97b7e98afd9f8e0968d5d59ff993101b38aaf48a/src/client.rs#L157
But I don't see anywhere that it's used in the library, so I think public_playing_item
uses a different endpoint that only reports when playing from public playlists
EDIT: I found what uses it
let Some(track) = user_client
.currently_playing_track()
.into_report()
.change_context(VrcError::Spotify)?
else {
continue;
};
let Some(item) = track.public_playing_item() else {
continue;
};
let PlayingType::Track(full_track) = item.item() else {
continue;
};
user_client.currently_playing_track()
seems to use me/player/currently-playing
though it still only works for public playlists, I don't know if there's a difference between that and playback_state.currently_playing_item()
EDIT: I've updated my program to use Ferrispot https://github.com/ShayBox/VRC-OSC
.playback_state()
returns a PlaybackState
struct which is a superset of CurrentlyPlayingItem
returned by .currently_playing_track()
(renamed to .currently_playing_item()
in the upcoming version, you can check out what's to come in the changelog). In addition to the playing item, the playback state also includes information about the device that's playing and the player's shuffle and repeat states.
It's great to see my library is useful to someone else than just me :blush: The sync API seems to be working fine now, so I'll close this issue. If you need more help with something, or maybe need some endpoints implemented, feel free to open a new issue!
I'd like to use this over
rspotify
as this doesn't make PKCE a completely separate struct, allowing me to swap at runtime.But I need blocking, so this is just me asking 😄
Unrelated but would you consider a way to automatically handle the Spotify callback via a
![image](https://user-images.githubusercontent.com/9505196/204013888-8e243956-d174-495e-8071-9f51f960e2d9.png)
tiny_http
server andwebbrowser
?This is my existing code using
rspotify
, it could be an optional feature.