Code Monkey home page Code Monkey logo

rspotify's Introduction

Continuous Integration License Crates.io Docs

RSpotify

RSpotify is a wrapper for the Spotify Web API, inspired by spotipy. It includes support for all the authorization flows, and helper functions for all endpoints.

To learn how to use RSpotify, please refer to the documentation. There are some examples that may be useful as well.

Changelog

Please see the changelog for a release history and indications on how to upgrade from one version to another.

Contributing

If you find any problems or have suggestions about this crate, please submit an issue. Moreover, any pull request, code review and feedback are welcome.

Code Guide

We use GitHub Actions to make sure the codebase is consistent (cargo fmt) and continuously tested (cargo test). We try to keep comments at a maximum of 80 characters of length (which isn't automatically checked by cargo fmt) and code at 120.

Trait Hierarchy

Crate Hierarchy

Building

RSpotify uses maybe_async to switch between async and blocking clients, which is triggered inside Cargo.toml. So that must be taken into account when building rspotify. Read the Configuration section in the docs for more information about how to build with custom TLS implementations, and more.

client-reqwest is used by default. It should be as easy as

$ cargo build

client-ureq is also available as the blocking interface, which compiles RSpotify with ureq (a TLS has to be specified as well):

$ cargo build --no-default-features --features client-ureq,ureq-rustls-tls

Notice that you can't build rspotify with all features like this:

$ cargo build --all-features

Because in order to switch between clients, the different clients have to implement the same base trait in src/http/mod.rs, so if you build with all features, you'll get duplicate definitions error. As every coin has two sides, you can only have one side at a time, not all sides of it.

WASM support

RSpotify supports building for the wasm32-unknown-unknown target. It should be as easy as:

$ cargo build --target wasm32-unknown-unknown

Refer to the documentation for more details

License

MIT

rspotify's People

Contributors

andrzejressel avatar aome510 avatar bkitor avatar braincow avatar caass avatar channingbabb avatar dependabot[bot] avatar domwilliams0 avatar dusterthefirst avatar ekuinox avatar eladyn avatar epwalsh avatar gelendir avatar hrkfdn avatar icewind1991 avatar jacobmichels avatar koffeinflummi avatar kstep avatar ljufa avatar marekkon5 avatar marioortizmanero avatar merisbahti avatar oliveroneill avatar qluxzz avatar ramsayleung avatar rigellute avatar ritiek avatar sabrinajewson avatar sputnick1124 avatar sydpy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

rspotify's Issues

Failure is now deprecated

This appears as a direct dependency and as a dependency of an old version of dotenv.

Result of cargo audit:

warning: 1 warning found

Crate:  failure
Title:  failure is officially deprecated/unmaintained
Date:   2020-05-02
URL:    https://rustsec.org/advisories/RUSTSEC-2020-0036
Dependency tree: 
failure 0.1.8
├── rspotify 0.10.0
│   └── holding 0.1.0
└── dotenv 0.13.0
    └── rspotify 0.10.0

Disable token caching

I'm writing a web app for multiple users and would like to save the API token in my database. Is there a way to prevent .spotify_token_cache.json from being created?

"from_token" is currently not a valid parameter

Hey,

thanks for your work on the library. I'm currently trying to search for tracks using rspotify. According to the documentation, from_token can be passed as a market parameter to search in the market tied to the token.

However, the search_* functions seem to require an Option parameter. There seems to be no Country enum representing from_token, unless I am missing something?

Introduce automatic token retrieval

This is a feature request.

This project works perfectly, but as I'm trying to create application where user interacts only with generated GUI, i'm in no way capable of providing the URL user is redirected to after allowing application to use their data - because command line is off the limits, and this is hardcoded into the library ("Enter the URL you were redirected to: ")

A very simple solution would be implementing http server going with localhost:, where user is redirected to and application is just reading the data off the request directly (just like in Listing 20-2 on rust cookbook).

If this is already implemented/has a possible workaround without rebuilding entire library, please let me know - I'll close the issue immediately.

Cleaning up the blocking module

This is a continuation of #102, which discussed how to properly export the blocking module without copy-pasting the async functions and removing async and await. The current solution makes it painful to maintain rspotify, and compilation times when using the blocking client are huge because you're basically compiling rspotify twice, which kinda defeats the point of #108.

I first thought that using an attribute macro could be a good idea because all that needs to be modified from the async implementations is removing the async and await keywords. See this comment for a demonstration. But this still doesn't fix the "compiling the codebase twice" issue, because the code would still be repeated, now after the macro is ran instead of manually. The macro in question would make compilation times even longer, and could be messy to implement. The only possible way to only "compile the codebase once" would be by having the blocking feature only export the blocking interface when used, but I'm not sure we can assume that the users won't need both the async and blocking interfaces for a program.

The second possible solution is calling the async implementations in the blocking functions using block_on. This might sound less efficient at first, but here's a comparison I made to prove my point:

use std::time::Instant;

async fn original() -> Result<String, reqwest::Error> {
    reqwest::get("https://www.rust-lang.org")
        .await?
        .text()
        .await
}

fn with_copypaste() -> Result<String, reqwest::Error> {
    reqwest::blocking::get("https://www.rust-lang.org")?
        .text()
}

fn with_block_on() -> Result<String, reqwest::Error> {
    let mut rt = tokio::runtime::Builder::new()
        .basic_scheduler()
        .enable_all()
        .build()
        .unwrap();

    rt.block_on(async move {
        original().await
    })
}

fn main() {
    let now = Instant::now();
    print!("benchmarking with copypaste ...");
    for _ in 1..100 {
        with_copypaste().unwrap();
    }
    println!("done in {}ms", now.elapsed().as_millis());

    let now = Instant::now();
    print!("benchmarking with block_on ...");
    for _ in 1..100 {
        with_block_on().unwrap();
    }
    println!("done in {}ms", now.elapsed().as_millis());
}
❯ cargo run --release
    Finished release [optimized] target(s) in 0.04s
     Running `/home/mario/.cache/cargo/release/macros`
benchmarking with copypaste ...done in 31988ms
benchmarking with block_on ...done in 30250ms

What? block_on was just as fast as the copypaste? Turns out that this is more or less what reqwest does with its own blocking module!. So both the copypaste and the runtime solutions are basically doing the same. This way we don't even need the blocking feature in reqwest, and rspotify will only be "compiled once".

This solution could use a smaller macro that automatically generates the boilerplate block_on functions to avoid some repetition.

The best part is that the example isn't even properly optimized. It generates a runtime every time the function is called. If it were global, it would only have to be initialized once. Here's a more complex and improvable (since it doesn't seem to be actually faster for some reason) version with lazy_static:

use std::time::Instant;
use std::sync::{Arc, Mutex};

use lazy_static::lazy_static;
use tokio::runtime;

async fn original() -> Result<String, reqwest::Error> {
    reqwest::get("https://www.rust-lang.org")
        .await?
        .text()
        .await
}

fn with_copypaste() -> Result<String, reqwest::Error> {
    reqwest::blocking::get("https://www.rust-lang.org")?
        .text()
}

lazy_static! {
    // Mutex to have mutable access and Arc so that it's thread-safe.
    static ref RT: Arc<Mutex<runtime::Runtime>> = Arc::new(Mutex::new(runtime::Builder::new()
        .basic_scheduler()
        .enable_all()
        .build()
        .unwrap()));
}

fn with_block_on() -> Result<String, reqwest::Error> {
    RT.lock().unwrap().block_on(async move {
        original().await
    })
}

fn main() {
    let now = Instant::now();
    print!("benchmarking with copypaste ...");
    for _ in 1..100 {
        with_copypaste().unwrap();
    }
    println!("done in {}ms", now.elapsed().as_millis());

    let now = Instant::now();
    print!("benchmarking with block_on ...");
    for _ in 1..100 {
        with_block_on().unwrap();
    }
    println!("done in {}ms", now.elapsed().as_millis());
}
❯ cargo run --release
   Compiling macros v0.1.0 (/tmp/macros)
    Finished release [optimized] target(s) in 2.42s
     Running `/home/mario/.cache/cargo/release/macros`
benchmarking with copypaste ...done in 30172ms
benchmarking with block_on ...done in 30606ms

Implement new playlist endpoints

Few months ago Spotify added changes to playlists: https://developer.spotify.com/community/news/2018/06/12/changes-to-playlist-uris/.

To implement

  • GET /v1/playlists/{playlist_id}/tracks

  • POST /v1/playlists/{playlist_id}/tracks

  • PATCH /v1/playlists/{playlist_id}/tracks

  • PUT /v1/playlists/{playlist_id}/tracks

  • DELETE /v1/playlists/{playlist_id}/tracks

  • GET /v1/playlists/{playlist_id} (PR)

  • PUT /v1/playlists/{playlist_id}

  • POST /v1/me/playlists

  • GET /v1/playlists/{playlist_id}/images

  • PUT /v1/playlists/{playlist_id}/images

Periodic API tests

It would be a good idea to have a GitHub action with a cronjob that periodically (say, once a day) checks that the endpoints work. This would let us know about changes in the Spotify API very early and give us more time to apply them. Here's how tekore does it.

Explicitly read from the environment variables with `from_env`

Calling SpotifyClientCredentials::default() automatically tries to read the environment values for CLIENT_ID and CLIENT_SECRET. I think this shouldn't be the default behavior, meaning that the current default method should become from_env, and have a default method that simply sets the values to None.

This is also done by the tekore library, for example.

This should be done after #109 is taken care of. I have to check if any other structs apart from SpotifyClientCredentials do anything similar.

rspotify fails to build with blocking feature

Reqwest has blocking set as an optional feature and is off by default. When trying to build rspotify with the blocking feature you get error messages similar to the following:

error[E0432]: unresolved import `reqwest::blocking`
 --> src/blocking/client.rs:5:14
  |
5 | use reqwest::blocking::Client;
  |              ^^^^^^^^ could not find `blocking` in `reqwest`

error[E0432]: unresolved import `reqwest::blocking`
 --> src/blocking/oauth2.rs:6:14
  |
6 | use reqwest::blocking::Client;
  |              ^^^^^^^^ could not find `blocking` in `reqwest`

error[E0433]: failed to resolve: could not find `blocking` in `reqwest`
  --> src/blocking/client.rs:65:21
   |
65 | impl From<&reqwest::blocking::Response> for ApiError {
   |                     ^^^^^^^^ could not find `blocking` in `reqwest`

error[E0433]: failed to resolve: could not find `blocking` in `reqwest`
  --> src/blocking/client.rs:66:33
   |
66 |     fn from(response: &reqwest::blocking::Response) -> Self {
   |                                 ^^^^^^^^ could not find `blocking` in `reqwest`

error[E0599]: no method named `request` found for type `blocking::client::CLIENT` in the current scope
   --> src/blocking/client.rs:153:34
    |
38  | / lazy_static! {
39  | |     /// HTTP Client
40  | |     pub static ref CLIENT: Client = Client::new();
41  | | }
    | |_- method `request` not found for this
...
153 |               let builder = CLIENT.request(method, &url.into_owned()).headers(headers);
    |                                    ^^^^^^^ method not found in `blocking::client::CLIENT`
    |
    = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

Update Reqwest

It looks like the issue behind #55 has been fixed in Reqwest 0.9.22 (seanmonstar/reqwest#541 (comment))

Updating to this version (or newer) will also cause it to use Rustls 0.16, which fixes some dependency collision issues I have been having with this and serenity.

ETag

A feature that would be handy would be the support of ETags, as detailed in Spotify's docs:

Conditional Requests

Most API responses contain appropriate cache-control headers set to assist in client-side caching:

  • If you have cached a response, do not request it again until the response has expired.
  • If the response contains an ETag, set the If-None-Match request header to the ETag value.
  • If the response has not changed, the Spotify service responds quickly with 304 Not Modified status, meaning that your cached version is still good and your application should use it.

I'm happy to work on this and submit a pull request, maybe as an optional --feature?

Null-value for album_type prevents parsing

I have already reported this upstream. but I doubt it'll be addressed anytime soon, if at all.

It seems that, unlike stated by the documentation, album_type can be null for local tracks. This causes parsing errors in rspotify. We could either transform album_type in our struct to an Option, or set it to an empty string for these cases. I'm guessing the latter would be better as it won't break API, but the former seems more correct?

Environmental variables should have the prefix `SPOTIFY_`

The environmental variable names currently are CLIENT_ID, CLIENT_SECRET and REFRESH_TOKEN. I think it would be a better idea to use these same names with the prefix SPOTIFY_ to avoid name collisions with other programs/libraries that use OAuth2, like SPOTIFY_CLIENT_ID.

By the way, I'll leave for 3-4 days so I won't be able to respond to issues/PRs.

give up playback | disconnect

I know that it is possible to give the playback to another device.

But it would be nice if was possible to work in a slightly different way: give up on playback control (or just disconnect).

why is this different from giving playback to any other device?:

  • when you have only the "rspotify device" (that is, only one) running, you cannot transfer the playback (because there are no other devices to transfer to),
    but it makes sense to throw away the playback control (or just disconnects).

Maybe i am talking nonsense here, or just requesting something that already exists, since i've come from searching this spotifyd repository, and saw that it was missing the Stop() Mpris implementation, because there was no explicit disconnect implementation from rspotify.
So i have near zero experience with the rspotify.

Issue when using corporate proxy

I'm using ncspot which uses rspotify, that's why I open the issue here. I'm facing an error when behind a proxy. I'm using the http_proxy, https_proxy env variables. From the logs I can see that other network tasks work in ncspot, like authentication and token handout, but rspotify fails to connect...

Since #59 is closed, I assume it's an unrealted issue...? But it seems that it has something to do with reqwest as well.

thread 'main' panicked at 'called Result::unwrap() on an Err value: reqwest::Error { kind: Request, url: "https://api.spotify.com/v1/me/", source: hyper::Error(Connect, Os { code: 10054, kind: ConnectionReset, message: "An existing connection was forcibly closed by the remote host." }) }', src\libcore\result.rs:1165:5
stack backtrace:
0: backtrace::backtrace::dbghelp::trace
at C:\Users\VssAdministrator.cargo\registry\src\github.com-1ecc6299db9ec823\backtrace-0.3.40\src\backtrace/dbghelp.rs:88
1: backtrace::backtrace::trace_unsynchronized
at C:\Users\VssAdministrator.cargo\registry\src\github.com-1ecc6299db9ec823\backtrace-0.3.40\src\backtrace/mod.rs:66
2: std::sys_common::backtrace::_print_fmt
at src\libstd\sys_common/backtrace.rs:77
3: <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt
at src\libstd\sys_common/backtrace.rs:61
4: core::fmt::write
at src\libcore\fmt/mod.rs:1028
5: std::io::Write::write_fmt
at src\libstd\io/mod.rs:1412
6: std::sys_common::backtrace::_print
at src\libstd\sys_common/backtrace.rs:65
7: std::sys_common::backtrace::print
at src\libstd\sys_common/backtrace.rs:50
8: std::panicking::default_hook::{{closure}}
at src\libstd/panicking.rs:188
9: std::panicking::default_hook
at src\libstd/panicking.rs:205
10: std::panicking::rust_panic_with_hook
at src\libstd/panicking.rs:464
11: std::panicking::continue_panic_fmt
at src\libstd/panicking.rs:373
12: rust_begin_unwind
at src\libstd/panicking.rs:302
13: core::panicking::panic_fmt
at src\libcore/panicking.rs:139
14: core::result::unwrap_failed
at src\libcore/result.rs:1165
15: core::result::Result<T,E>::unwrap
at /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14\src\libcore/result.rs:933
16: rspotify::spotify::client::Spotify::internal_call
at C:\Users\jonas.frei.cargo\registry\src\github.com-1ecc6299db9ec823\rspotify-0.8.0\src\spotify/client.rs:163
17: rspotify::spotify::client::Spotify::get
at C:\Users\jonas.frei.cargo\registry\src\github.com-1ecc6299db9ec823\rspotify-0.8.0\src\spotify/client.rs:189
18: rspotify::spotify::client::Spotify::me
at C:\Users\jonas.frei.cargo\registry\src\github.com-1ecc6299db9ec823\rspotify-0.8.0\src\spotify/client.rs:960
19: rspotify::spotify::client::Spotify::current_user
at C:\Users\jonas.frei.cargo\registry\src\github.com-1ecc6299db9ec823\rspotify-0.8.0\src\spotify/client.rs:966

Parsing fails when searching by genre

RecommendationsSeed should have an Option<String>. From the Spotify API Doc's:

A link to the full track or artist data for this seed. For tracks this will be a link to a Track Object. For artists a link to an Artist Object. For genre seeds, this value will be null.

Getting bad request

I recently started getting bad request from all the requests. I can reproduce it with the example in the README with brand new client id and secret.
This is the response I get:

response: Response { url: "https://api.spotify.com/v1/me/player/recently-played?limit=10&", status: BadRequest, headers: {"Content-Type": "text/html; charset=UTF-8", "Referrer-Policy": "no-referrer", "Content-Length": "1555", "Date": "Fri, 18 May 2018 21:49:43 GMT"} }
content: "<!DOCTYPE html>\n<html lang=en>\n  <meta charset=utf-8>\n  <meta name=viewport content=\"initial-scale=1, minimum-scale=1, width=device-width\">\n  <title>Error 400 (Bad Request)!!1</title>\n  <style>\n    *{margin:0;padding:0}html,code{font:15px/22px arial,sans-serif}html{background:#fff;color:#222;padding:15px}body{margin:7% auto 0;max-width:390px;min-height:180px;padding:30px 0 15px}* > body{background:url(//www.google.com/images/errors/robot.png) 100% 5px no-repeat;padding-right:205px}p{margin:11px 0 22px;overflow:hidden}ins{color:#777;text-decoration:none}a img{border:0}@media screen and (max-width:772px){body{background:none;margin-top:0;max-width:none;padding-right:0}}#logo{background:url(//www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png) no-repeat;margin-left:-5px}@media only screen and (min-resolution:192dpi){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat 0% 0%/100% 100%;-moz-border-image:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) 0}}@media only screen and (-webkit-min-device-pixel-ratio:2){#logo{background:url(//www.google.com/images/branding/googlelogo/2x/googlelogo_color_150x54dp.png) no-repeat;-webkit-background-size:100% 100%}}#logo{display:inline-block;height:54px;width:150px}\n  </style>\n  <a href=//www.google.com/><span id=logo aria-label=Google></span></a>\n  <p><b>400.</b> <ins>That’s an error.</ins>\n  <p>Your client has issued a malformed or illegal request.  <ins>That’s all we know.</ins>\n"
Err(Error(Msg("convert result failed, content \"\""), State { next_error: Some(Error("EOF while parsing a value", line: 1, column: 0)), backtrace: None })

Displays authentication warning message even after successful authentication

Hi, thanks for this cool library.

I launched the album_tracks.rs example and it works fine but it displays this warning:

    You need to set your Spotify API credentials. You can do this by
    setting environment variables in `.env` file:
    CLIENT_ID='your-spotify-client-id'
    CLIENT_SECRET='your-spotify-client-secret'
    REDIRECT_URI='your-app-redirect-url'
    Get your credentials at `https://developer.spotify.com/my-applications`

I tried it both with exporting CLIENT_ID and CLIENT_SECRET environment variables, and also setting them explicitly in my test crate but it still displays this warning.

Complete output
SpotifyClientCredentials.default(): client_id:"mah_client_id", client_secret:"mah_client_secret"
SpotifyClientCredentials.default(): client_id:"mah_client_id", client_secret:"mah_client_secret" empty_flag:true

    You need to set your Spotify API credentials. You can do this by
    setting environment variables in `.env` file:
    CLIENT_ID='your-spotify-client-id'
    CLIENT_SECRET='your-spotify-client-secret'
    REDIRECT_URI='your-app-redirect-url'
    Get your credentials at `https://developer.spotify.com/my-applications`
Ok(Page { href: "https://api.spotify.com/v1/albums/6akEvsycLGftJxYudPjmqK/tracks?offset=0&limit=2", items: [SimplifiedTrack { artists: [SimplifiedArtist { external_urls:
{"spotify": "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q"}, href: "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q", id: "08td7MxkoHQkXnWAYD8d6Q", name: "Tania Bowra", _type: artist, uri: "spotify:artist:08td7MxkoHQkXnWAYD8d6Q" }], available_markets: Some(["AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "ID", "IE", "IL", "IS", "IT", "JP", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SK", "SV", "TH", "TR", "TW", "US", "UY", "VN", "ZA"]), disc_number: 1, duration_ms: 276773, explicit: false, external_urls: {"spotify": "https://open.spotify.com/track/2TpxZ7JUBn3uw46aR7qd6V"}, href: "https://api.spotify.com/v1/tracks/2TpxZ7JUBn3uw46aR7qd6V", id: "2TpxZ7JUBn3uw46aR7qd6V", name: "All I Want", preview_url: Some("https://p.scdn.co/mp3-preview/12b8cee72118f995f5494e1b34251e4ac997445e?cid=feebf12513c448a383739f9fdadc7dbc"), track_number: 1, _type: track, uri: "spotify:track:2TpxZ7JUBn3uw46aR7qd6V" }, SimplifiedTrack { artists: [SimplifiedArtist { external_urls: {"spotify": "https://open.spotify.com/artist/08td7MxkoHQkXnWAYD8d6Q"}, href: "https://api.spotify.com/v1/artists/08td7MxkoHQkXnWAYD8d6Q", id: "08td7MxkoHQkXnWAYD8d6Q", name: "Tania Bowra", _type: artist, uri: "spotify:artist:08td7MxkoHQkXnWAYD8d6Q" }], available_markets: Some(["AD", "AR", "AT", "AU", "BE", "BG", "BO", "BR", "CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "EC", "EE", "ES", "FI", "FR", "GB", "GR", "GT", "HK", "HN", "HU", "ID", "IE", "IL", "IS", "IT", "JP", "LI", "LT", "LU", "LV", "MC", "MT", "MX", "MY", "NI", "NL", "NO", "NZ", "PA", "PE", "PH", "PL", "PT", "PY", "RO", "SE", "SG", "SK", "SV", "TH", "TR", "TW", "US", "UY", "VN", "ZA"]), disc_number: 1, duration_ms: 247680, explicit: false, external_urls: {"spotify": "https://open.spotify.com/track/4PjcfyZZVE10TFd9EKA72r"}, href: "https://api.spotify.com/v1/tracks/4PjcfyZZVE10TFd9EKA72r", id: "4PjcfyZZVE10TFd9EKA72r", name: "Someday", preview_url: Some("https://p.scdn.co/mp3-preview/4a54d83c195d0bc17b1b23fc931d37fb363224d8?cid=feebf12513c448a383739f9fdadc7dbc"), track_number: 2, _type: track, uri: "spotify:track:4PjcfyZZVE10TFd9EKA72r" }], limit: 2, next: Some("https://api.spotify.com/v1/albums/6akEvsycLGftJxYudPjmqK/tracks?offset=2&limit=2"), offset: 0, previous: None, total: 11 })

Is this warning expected or does this needs fixing?

Error during device listing

I get an error when I try to:

let devices = spotify.device();
println!("{:?}", devices);
ErrorMessage { msg: "convert result failed, content \"{\\n  \\\"devices\\\" : [ {\\n    \\\"id\\\" : \\\"1b6e8429877f28aeb2f0af52567e9b9224319d3a\\\",\\n    \\\"is_active\\\" : false,\\n    \\\"is_private_session\\\" : false,\\n    \\\"is_restricted\\\" : false,\\n    \\\"name\\\" : \\\"kBook Pro\\\",\\n    \\\"type\\\" : \\\"Computer\\\",\\n    \\\"volume_percent\\\" : 64\\n  }, {\\n    \\\"id\\\" : \\\"aaea1a3c3778e6d17b90170c5ccb4e2988e847e7\\\",\\n    \\\"is_active\\\" : true,\\n    \\\"is_private_session\\\" : false,\\n    \\\"is_restricted\\\" : false,\\n    \\\"name\\\" : \\\"Theater + Kitchen + Bathroom + Livi + Living Room\\\",\\n    \\\"type\\\" : \\\"Unknown\\\",\\n    \\\"volume_percent\\\" : 25\\n  }, {\\n    \\\"id\\\" : \\\"bc185b9d6dce9dca3994523d627c3597da3038dc\\\",\\n    \\\"is_active\\\" : false,\\n    \\\"is_private_session\\\" : false,\\n    \\\"is_restricted\\\" : false,\\n    \\\"name\\\" : \\\"kofBook\\\",\\n    \\\"type\\\" : \\\"Computer\\\",\\n    \\\"volume_percent\\\" : 67\\n  }, {\\n    \\\"id\\\" : \\\"bdc21246944b2854b6026e541ef7df46cee4db08\\\",\\n    \\\"is_active\\\" : false,\\n    \\\"is_private_session\\\" : false,\\n    \\\"is_restricted\\\" : false,\\n    \\\"name\\\" : \\\"Living Room\\\",\\n    \\\"type\\\" : \\\"Unknown\\\",\\n    \\\"volume_percent\\\" : 20\\n  } ]\\n}\"" }

The response of curl -X "GET" "https://api.spotify.com/v1/me/player/devices" -H "Accept: application/json" -H "Content-Type: application/json" -H "Authorization: Bearer ***" is:

{
  "devices": [
    {
      "id": "1b6e8429877f28aeb2f0af52567e9b9224319d3a",
      "is_active": false,
      "is_private_session": false,
      "is_restricted": false,
      "name": "kBook Pro",
      "type": "Computer",
      "volume_percent": 64
    },
    {
      "id": "aaea1a3c3778e6d17b90170c5ccb4e2988e847e7",
      "is_active": true,
      "is_private_session": false,
      "is_restricted": false,
      "name": "Theater + Kitchen + Bathroom + Livi + Living Room",
      "type": "Unknown",
      "volume_percent": 25
    },
    {
      "id": "bc185b9d6dce9dca3994523d627c3597da3038dc",
      "is_active": false,
      "is_private_session": false,
      "is_restricted": false,
      "name": "kofBook",
      "type": "Computer",
      "volume_percent": 67
    },
    {
      "id": "bdc21246944b2854b6026e541ef7df46cee4db08",
      "is_active": false,
      "is_private_session": false,
      "is_restricted": false,
      "name": "Living Room",
      "type": "Unknown",
      "volume_percent": 20
    }
  ]
}

Any idea what went wrong here?

Automatic Re-authentication

Hey, thanks for the awesome library!

Is there a good way to automatically re-authenticate the SpotifyClient?

Currently I create the client using the OAuth pattern that you have in the README.
However, the authentication only lasts for an hour, and I have to restart my program in order to re-authenticate.

Is there a good way to do this automatically? Sorry if the question doesn't make sense, and thank you again for rspotify.

Using proxy

Hey there!

I tried to use spotify-tui (which apparently uses this library) inside my company, where everything runs behind a proxy. After a little digging, I noticed they were trying to get the json token for oauth (https://github.com/ramsayleung/rspotify/blob/master/src/spotify/oauth2.rs), but the client from reqwest is being built without the proxy.

They have a good doc on it (https://docs.rs/reqwest/0.7.0/reqwest/struct.Proxy.html), so I was able to change the Client building line the proxy.

Question is: do you guys plan to have any take on that? I might be able to make a PR if that is not/not supposed to be in the roadmap or anything.

Thanks!

The workload of adding support for `async/await`.

I have refactored the core code of Rspotify to support async/await, and refactored the project structure to shorten import path either. Now the foundation is done, it's time to build the roof, and it'll be great if you have time to help me do this:
The following endpoint functions need to be asynced/awaited before Rspotify can take advantage of async/await, check this branch: ramsay/support-for-async-await for more details:

  • artist
  • artists
  • artist_albums
  • artists
  • artist_albums
  • artist_top_tracks
  • artist_related_artists
  • album
  • albums
  • search_album
  • search_artist
  • search_track
  • search_playlist
  • album_track
  • user
  • playlist
  • current_user_playlists
  • user_playlists
  • user_playlist
  • user_playlist_tracks
  • user_playlist_create
  • user_playlist_change_detail
  • user_playlist_unfollow
  • user_playlist_add_tracks
  • user_playlist_replace_tracks
  • user_playlist_recorder_tracks
  • user_playlist_remove_all_occurrences_of_tracks
  • user_playlist_remove_specific_occurrenes_of_tracks
  • user_playlist_follow_playlist
  • user_playlist_check_follow
  • me
  • current_user
  • current_user_playing_track
  • current_user_saved_albums
  • current_user_saved_tracks
  • current_user_followed_artists
  • current_user_saved_tracks_delete
  • current_user_saved_tracks_contains
  • current_user_saved_tracks_add
  • current_user_top_artists
  • current_user_top_tracks
  • current_user_recently_played
  • current_user_saved_albums_add
  • current_user_saved_albums_delete
  • current_user_saved_albums_contains
  • user_follow_artists
  • user_unfollow_artists
  • user_artist_check_follow
  • user_follow_users
  • user_unfollow_users
  • featured_playlists
  • new_releases
  • categories
  • recommendations
  • audio_features
  • audios_features
  • audio_analysis
  • device
  • current_playback
  • current_playing
  • transfer_playback
  • start_playback
  • pause_playback
  • next_track
  • previous_track
  • seek_track
  • repeat
  • volume
  • shuffle

the following examples need to update to async/await version, for example:

extern crate rspotify;

use futures;
use rspotify::client::Spotify;
use rspotify::oauth2::SpotifyClientCredentials;
use tokio;

#[tokio::main]
async fn main() {
    let mut handlers = vec![];
    for _ in 0..20 {
        let handler = tokio::spawn(async move {
            // Set client_id and client_secret in .env file or
            // export CLIENT_ID="your client_id"
            // export CLIENT_SECRET="secret"
            let client_credential = SpotifyClientCredentials::default().build();

            // Or set client_id and client_secret explictly
            // let client_credential = SpotifyClientCredentials::default()
            //     .client_id("this-is-my-client-id")
            //     .client_secret("this-is-my-client-secret")
            //     .build();
            let spotify = Spotify::default()
                .client_credentials_manager(client_credential)
                .build();
            let birdy_uri = "spotify:artist:2WX2uTcsvV5OnS0inACecP";
            let artist = spotify.artist(birdy_uri).await;
            println!("{:?}", artist);
            return;
        });
        handlers.push(handler);
    }
    futures::future::join_all(handlers).await;
}
  • artists
  • track
  • artists_albums
  • artist_related_artists
  • albums
  • audios_features
  • audio_analysis
  • album_tracks
  • audio_features
  • artist
  • album

the following example need to move to blocking directory, and explicitly set required-feature=["blocking"], for example:

[[example]]
name = "device"
required-features = ["blocking"]
path = "examples/blocking/device.rs"
  • device
  • artist_top_tracks
  • categories
  • current_playback
  • current_playing
  • current_user_followed_artists
  • current_user_playing_track
  • current_user_playlists
  • current_user_recently_played
  • current_user_saved_albums_add
  • current_user_saved_albums_contains
  • current_user_saved_albums_delete
  • current_user_saved_albums
  • current_user_saved_tracks_add
  • current_user_saved_tracks_contains
  • current_user_saved_tracks_delete
  • current_user_saved_tracks
  • current_user_top_artists
  • current_user_top_tracks
  • featured_playlists
  • me
  • new_releases
  • next_playback
  • pause_playback
  • playlist
  • previous_playback
  • recommendations
  • repeat
  • search_album
  • search_artist
  • search_playlist
  • search_track
  • seek_track
  • shuffle
  • start_playback
  • transfer_playback
  • user_artist_check_follow
  • user_follow_artists
  • user_follow_users
  • user_playlist_add_tracks
  • user_playlist_change_detail
  • user_playlist_check_follow
  • user_playlist_create
  • user_playlist_follow_playlist
  • user_playlist_recorder_tracks
  • user_playlist_remove_all_occurrences_of_tracks
  • user_playlist_remove_specific_occurrenes_of_tracks
  • user_playlist_replace_tracks
  • user_playlist
  • user_playlists
  • user_playlist_tracks
  • user_playlist_unfollow
  • user_unfollow_artists
  • user_unfollow_users
  • volume

Clean up and re-structure the errors

The ClientError type currently defined for the next release could definitely be improved. Some of its variants are repetitive and confusing. I kept adding variants for the rewrite without thinking much about the structure because the important thing was to get the rewrite done. But I would like to discuss what it should be like.

Here's more or less the current definition:

#[derive(Debug, Error)]
pub enum ClientError {
    #[error("invalid client authentication: {0}")]
    InvalidAuth(String),

    #[error("request unauthorized")]
    Unauthorized,

    #[error("exceeded request limit")]
    RateLimited(Option<usize>),

    #[error("request error: {0}")]
    Request(String),

    #[error("status code {0}: {1}")]
    StatusCode(u16, String),

    #[error("spotify error: {0}")]
    API(#[from] APIError),

    #[error("json parse error: {0}")]
    ParseJSON(#[from] serde_json::Error),

    #[error("url parse error: {0}")]
    ParseURL(#[from] url::ParseError),

    #[error("input/output error: {0}")]
    IO(#[from] std::io::Error),

    #[cfg(feature = "cli")]
    #[error("cli error: {0}")]
    CLI(String),

    #[error("cache file error: {0}")]
    CacheFile(String),
}

Some thoughts:

  • StatusCode, Request, RateLimited and Unauthorized could be wrapped under the same variant. These are just possible HTTP errors, so something like HTTPError could do fine. I understand that having Unauthorized and such can be useful to match against these specific errors, but if HTTPError was defined properly it wouldn't be necessary (say, using http::status::StatusCode, for example).
  • Maybe CLI should be renamed to UserError? It describes better its purpose.
  • Is CacheFile necessary? I don't see it used anywhere, and it could be replaced with IO.
  • Should ParseURL and ParseJSON be wrapped under a Parse variant? Maybe that's unnecessary?
  • Should InvalidAuth be renamed to something different? It might be confusing when compared to Unauthorized, but InvalidAuth means that Spotify wasn't initialized correctly (say, there's no token when it's needed). Maybe MissingAuth? IDK
  • None of these errors are documented (with ///). They should include a comment with a brief description.

Please let me know any other problems you see and if you'd like to change something else.

invalid value: integer `1520543558700`, expected u32

Hi!

Thanks for this library, it's really useful!

Today I tried to get the current playing track using sp.current_user_playing_track(). This used to work fine, but today I get:

State { next_error: Some(ErrorImpl { code: Message("invalid value: integer `1520543558700`, expected u32"), line: 2, column: 29 }), backtrace: None }

in my log. I guess spotify changed something.

Add support for `async/await`

I tried to add support for async/await in my branch. I added an example (examples/async-artist-top-tracks) for it and went well.

If you have no future or ongoing plans to add support for async/await, please try my branch and leave me feedback.

Using `derive_builder` to avoid repetition with the builder pattern

When I was reading the code in src/oauth.rs and src/client.rs I couldn't help but notice all the boilerplate and repetition needed to implement builder patterns for some of the structs declared.

I think this would be a great opportunity to use the derive_builder crate in order to have the codebase more maintainable, easier to read, and consistent. Read more about the features it provides here.

The downside is possibly longer compilation times, but after improving this part in the latest commits I wouldn't think it's much of a problem. And I would like to compare the compilation times before and after the changes before this is merged into master to see how much of a difference there actually is.

Furthermore, using From<String> instead of &str for the builder parameters like this one would avoid an extra allocation for whenever the user passes a String, since the to_owned() can be omitted, and it will be directly moved into the struct. If the user passes a &str it will just be cloned. This is also supported by derive_builder.

search_track fails when called with `market` set

Using the search_track method and specifying a market is currently giving an error - such as in the examples/search_track.rs:

https://github.com/samrayleung/rspotify/blob/f8463a302f1df9393cf4e05a27252ffd2b3ff2c3/examples/search_track.rs#L35

Error is:

search result:Err(ErrorMessage { msg: "convert result failed, content \"{\\n  \\\"tracks\\\" : {\\n    \\\"href\\\" : \\\"https://api.spotify.com/v1/search?query=abba&type=track&market=US&offset=0&limit=10\\\",\\n    \\\"items\\\" : [ {\\n      \\\"album\\\" : {\\n        \\\"album_type\\\" : \\\"album\\\",\\n        \\\"artists\\\" : [ {\\n          \\\"external_urls\\\" : {\\n            \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n          },\\n          \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"name\\\" : \\\"ABBA\\\",\\n          \\\"type\\\" : \\\"artist\\\",\\n          \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        } ],\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/album/1M4anG49aEs4YimBdj96Oy\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/albums/1M4anG49aEs4YimBdj96Oy\\\",\\n        \\\"id\\\" : \\\"1M4anG49aEs4YimBdj96Oy\\\",\\n        \\\"images\\\" : [ {\\n          \\\"height\\\" : 575,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/361387b8f4297afda0394896aeb1f315b3fbad0b\\\",\\n          \\\"width\\\" : 640\\n        }, {\\n          \\\"height\\\" : 270,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/803de302874cdff3ecbd531ecfd7585793c6b7c1\\\",\\n          \\\"width\\\" : 300\\n        }, {\\n          \\\"height\\\" : 57,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/c43883a6978668e0636d96887cf43ddbb6b5965d\\\",\\n          \\\"width\\\" : 63\\n        } ],\\n        \\\"name\\\" : \\\"Arrival\\\",\\n        \\\"release_date\\\" : \\\"1976\\\",\\n        \\\"release_date_precision\\\" : \\\"year\\\",\\n        \\\"total_tracks\\\" : 12,\\n        \\\"type\\\" : \\\"album\\\",\\n        \\\"uri\\\" : \\\"spotify:album:1M4anG49aEs4YimBdj96Oy\\\"\\n      },\\n      \\\"artists\\\" : [ {\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"name\\\" : \\\"ABBA\\\",\\n        \\\"type\\\" : \\\"artist\\\",\\n        \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n      } ],\\n      \\\"disc_number\\\" : 1,\\n      \\\"duration_ms\\\" : 230693,\\n      \\\"explicit\\\" : false,\\n      \\\"external_ids\\\" : {\\n        \\\"isrc\\\" : \\\"SEAYD7601020\\\"\\n      },\\n      \\\"external_urls\\\" : {\\n        \\\"spotify\\\" : \\\"https://open.spotify.com/track/4NtUY5IGzHCaqfZemmAu56\\\"\\n      },\\n      \\\"href\\\" : \\\"https://api.spotify.com/v1/tracks/4NtUY5IGzHCaqfZemmAu56\\\",\\n      \\\"id\\\" : \\\"4NtUY5IGzHCaqfZemmAu56\\\",\\n      \\\"is_local\\\" : false,\\n      \\\"is_playable\\\" : true,\\n      \\\"name\\\" : \\\"Dancing Queen\\\",\\n      \\\"popularity\\\" : 69,\\n      \\\"preview_url\\\" : null,\\n      \\\"track_number\\\" : 2,\\n      \\\"type\\\" : \\\"track\\\",\\n      \\\"uri\\\" : \\\"spotify:track:4NtUY5IGzHCaqfZemmAu56\\\"\\n    }, {\\n      \\\"album\\\" : {\\n        \\\"album_type\\\" : \\\"album\\\",\\n        \\\"artists\\\" : [ {\\n          \\\"external_urls\\\" : {\\n            \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n          },\\n          \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"name\\\" : \\\"ABBA\\\",\\n          \\\"type\\\" : \\\"artist\\\",\\n          \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        } ],\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/album/0zdZSyxWaYmaRMPeUHcG1K\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/albums/0zdZSyxWaYmaRMPeUHcG1K\\\",\\n        \\\"id\\\" : \\\"0zdZSyxWaYmaRMPeUHcG1K\\\",\\n        \\\"images\\\" : [ {\\n          \\\"height\\\" : 575,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/9a06fe3059762156cff0ea02053592bb625f9350\\\",\\n          \\\"width\\\" : 640\\n        }, {\\n          \\\"height\\\" : 270,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/51bba354b425161d4f07a35a14bb4971646892a0\\\",\\n          \\\"width\\\" : 300\\n        }, {\\n          \\\"height\\\" : 57,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/a33d19704f08f5651000c866d7fb00117d3af44d\\\",\\n          \\\"width\\\" : 63\\n        } ],\\n        \\\"name\\\" : \\\"Voulez-Vous\\\",\\n        \\\"release_date\\\" : \\\"1979\\\",\\n        \\\"release_date_precision\\\" : \\\"year\\\",\\n        \\\"total_tracks\\\" : 13,\\n        \\\"type\\\" : \\\"album\\\",\\n        \\\"uri\\\" : \\\"spotify:album:0zdZSyxWaYmaRMPeUHcG1K\\\"\\n      },\\n      \\\"artists\\\" : [ {\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"name\\\" : \\\"ABBA\\\",\\n        \\\"type\\\" : \\\"artist\\\",\\n        \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n      } ],\\n      \\\"disc_number\\\" : 1,\\n      \\\"duration_ms\\\" : 292571,\\n      \\\"explicit\\\" : false,\\n      \\\"external_ids\\\" : {\\n        \\\"isrc\\\" : \\\"SEAYD7901120\\\"\\n      },\\n      \\\"external_urls\\\" : {\\n        \\\"spotify\\\" : \\\"https://open.spotify.com/track/2PzCOP5Aj9SABiBgNEZ52G\\\"\\n      },\\n      \\\"href\\\" : \\\"https://api.spotify.com/v1/tracks/2PzCOP5Aj9SABiBgNEZ52G\\\",\\n      \\\"id\\\" : \\\"2PzCOP5Aj9SABiBgNEZ52G\\\",\\n      \\\"is_local\\\" : false,\\n      \\\"is_playable\\\" : true,\\n      \\\"name\\\" : \\\"Gimme! Gimme! Gimme! (A Man After Midnight)\\\",\\n      \\\"popularity\\\" : 61,\\n      \\\"preview_url\\\" : null,\\n      \\\"track_number\\\" : 13,\\n      \\\"type\\\" : \\\"track\\\",\\n      \\\"uri\\\" : \\\"spotify:track:2PzCOP5Aj9SABiBgNEZ52G\\\"\\n    }, {\\n      \\\"album\\\" : {\\n        \\\"album_type\\\" : \\\"album\\\",\\n        \\\"artists\\\" : [ {\\n          \\\"external_urls\\\" : {\\n            \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n          },\\n          \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"name\\\" : \\\"ABBA\\\",\\n          \\\"type\\\" : \\\"artist\\\",\\n          \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        } ],\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/album/0C36RlW2Fa0C7n1JnWBBMP\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/albums/0C36RlW2Fa0C7n1JnWBBMP\\\",\\n        \\\"id\\\" : \\\"0C36RlW2Fa0C7n1JnWBBMP\\\",\\n        \\\"images\\\" : [ {\\n          \\\"height\\\" : 575,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/8a83cb534bc00fb42620cd1bdb941e02aa20761d\\\",\\n          \\\"width\\\" : 640\\n        }, {\\n          \\\"height\\\" : 269,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/60533fe6bbb2dd9fcc896f34d0fbcf66b88cf613\\\",\\n          \\\"width\\\" : 300\\n        }, {\\n          \\\"height\\\" : 57,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/6d811c4aa3b7e737b67a2e1f8ca6419c0bcfbb64\\\",\\n          \\\"width\\\" : 63\\n        } ],\\n        \\\"name\\\" : \\\"Abba\\\",\\n        \\\"release_date\\\" : \\\"1975\\\",\\n        \\\"release_date_precision\\\" : \\\"year\\\",\\n        \\\"total_tracks\\\" : 13,\\n        \\\"type\\\" : \\\"album\\\",\\n        \\\"uri\\\" : \\\"spotify:album:0C36RlW2Fa0C7n1JnWBBMP\\\"\\n      },\\n      \\\"artists\\\" : [ {\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"name\\\" : \\\"ABBA\\\",\\n        \\\"type\\\" : \\\"artist\\\",\\n        \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n      } ],\\n      \\\"disc_number\\\" : 1,\\n      \\\"duration_ms\\\" : 215266,\\n      \\\"explicit\\\" : false,\\n      \\\"external_ids\\\" : {\\n        \\\"isrc\\\" : \\\"SEAYD7501010\\\"\\n      },\\n      \\\"external_urls\\\" : {\\n        \\\"spotify\\\" : \\\"https://open.spotify.com/track/22NN4BS1AlqVbyKIWExgON\\\"\\n      },\\n      \\\"href\\\" : \\\"https://api.spotify.com/v1/tracks/22NN4BS1AlqVbyKIWExgON\\\",\\n      \\\"id\\\" : \\\"22NN4BS1AlqVbyKIWExgON\\\",\\n      \\\"is_local\\\" : false,\\n      \\\"is_playable\\\" : true,\\n      \\\"name\\\" : \\\"Mamma Mia\\\",\\n      \\\"popularity\\\" : 61,\\n      \\\"preview_url\\\" : null,\\n      \\\"track_number\\\" : 1,\\n      \\\"type\\\" : \\\"track\\\",\\n      \\\"uri\\\" : \\\"spotify:track:22NN4BS1AlqVbyKIWExgON\\\"\\n    }, {\\n      \\\"album\\\" : {\\n        \\\"album_type\\\" : \\\"album\\\",\\n        \\\"artists\\\" : [ {\\n          \\\"external_urls\\\" : {\\n            \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n          },\\n          \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"name\\\" : \\\"ABBA\\\",\\n          \\\"type\\\" : \\\"artist\\\",\\n          \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        } ],\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/album/5GwbPSgiTECzQiE6u7s0ZN\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/albums/5GwbPSgiTECzQiE6u7s0ZN\\\",\\n        \\\"id\\\" : \\\"5GwbPSgiTECzQiE6u7s0ZN\\\",\\n        \\\"images\\\" : [ {\\n          \\\"height\\\" : 575,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/cb818993d01447fdb9319db94c9976c59885f828\\\",\\n          \\\"width\\\" : 640\\n        }, {\\n          \\\"height\\\" : 270,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/f53c3cc7228a5ac768371fa8f35db6891b7aa7ff\\\",\\n          \\\"width\\\" : 300\\n        }, {\\n          \\\"height\\\" : 58,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/2ab03cbc259d1c70cfe5120148937b38bd0505a3\\\",\\n          \\\"width\\\" : 64\\n        } ],\\n        \\\"name\\\" : \\\"The Album\\\",\\n        \\\"release_date\\\" : \\\"1977\\\",\\n        \\\"release_date_precision\\\" : \\\"year\\\",\\n        \\\"total_tracks\\\" : 10,\\n        \\\"type\\\" : \\\"album\\\",\\n        \\\"uri\\\" : \\\"spotify:album:5GwbPSgiTECzQiE6u7s0ZN\\\"\\n      },\\n      \\\"artists\\\" : [ {\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"name\\\" : \\\"ABBA\\\",\\n        \\\"type\\\" : \\\"artist\\\",\\n        \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n      } ],\\n      \\\"disc_number\\\" : 1,\\n      \\\"duration_ms\\\" : 243933,\\n      \\\"explicit\\\" : false,\\n      \\\"external_ids\\\" : {\\n        \\\"isrc\\\" : \\\"SEAYD7702020\\\"\\n      },\\n      \\\"external_urls\\\" : {\\n        \\\"spotify\\\" : \\\"https://open.spotify.com/track/6vQN2a9QSgWcm74KEZYfDL\\\"\\n      },\\n      \\\"href\\\" : \\\"https://api.spotify.com/v1/tracks/6vQN2a9QSgWcm74KEZYfDL\\\",\\n      \\\"id\\\" : \\\"6vQN2a9QSgWcm74KEZYfDL\\\",\\n      \\\"is_local\\\" : false,\\n      \\\"is_playable\\\" : true,\\n      \\\"name\\\" : \\\"Take A Chance On Me\\\",\\n      \\\"popularity\\\" : 60,\\n      \\\"preview_url\\\" : null,\\n      \\\"track_number\\\" : 2,\\n      \\\"type\\\" : \\\"track\\\",\\n      \\\"uri\\\" : \\\"spotify:track:6vQN2a9QSgWcm74KEZYfDL\\\"\\n    }, {\\n      \\\"album\\\" : {\\n        \\\"album_type\\\" : \\\"album\\\",\\n        \\\"artists\\\" : [ {\\n          \\\"external_urls\\\" : {\\n            \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n          },\\n          \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"name\\\" : \\\"ABBA\\\",\\n          \\\"type\\\" : \\\"artist\\\",\\n          \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        } ],\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/album/5PVuX09frPq7AMYkkdcDfR\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/albums/5PVuX09frPq7AMYkkdcDfR\\\",\\n        \\\"id\\\" : \\\"5PVuX09frPq7AMYkkdcDfR\\\",\\n        \\\"images\\\" : [ {\\n          \\\"height\\\" : 573,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/b0307cd408f497243171826c19994ea124197cb7\\\",\\n          \\\"width\\\" : 640\\n        }, {\\n          \\\"height\\\" : 268,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/ccb216e53163aefacc95be1029b36f570af1cd7b\\\",\\n          \\\"width\\\" : 299\\n        }, {\\n          \\\"height\\\" : 57,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/c3fb7f460df28495cbb2f741eeb4672749b7f311\\\",\\n          \\\"width\\\" : 64\\n        } ],\\n        \\\"name\\\" : \\\"Super Trouper\\\",\\n        \\\"release_date\\\" : \\\"1980\\\",\\n        \\\"release_date_precision\\\" : \\\"year\\\",\\n        \\\"total_tracks\\\" : 12,\\n        \\\"type\\\" : \\\"album\\\",\\n        \\\"uri\\\" : \\\"spotify:album:5PVuX09frPq7AMYkkdcDfR\\\"\\n      },\\n      \\\"artists\\\" : [ {\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"name\\\" : \\\"ABBA\\\",\\n        \\\"type\\\" : \\\"artist\\\",\\n        \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n      } ],\\n      \\\"disc_number\\\" : 1,\\n      \\\"duration_ms\\\" : 294733,\\n      \\\"explicit\\\" : false,\\n      \\\"external_ids\\\" : {\\n        \\\"isrc\\\" : \\\"SEAYD8001020\\\"\\n      },\\n      \\\"external_urls\\\" : {\\n        \\\"spotify\\\" : \\\"https://open.spotify.com/track/2HeTmGTjl870ucJ8mF7zl5\\\"\\n      },\\n      \\\"href\\\" : \\\"https://api.spotify.com/v1/tracks/2HeTmGTjl870ucJ8mF7zl5\\\",\\n      \\\"id\\\" : \\\"2HeTmGTjl870ucJ8mF7zl5\\\",\\n      \\\"is_local\\\" : false,\\n      \\\"is_playable\\\" : true,\\n      \\\"name\\\" : \\\"The Winner Takes It All\\\",\\n      \\\"popularity\\\" : 57,\\n      \\\"preview_url\\\" : null,\\n      \\\"track_number\\\" : 2,\\n      \\\"type\\\" : \\\"track\\\",\\n      \\\"uri\\\" : \\\"spotify:track:2HeTmGTjl870ucJ8mF7zl5\\\"\\n    }, {\\n      \\\"album\\\" : {\\n        \\\"album_type\\\" : \\\"album\\\",\\n        \\\"artists\\\" : [ {\\n          \\\"external_urls\\\" : {\\n            \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n          },\\n          \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"name\\\" : \\\"ABBA\\\",\\n          \\\"type\\\" : \\\"artist\\\",\\n          \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        } ],\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/album/1WAlaVMaCstzAQ1Y5i0VeX\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/albums/1WAlaVMaCstzAQ1Y5i0VeX\\\",\\n        \\\"id\\\" : \\\"1WAlaVMaCstzAQ1Y5i0VeX\\\",\\n        \\\"images\\\" : [ {\\n          \\\"height\\\" : 575,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/8548134da5b3f2cee77654ee0ac5d674cecc0e6e\\\",\\n          \\\"width\\\" : 640\\n        }, {\\n          \\\"height\\\" : 269,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/04020a453f43f47182a8416d43afaa01b405aafc\\\",\\n          \\\"width\\\" : 300\\n        }, {\\n          \\\"height\\\" : 57,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/d1af32b3658846f43badb78327747d935852f5f1\\\",\\n          \\\"width\\\" : 63\\n        } ],\\n        \\\"name\\\" : \\\"Waterloo\\\",\\n        \\\"release_date\\\" : \\\"1974-01-01\\\",\\n        \\\"release_date_precision\\\" : \\\"day\\\",\\n        \\\"total_tracks\\\" : 14,\\n        \\\"type\\\" : \\\"album\\\",\\n        \\\"uri\\\" : \\\"spotify:album:1WAlaVMaCstzAQ1Y5i0VeX\\\"\\n      },\\n      \\\"artists\\\" : [ {\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"name\\\" : \\\"ABBA\\\",\\n        \\\"type\\\" : \\\"artist\\\",\\n        \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n      } ],\\n      \\\"disc_number\\\" : 1,\\n      \\\"duration_ms\\\" : 168826,\\n      \\\"explicit\\\" : false,\\n      \\\"external_ids\\\" : {\\n        \\\"isrc\\\" : \\\"SEAYD7415010\\\"\\n      },\\n      \\\"external_urls\\\" : {\\n        \\\"spotify\\\" : \\\"https://open.spotify.com/track/0RzhMHIsFMbOGh0oWDvNNK\\\"\\n      },\\n      \\\"href\\\" : \\\"https://api.spotify.com/v1/tracks/0RzhMHIsFMbOGh0oWDvNNK\\\",\\n      \\\"id\\\" : \\\"0RzhMHIsFMbOGh0oWDvNNK\\\",\\n      \\\"is_local\\\" : false,\\n      \\\"is_playable\\\" : true,\\n      \\\"name\\\" : \\\"Waterloo\\\",\\n      \\\"popularity\\\" : 55,\\n      \\\"preview_url\\\" : null,\\n      \\\"track_number\\\" : 1,\\n      \\\"type\\\" : \\\"track\\\",\\n      \\\"uri\\\" : \\\"spotify:track:0RzhMHIsFMbOGh0oWDvNNK\\\"\\n    }, {\\n      \\\"album\\\" : {\\n        \\\"album_type\\\" : \\\"compilation\\\",\\n        \\\"artists\\\" : [ {\\n          \\\"external_urls\\\" : {\\n            \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n          },\\n          \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"name\\\" : \\\"ABBA\\\",\\n          \\\"type\\\" : \\\"artist\\\",\\n          \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        } ],\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/album/2cKZfaz7GiGtZEeQNj1RyR\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/albums/2cKZfaz7GiGtZEeQNj1RyR\\\",\\n        \\\"id\\\" : \\\"2cKZfaz7GiGtZEeQNj1RyR\\\",\\n        \\\"images\\\" : [ {\\n          \\\"height\\\" : 640,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/ac59fc95d6f80b304ff4ab07c85d88a9d53fe9dd\\\",\\n          \\\"width\\\" : 640\\n        }, {\\n          \\\"height\\\" : 300,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/64e4b762ac4d85f69e13b31a87300e3dbd838c4c\\\",\\n          \\\"width\\\" : 300\\n        }, {\\n          \\\"height\\\" : 64,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/e082db092807a026ec6eb79ec540411d8392e2bc\\\",\\n          \\\"width\\\" : 64\\n        } ],\\n        \\\"name\\\" : \\\"ABBA Gold\\\",\\n        \\\"release_date\\\" : \\\"2008-01-01\\\",\\n        \\\"release_date_precision\\\" : \\\"day\\\",\\n        \\\"total_tracks\\\" : 19,\\n        \\\"type\\\" : \\\"album\\\",\\n        \\\"uri\\\" : \\\"spotify:album:2cKZfaz7GiGtZEeQNj1RyR\\\"\\n      },\\n      \\\"artists\\\" : [ {\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"name\\\" : \\\"ABBA\\\",\\n        \\\"type\\\" : \\\"artist\\\",\\n        \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n      } ],\\n      \\\"disc_number\\\" : 1,\\n      \\\"duration_ms\\\" : 231093,\\n      \\\"explicit\\\" : false,\\n      \\\"external_ids\\\" : {\\n        \\\"isrc\\\" : \\\"SEAYD7601020\\\"\\n      },\\n      \\\"external_urls\\\" : {\\n        \\\"spotify\\\" : \\\"https://open.spotify.com/track/2ATDkfqprlNNe9mYWodgdc\\\"\\n      },\\n      \\\"href\\\" : \\\"https://api.spotify.com/v1/tracks/2ATDkfqprlNNe9mYWodgdc\\\",\\n      \\\"id\\\" : \\\"2ATDkfqprlNNe9mYWodgdc\\\",\\n      \\\"is_local\\\" : false,\\n      \\\"is_playable\\\" : true,\\n      \\\"name\\\" : \\\"Dancing Queen\\\",\\n      \\\"popularity\\\" : 59,\\n      \\\"preview_url\\\" : null,\\n      \\\"track_number\\\" : 1,\\n      \\\"type\\\" : \\\"track\\\",\\n      \\\"uri\\\" : \\\"spotify:track:2ATDkfqprlNNe9mYWodgdc\\\"\\n    }, {\\n      \\\"album\\\" : {\\n        \\\"album_type\\\" : \\\"album\\\",\\n        \\\"artists\\\" : [ {\\n          \\\"external_urls\\\" : {\\n            \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n          },\\n          \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"name\\\" : \\\"ABBA\\\",\\n          \\\"type\\\" : \\\"artist\\\",\\n          \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        } ],\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/album/5PVuX09frPq7AMYkkdcDfR\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/albums/5PVuX09frPq7AMYkkdcDfR\\\",\\n        \\\"id\\\" : \\\"5PVuX09frPq7AMYkkdcDfR\\\",\\n        \\\"images\\\" : [ {\\n          \\\"height\\\" : 573,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/b0307cd408f497243171826c19994ea124197cb7\\\",\\n          \\\"width\\\" : 640\\n        }, {\\n          \\\"height\\\" : 268,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/ccb216e53163aefacc95be1029b36f570af1cd7b\\\",\\n          \\\"width\\\" : 299\\n        }, {\\n          \\\"height\\\" : 57,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/c3fb7f460df28495cbb2f741eeb4672749b7f311\\\",\\n          \\\"width\\\" : 64\\n        } ],\\n        \\\"name\\\" : \\\"Super Trouper\\\",\\n        \\\"release_date\\\" : \\\"1980\\\",\\n        \\\"release_date_precision\\\" : \\\"year\\\",\\n        \\\"total_tracks\\\" : 12,\\n        \\\"type\\\" : \\\"album\\\",\\n        \\\"uri\\\" : \\\"spotify:album:5PVuX09frPq7AMYkkdcDfR\\\"\\n      },\\n      \\\"artists\\\" : [ {\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"name\\\" : \\\"ABBA\\\",\\n        \\\"type\\\" : \\\"artist\\\",\\n        \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n      } ],\\n      \\\"disc_number\\\" : 1,\\n      \\\"duration_ms\\\" : 254600,\\n      \\\"explicit\\\" : false,\\n      \\\"external_ids\\\" : {\\n        \\\"isrc\\\" : \\\"SEAYD8001010\\\"\\n      },\\n      \\\"external_urls\\\" : {\\n        \\\"spotify\\\" : \\\"https://open.spotify.com/track/2nMghZvtLx6DDgTEHEsb4w\\\"\\n      },\\n      \\\"href\\\" : \\\"https://api.spotify.com/v1/tracks/2nMghZvtLx6DDgTEHEsb4w\\\",\\n      \\\"id\\\" : \\\"2nMghZvtLx6DDgTEHEsb4w\\\",\\n      \\\"is_local\\\" : false,\\n      \\\"is_playable\\\" : true,\\n      \\\"name\\\" : \\\"Super Trouper\\\",\\n      \\\"popularity\\\" : 54,\\n      \\\"preview_url\\\" : null,\\n      \\\"track_number\\\" : 1,\\n      \\\"type\\\" : \\\"track\\\",\\n      \\\"uri\\\" : \\\"spotify:track:2nMghZvtLx6DDgTEHEsb4w\\\"\\n    }, {\\n      \\\"album\\\" : {\\n        \\\"album_type\\\" : \\\"album\\\",\\n        \\\"artists\\\" : [ {\\n          \\\"external_urls\\\" : {\\n            \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n          },\\n          \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"name\\\" : \\\"ABBA\\\",\\n          \\\"type\\\" : \\\"artist\\\",\\n          \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        } ],\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/album/1M4anG49aEs4YimBdj96Oy\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/albums/1M4anG49aEs4YimBdj96Oy\\\",\\n        \\\"id\\\" : \\\"1M4anG49aEs4YimBdj96Oy\\\",\\n        \\\"images\\\" : [ {\\n          \\\"height\\\" : 575,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/361387b8f4297afda0394896aeb1f315b3fbad0b\\\",\\n          \\\"width\\\" : 640\\n        }, {\\n          \\\"height\\\" : 270,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/803de302874cdff3ecbd531ecfd7585793c6b7c1\\\",\\n          \\\"width\\\" : 300\\n        }, {\\n          \\\"height\\\" : 57,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/c43883a6978668e0636d96887cf43ddbb6b5965d\\\",\\n          \\\"width\\\" : 63\\n        } ],\\n        \\\"name\\\" : \\\"Arrival\\\",\\n        \\\"release_date\\\" : \\\"1976\\\",\\n        \\\"release_date_precision\\\" : \\\"year\\\",\\n        \\\"total_tracks\\\" : 12,\\n        \\\"type\\\" : \\\"album\\\",\\n        \\\"uri\\\" : \\\"spotify:album:1M4anG49aEs4YimBdj96Oy\\\"\\n      },\\n      \\\"artists\\\" : [ {\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"name\\\" : \\\"ABBA\\\",\\n        \\\"type\\\" : \\\"artist\\\",\\n        \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n      } ],\\n      \\\"disc_number\\\" : 1,\\n      \\\"duration_ms\\\" : 255533,\\n      \\\"explicit\\\" : false,\\n      \\\"external_ids\\\" : {\\n        \\\"isrc\\\" : \\\"SEAYD7602070\\\"\\n      },\\n      \\\"external_urls\\\" : {\\n        \\\"spotify\\\" : \\\"https://open.spotify.com/track/1IxDBsZdVMhfkLqrZjARpk\\\"\\n      },\\n      \\\"href\\\" : \\\"https://api.spotify.com/v1/tracks/1IxDBsZdVMhfkLqrZjARpk\\\",\\n      \\\"id\\\" : \\\"1IxDBsZdVMhfkLqrZjARpk\\\",\\n      \\\"is_local\\\" : false,\\n      \\\"is_playable\\\" : true,\\n      \\\"name\\\" : \\\"Fernando\\\",\\n      \\\"popularity\\\" : 54,\\n      \\\"preview_url\\\" : null,\\n      \\\"track_number\\\" : 11,\\n      \\\"type\\\" : \\\"track\\\",\\n      \\\"uri\\\" : \\\"spotify:track:1IxDBsZdVMhfkLqrZjARpk\\\"\\n    }, {\\n      \\\"album\\\" : {\\n        \\\"album_type\\\" : \\\"album\\\",\\n        \\\"artists\\\" : [ {\\n          \\\"external_urls\\\" : {\\n            \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n          },\\n          \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n          \\\"name\\\" : \\\"ABBA\\\",\\n          \\\"type\\\" : \\\"artist\\\",\\n          \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        } ],\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/album/0zdZSyxWaYmaRMPeUHcG1K\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/albums/0zdZSyxWaYmaRMPeUHcG1K\\\",\\n        \\\"id\\\" : \\\"0zdZSyxWaYmaRMPeUHcG1K\\\",\\n        \\\"images\\\" : [ {\\n          \\\"height\\\" : 575,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/9a06fe3059762156cff0ea02053592bb625f9350\\\",\\n          \\\"width\\\" : 640\\n        }, {\\n          \\\"height\\\" : 270,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/51bba354b425161d4f07a35a14bb4971646892a0\\\",\\n          \\\"width\\\" : 300\\n        }, {\\n          \\\"height\\\" : 57,\\n          \\\"url\\\" : \\\"https://i.scdn.co/image/a33d19704f08f5651000c866d7fb00117d3af44d\\\",\\n          \\\"width\\\" : 63\\n        } ],\\n        \\\"name\\\" : \\\"Voulez-Vous\\\",\\n        \\\"release_date\\\" : \\\"1979\\\",\\n        \\\"release_date_precision\\\" : \\\"year\\\",\\n        \\\"total_tracks\\\" : 13,\\n        \\\"type\\\" : \\\"album\\\",\\n        \\\"uri\\\" : \\\"spotify:album:0zdZSyxWaYmaRMPeUHcG1K\\\"\\n      },\\n      \\\"artists\\\" : [ {\\n        \\\"external_urls\\\" : {\\n          \\\"spotify\\\" : \\\"https://open.spotify.com/artist/0LcJLqbBmaGUft1e9Mm8HV\\\"\\n        },\\n        \\\"href\\\" : \\\"https://api.spotify.com/v1/artists/0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"id\\\" : \\\"0LcJLqbBmaGUft1e9Mm8HV\\\",\\n        \\\"name\\\" : \\\"ABBA\\\",\\n        \\\"type\\\" : \\\"artist\\\",\\n        \\\"uri\\\" : \\\"spotify:artist:0LcJLqbBmaGUft1e9Mm8HV\\\"\\n      } ],\\n      \\\"disc_number\\\" : 1,\\n      \\\"duration_ms\\\" : 327040,\\n      \\\"explicit\\\" : false,\\n      \\\"external_ids\\\" : {\\n        \\\"isrc\\\" : \\\"SEAYD7901080\\\"\\n      },\\n      \\\"external_urls\\\" : {\\n        \\\"spotify\\\" : \\\"https://open.spotify.com/track/4g5LUzHFRr4Zlok8KzERmI\\\"\\n      },\\n      \\\"href\\\" : \\\"https://api.spotify.com/v1/tracks/4g5LUzHFRr4Zlok8KzERmI\\\",\\n      \\\"id\\\" : \\\"4g5LUzHFRr4Zlok8KzERmI\\\",\\n      \\\"is_local\\\" : false,\\n      \\\"is_playable\\\" : true,\\n      \\\"name\\\" : \\\"Chiquitita\\\",\\n      \\\"popularity\\\" : 54,\\n      \\\"preview_url\\\" : null,\\n      \\\"track_number\\\" : 8,\\n      \\\"type\\\" : \\\"track\\\",\\n      \\\"uri\\\" : \\\"spotify:track:4g5LUzHFRr4Zlok8KzERmI\\\"\\n    } ],\\n    \\\"limit\\\" : 10,\\n    \\\"next\\\" : \\\"https://api.spotify.com/v1/search?query=abba&type=track&market=US&offset=10&limit=10\\\",\\n    \\\"offset\\\" : 0,\\n    \\\"previous\\\" : null,\\n    \\\"total\\\" : 9278\\n  }\\n}\"" })

..although that error isn't too helpful - the error from serde_json::from_str is missing field 'available_markets'

I think this is intended behavior of the API, at least based on this page
https://developer.spotify.com/documentation/general/guides/track-relinking-guide/

There are a number of important differences between the response you get with and without the market query parameter.

When the market parameter is supplied:

  • The available_markets property in the Track object is replaced by the is_playable property. (Since the request contains the market query parameter, there’s no need for the available_markets property to determine if the user can play the track or not.)
  • [...]

In my case workaround is easy, just don;t specify market as I don't need this functionality

Occurring with version 0.2.5 and master as of f8463a3

Add unlimited endpoints

One thing I really like about tekore is that it offers endpoints without limits. Spotify has a limit of items that may be returned with certain requests, such as the followed artists or the songs in a playlist.

There are two different ways to do that with tekore AFAIK:

And then the endpoints themselves are what you'd expect: followed_artists. These don't mention the "unlimited" mode, it's only tied to the client itself. The client will internally send multiple requests until the endpoint returned value is complete, and return the results.

How this could be implemented in this library could of course vary, but this feature itself is very useful and interesting.

Consider making the cache file a feature

The cache file feature is something I'm not sure how I feel about. While it's definitely useful, I'm not sure if it should be the default behaviour. It could be moved into a feature, which when activated, would use the cache file anywhere possible. Or instead of having do_something and do_something_without_cache, it could be do_something and do_something_with_cache.

What do you think? I need some opinions on this. I don't actually plan on changing anything but I want to propose any breaking changes before the next release.

Optional parameters

I would like to discuss how to handle optional parameters for this library. I recently made an article inspired by this issue which sums up the different ways to approach this: https://vidify.org/blog/rust-parameters/

I think the most convenient ones in there are the following:

A: Using a bare Option<T>
B: Using Into<Option<T>>
E: Endpoint-oriented interface. We have related groups, like track, album... which could be used as the endpoint implementations.
F: Hybrid derive pattern. More complex but is super nice for the user, and we could find a way to make it easier to implement.
G: There are optional parameters repeated lots of times throughout the code, like market, country, limit... So we might not even need the group implementation, just in the client.

Currently, we use the second one, but it's not consistent. Some functions also use the first one. We should decide one of them and use it consistently throughout the codebase.

My personal favorite is just the first one. Using Into<Option<T>> seems unnecessary to me and doesn't offer that much over a bare Option<T>. That, or approach E, which seems perfect for the parameters I mentioned: market, country... and we could just ignore it when these params are specified for an endpoint that doesn't need it. As long as it's well documented I don't think it should be a problem. market() would just set a value inside the client (similar to the constructor) which would be used for all the calls afterwards.

I'd like to tag @Koxiaet, who opened an issue with a very opinionated list of improvements; I'm sure they can help with this as well :)

Reducing rspotify's core dependencies

I've been creating various PRs to clean up some unnecessary dependencies I found and such. Not only does this decrease compilation times, but this may also reduce binary size in case the optimizer is unable to know a module isn't used at all.

Here are some of my proposals, don't hesitate on criticizing them; I want to discuss these changes with @ramsayleung and if possible with more users of rspotify. It might also be too much to remove some of them:

Optional features

  • Make webbrowser optional, under some cli feature. This dependency isn't used by all the users, since some may not need to open a browser for the authentication process. It's only used for the util::request_token function currently.
  • Make dotenv optional, under some env feature. This is also only used by some users, mostly web servers.

These changes will break backward compatibility, so a new version would have to be released. I could create an UPGRADING.md file that explains what changes were made in order to make this process easier.

Removing dependencies

  • [accepted PR] 2 less dependencies after #100
  • [pending PR accept] 4 less dependencies after #106
  • [pending PR accept] upgrading the dependencies curiously goes from 191 dependencies with cargo run --example current_playing to 177, so ~14 less dependencies after #105
  • [discussion] lazy_static is only used in client.rs for CLIENT here. This CLIENT variable is then only used once here. I haven't used lazy_static myself, but I wanted to re-evaluate the following questions:
    - Is it necessary that CLIENT is a global, static, public variable?
    - Is lazy_static is necessary here?
  • [discussion] Is rand necessary only for util::generate_random_string here? rand is a considerably big dependency because it includes lots of things that are often unnecessary. I recently saw this PR rust-lang/rust-analyzer#5574 that replaced it with oorandom, a much simpler and straightforward crate. Could this also be applied to this use case?

reqwest introduced breaking change on patch version change.

As of v0.9.18 of reqwest, rspotify does exit with an unwrap on an Err value. This Is due to some breaking changes on a patch version of reqwest, meaning that the this library will only work with reqwest up to v0.9.17.

22:20:02 [ERROR] Caught panic with message: called `Result::unwrap()` on an `Err` value: Error(Executor(EnterError { reason: "attempted to run an executor while another executor is already running" }), "https://api.spotify.com/v1/me/player/currently-playing")

The fix is to lock down the version within the Cargo.toml file to v0.9.17.

Partial model object with `fields` query parameter

Some Spotify endpoints, especially the ones returning a paging object, allow to specify the fields that we want to fetch.

For example, Get a Playlist's Items has a fields query parameter that allow selection of fields, but we can't use it since it would raise an error when trying to deserialize the fields that are not specify :

// Return an Err(ErrorMessage { msg: "convert result failed,  reason: Error(\"missing field `album`\", ... })
spotify.playlist("37i9dQZF1DWXncK9DGeLh7", Some("tracks.items.track.(id,name)"), None);

This behaviour makes the fields parameter useless and forces to fetch data even if we don't need it. Solutions to that would be :

  1. use #[serde(default)] on existing model object fields
  2. use an Option on existing model object fields
  3. create a new object model, for example PartialTrack, that implement either 1. or 2. and make endpoints that use a fields query parameter return such a Partial object instead of a Full or Simplified one.

I think 3. would be the most reasonable solution, moreover it might be possible to implement it using a macro.

Get Player Error Reasons

Over in spotify-tui, I am getting API errors fairly frequently. Much of the time, the error code is 403 (I am able to see that now since #62), which likely means that I am getting a player error, but I get no more information than that.

I'm not great at web stuff; I don't understand what is happening to this object in the request response. I figured I would be able to define a struct to hold the player error:

struct PlayerError {
    status: u16,
    message: String, 
    reason: String,
}

and then (deserialize the json)[https://docs.rs/reqwest/0.10.0-alpha.1/reqwest/struct.Response.html#method.json] from the response directly into it, but reqwest seems to be hiding the json from me somehow. What's happening to it? How can I get access to the error reason? This would help immensely in providing more information about failure to both developers and end-users.

I'm happy to implement any changes that need to happen, but I don't have much experience handling web requests, and none of that experience is with Rust/reqwest, so any other devs here that could shed some light would be appreciated.

Update derive_builder

This dependency draws in pre-1.0 versions of proc_macro2 and syn, which will be duplicated in most codebases. derive_builder 0.8 updated to use the 1.0 versions of proc_macro and syn, which would improve the build times of most crates that depend on rspotify

Start playback at `position_ms`

Firstly, thank you for creating this amazing crate, it's made working with the spotify API so fun.

I have this usecase

As a user, I should be able to toggle play/pause and resume playback at the previous position.

Looks like the spotify api let's you pass in position_ms to the play endpoint https://developer.spotify.com/documentation/web-api/reference/player/start-a-users-playback/

Which I think is what I need?

Potential solution

Pass position_ms as a fourth paramter to the start_playback function.

Or perhaps I am missing something? Is there an existing way of resuming playback?

Track object missing images field

According to the reference for the track endpoint, one of the response fields is an array of images, which can be empty, that just stores the album art for that song. Is there any reason this isn't implemented in here?

Adding undocumented endpoint "made for x"

First of all, thank you for your great work.

I've noticed that both you and the spotify web api don't have the "made for x" endpoint.
After digging in the original github web-api repository, I've found this issue.

Do you plan on implementing it?

Bubble up more detailed error data

It'd be nice if the API provided access to more detailed error data, such as the HTTP status code and headers.

This is necessary to implement rate limiting on the client side imposed by the Spotify API servers. Alternatively, an implementation in rspotify that respects Spotify's rate limits would be great, as well.

I'm currently encountering this in hrkfdn/ncspot#29 and am not sure how to handle this.

Meta-Issue

This is a meta-issue of many issues with rspotify. Some of it's opinionated but most of it I think is just an improvement.

General

  • #173 The crate root contains nothing other than modules. This means that paths get very long and verbose (e.g. rspotify::client::Spotify). The entire contents of the client module should be moved to the crate root to make it easier to access.
  • #128 senum; this module contains all the enums used (and also Error for some reason), but could be split into more fitting modules such as model.
  • #129 Rspotify is designed for use from a CLI. Lots of the functions read from stdin, but this is out of scope for this library. Either they should be removed or contained in a cli/tui/terminal/console module and feature-gated.
  • #129 This library extensively uses a fake builder pattern - it's like the real builder pattern but it's all the same type. To add proper type safety the builder patterns should be properly implemented in their own types.
  • #128 Many functions like Spotify::tracks take in a Vec<&str>/Vec<String>/&[String]/&[&str], when they should take in an impl IntoIterator<Item = &str>.
  • #128 Some endpoints and functions in Rspotify take String as parameters instead of &str.
  • limit and offset parameters should universally be usizes as they are easier to work with from Rust.
  • #129 Actual Default::default impl (see TokenInfo::default)
  • #129 Spotify::get_uri should useformat!:
    fn get_uri(&self, _type: Type, _id: &str) -> String {
        format!("spotify:{}:{}", _type.as_str(), self.get_id(_type, _id))
    }
  • #129 Fix the documentation links to Spotify endpoints and make the format more consistent
  • #161 Make a separate type for IDs parsed by Spotify::get_id
  • #226 Authenticated tests should actually not modify the user's account and cause minimal changes (as in, don't completely delete the user's library for a test).

OAuth

  • #185 TokenInfo stores expires_in as a u32, and additionally has the expires_at field. This is not easy to use, instead it should have a single expires field with the type Instant.
  • #185 TokenInfo stores a scope: String, when it should store a scope: Vec<String> obtained by splitting the scopes by spaces.
  • #129 TokenInfo stores token_type, which is useless since it is always Bearer.
  • #129 The SpotifyClientCredentials and SpotifyOAuth have a superfluous Spotify prefix; just ClientCredentials would be shorter and not duplicate the crate name.
  • #129 SpotifyClientCredentials contains not only the user's client credentials but also the token info if present, so the name is misleading. In fact the SpotifyOAuth struct duplicates the client_id and client_secret fields due to the lack of a real ClientCredentials type.
  • #129 A new Client is created every time fetch_access_token is called. This is very inefficient. Instead, the authorization logic should be moved to Spotify allowing it to reuse the same client.
  • #129 As far as I can tell token_info is never mutated on SpotifyClientCredentials. This means that once the initial token has expired rspotify will get a new token every time. The solution is to wrap TokenInfo into a Mutex and mutate it when appropriate.
  • #129 Spotify contains an access_token twice; once in the struct itself and once in client_credentials_manager.token_info_access_token.

Utils

  • #128 convert_map_to_string is better served by serde_urlencoded or even better Reqwest's query method.
  • A few of these functions like generate_random_string should be private.
  • #185 datetime_to_timestamp is only necessary due to this library's use of numbers instead of Instants or Durations.
  • #129 The token functions should probably be methods of Spotify instead of standalone functions.
  • #185 If all the above suggestions are incorporated the utils module can be removed entirely, and any remaining functions can simply be placed in the crate root for easy access.

Model

  • #128 The model module is not reexported from the crate root. Having pub use model::* and pub use {album, artist, audio, etc}::* would keep the docs uncluttered and allow users to write rspotify::FullAlbum instead of the overly verbose rspotify::model::album::FullAlbum.
  • #145 Derive PartialEq (and Eq if possible) for everything.
  • #244 The _type fields on the models are unnecessary data. It would be better to use a macro to create constants that can only serialize to and deserialize as their string, for example TypeAlbum which is represented in JSON as only "album". This avoids carrying extra data while still storing the type field.
  • The uri and href fields are unnecessary, since it can be recreated easily. Make these methods instead.
  • #145 copyrights is represented as a Vec<HashMap<String, String>>; a much more accurate representation is Vec<Copyright> and
struct Copyright {
    text: String,
    r#type: CopyrightType,
}
enum CopyrightType {
    P,
    C,
}
  • #145 FullAlbum::release_date and FullAlbum::release_date_precision are Strings. Instead, it should be deserialized into a proper date type like NaiveDate and a enum DatePrecision { Year, Month, Day }.
  • #145 The same applies to SimplifiedEpisode and FullEpisode.
  • #157 Types like FullAlbums, PageSimplifiedAlbums, etc exist as wrappers. This makes the API more complex. Instead, have that type exist solely in the API function that needs it, and then return a Vec<FullAlbum>. This drastically reduces the number of public API types.
  • #145 Restrictions is located in the album module, despite it not having any particular relevance to albums.
  • #145 {FullArtist, FullPlaylist}::followers is a HashMap<String, Option<Value>>. It should be a struct Followers { total: usize }.
  • #177 AudioAnalysisSection::mode and AudioFeatures::mode are f32s but should be Option<Mode>s where enum Mode { Major, Minor } as it is more useful.
  • #145 This is more opinionated but AudioAnalysisSection and AudioAnalysisSegment could contain a #[serde::flatten]ed TimeInterval instead of the having the fields inline.
  • #145 AudioAnalysisMeasure is identical to TimeInterval, and so should be replaced by it.
  • AudioFeatures::analysis_url and AudioFeatures::track_href aren't useful for the user, since they will access it via this library.(The assumption about how do users use this library is not accurate, since we have no idea about how do users use this library)
  • #157 AudioFeatures::duration_ms should be replaced with a duration of type Duration.
  • [ ] Context contains a uri, but since those aren't particularly useful it should be replaced with a transformer into an id.
  • #145 Actions::disallows can be replaced with a Vec<DisallowKey> or HashSet<DisallowKey> by removing all entires whose value is false, which will result in a simpler API.
  • #145 CurrentlyPlayback should be renamed CurrentPlayback, since CurrentlyPlayback doesn't make sense.
  • #157 CurrentlyPlayingContext::timestamp should be a DateTime<Utc>.
  • #157 CurrentlyPlayingContext::progress_ms should be progress: Duration.
  • Since CurrentlyPlaybackis a superset of CurrentlyPlaying, it should probably contain it instead of containing all its fields.
  • #145 Device is missing the field is_private_session: bool.
  • #145 Device::id and Device::volume_percent can be null in the Spotify API, so they should be an Option.
  • #145 FullPlayingContext doesn't exist in the Spotify API. It should be replaced with CurrentlyPlaybackContext.
  • #145 SimplifiedPlayingContext also doesn't exist, and should be replaced with CurrentlyPlayingContext.
  • #145 CUDResult is oddly named; it should refer to playlists in its title. PlaylistResult might be a better name. But it's not even necessary at all, the functions can just return String directly.
  • Single-item modules like image should have their items placed in the above scope.
  • #161 Offset should be represented as a Type + id since that is easier for the user.
  • #157 Offset::position should be a Duration.
  • Page::{limit, offset, total} should be usizes, because they're easier to work with.
  • Page::{next, previous} are not useful to the user since they'll use this program's API.
  • CursorBasedPage should also use usizes.
  • CursorBasedPage should also not include the URLs.
  • #145 Playing should be replaced with CurrentlyPlayingContext, since it is the same.
  • #185 SimplifiedPlaylist::tracks should be a struct Tracks { total: usize }; this isn't document but is what the API returns.
  • #145 PlaylistTrack::added_at can be null so should be Option.
  • #145 PlaylistTrack is poorly named in the Spotify API. It should be called PlaylistItem since it can also contain episodes.
  • #194 Allow PlaylistTrack/PlaylistItem to contain episodes.
  • RecommendationsSeed::{after_filtering_size, after_relinking_size, initial_pool_size} should be usizes since they are easier to use.
  • SearchResult should be a struct not an enum, since you can search for multiple items at once.(You have to specify the searchType when using search endpoint, so you can't search for multiple items at once.)
  • #157 SimplifiedEpisode::duration_ms should be a Duration.
  • #145 language shouldn't be in SimplifiedEpisode if it's deprecated.
  • #157 ResumePoint::resume_position_ms should be a Duration.
  • SimplifiedTrack::disc_number should be a usize, for ease of use.
  • #145 SimplfiiedTrack is missing the is_playable, linked_from and restrictions fields.
  • SimplifiedTrack::track_number should be a usize, again for ease of use.
  • #145 PublicUser::images should be a Vec<Image> and represent None as an empty Vec.
  • #145 PrivateUser is missing the product field, which should be an enum Subscription { Premium, Free }.
  • Country should probably be moved to a separate crate like isocountry(It's not that necessary)
  • IncludeExternal can just be replaced with a bool in the search function.
  • #128 Given that ClientError exists I'm not sure on the purpose of Error and ErrorKind; are they necessary?

Internals

  • #129 The Authorization header is constructed manually instead of using Reqwest's bearer_auth method.
  • #206 Use iterators wherever possible
  • #129 More idiomatic Option and Result parsing, avoid so many match expressions
  • Reduce endpoints, make them as concise as possible:
    • #200 Is it possible to use HashMap<&str, &str> instead of HashMap<String, String> to avoid so many to_string and to_owned?
    • #202 Maybe a hashmap macro like json?
    • Is it possible that endpoint_{get,post,put,delete} methods also run convert_result inside?
  • #189 More tests for methods like get_uri and get_id
  • #257, #224 Clean up all TODOs
  • Remove all possible unwrap and panic, or attach an explanation next to them.
  • #250 Remove pub(in crate)s inside already pub(in crate) modules; only use (in crate) when necessary (reduce their usage as much as possible or it becomes a mess). You can find all instances with rg 'pub\s*\((in crate|crate)\)'
  • #257 Make sure the visibilities are correct, we don't want to export internal parts of the library to keep the docs clean.
  • #257 Add a bit of logging to the main crate

Percent sign in search query leads to a 400

I found this while using spotify-tui. After some digging, it seems like the issue is coming from rspotify.

Using a % symbol in your query string when calling the search function leads to a 400. Here's a quick and tiny snippet from spotify-tui:

let search_show = self.spotify.search(
        &search_term,
        SearchType::Show,
        self.small_search_limit,
        0,
        country,
        None,
      );

If &search_term above were to be, e.g., 99% invisible, we get a HTTP 400.

However, if we escape it as 99%25 invisible, the query goes through as expected.

It is unclear to me why we don't see other issues, like, why space ' ' is not necessary to explicitly escape as '%20'. I'm new to rust but I imagine that perhaps space is handled already somehow/somewhere in the stack when we use the RequestBuilder.

Create "default" values/Builder pattern for some structs

Hello,

I'm unit testing my code and in order to do it I need to create a simplified version of SimplifiedAlbum (I understand the irony here of simplified version of something already simplified ) since I need just external_urls, id, name.

An example of one that I'm creating:

SimplifiedAlbum {
            artists: vec![],
            album_type: "".to_string(),
            name: "black holes and revelations".to_string(),
            _type: Type::Artist,
            id: "black_holes_123".to_string(),
            external_urls: [
                ("spotify", "https://some_url".to_string())
            ].iter().collect(),
            available_markets: vec![],
            href: "".to_string(),
            images: vec![],
            uri: "".to_string()
        };

In the end, I would like some flexibility to create a simplified version of this struct with less code. I think I will try to work in this change this weekend, but just in case if someone also wants to do it...

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.