Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@

## 0.12.0 (2023.08.26)
**New features**
- ([#439](https://github.com/ramsayleung/rspotify/pull/439)) Getter and Setter of playlist api endpoint
- ([#390](https://github.com/ramsayleung/rspotify/pull/390)) The `scopes!` macro supports to split the scope by whitespace.
- ([#418](https://github.com/ramsayleung/rspotify/pull/418)) Add a user-settable callback function whenever token is updated.

Expand Down
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,8 @@ path = "examples/pagination_sync.rs"
name = "pagination_async"
required-features = ["env-file", "cli", "client-reqwest"]
path = "examples/pagination_async.rs"

[[example]]
name = "playlist"
required-features = ["env-file", "cli", "client-reqwest"]
path = "examples/playlist_with_oauth.rs"
253 changes: 253 additions & 0 deletions examples/playlist_with_oauth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
use rspotify::{prelude::*,
clients::pagination::Paginator,
model::{
EpisodeId, FullPlaylist,
ItemPositions, PlaylistId,
TrackId, UserId,
},
scopes,ClientResult, AuthCodeSpotify, Credentials, OAuth};
use base64::{engine::general_purpose, Engine as _};
use maybe_async::maybe_async;
use reqwest;

async fn fetch_all<T>(paginator: Paginator<'_, ClientResult<T>>) -> Vec<T> {
#[cfg(feature = "__async")]
{
use futures::stream::TryStreamExt;

paginator.try_collect::<Vec<_>>().await.unwrap()
}

#[cfg(feature = "__sync")]
{
paginator.filter_map(|a| a.ok()).collect::<Vec<_>>()
}
}

#[maybe_async]
async fn check_num_tracks(client: &AuthCodeSpotify, playlist_id: PlaylistId<'_>, num: i32) {
let fetched_tracks = fetch_all(client.playlist_items(playlist_id, None, None)).await;
assert_eq!(fetched_tracks.len() as i32, num);
}

async fn check_playlist_cover(client: &AuthCodeSpotify, playlist_id: &PlaylistId<'_>) {
let img_url = "https://images.dog.ceo/breeds/poodle-toy/n02113624_8936.jpg";
let img_bytes = tokio::task::spawn_blocking(move || {reqwest::blocking::get(img_url).unwrap().bytes().unwrap()}).await.unwrap();
let playlist_cover_base64 = general_purpose::URL_SAFE_NO_PAD.encode(img_bytes.clone());

println!("playlist id : {}", playlist_id);

// check cover image
let cover_res = client
.playlist_cover_image(playlist_id.as_ref())
.await
.unwrap();

println!("cover_res pre upload: {:?}", cover_res);

// add playlist cover image
client
.playlist_upload_cover_image(playlist_id.as_ref(), &playlist_cover_base64)
.await
.unwrap();

// check cover image
let cover_res = client
.playlist_cover_image(playlist_id.as_ref())
.await
.unwrap();

println!("cover_res post upload: {:?}", cover_res);
}


async fn check_playlist_create(client: &AuthCodeSpotify) -> FullPlaylist {
let user = client.me().await.unwrap();
let name = "A New Playlist";

// First creating the base playlist over which the tests will be ran
let playlist = client
.user_playlist_create(user.id.as_ref(), name, Some(false), None, None)
.await
.unwrap();

// Making sure that the playlist has been added to the user's profile
let fetched_playlist = client
.user_playlist(user.id.as_ref(), Some(playlist.id.as_ref()), None)
.await
.unwrap();
assert_eq!(playlist.id, fetched_playlist.id);
let user_playlists = fetch_all(client.user_playlists(user.id)).await;
let current_user_playlists = fetch_all(client.current_user_playlists()).await;
assert_eq!(user_playlists.len(), current_user_playlists.len());

// Modifying the playlist details
let name = "A New Playlist-update";
let description = "A random description";
client
.playlist_change_detail(
playlist.id.as_ref(),
Some(name),
Some(true),
Some(description),
Some(false),
)
.await
.unwrap();

playlist
}

async fn check_playlist_tracks(client: &AuthCodeSpotify, playlist: &FullPlaylist) {
// The tracks in the playlist, some of them repeated
let tracks = [
PlayableId::Track(TrackId::from_uri("spotify:track:5iKndSu1XI74U2OZePzP8L").unwrap()),
PlayableId::Track(TrackId::from_uri("spotify:track:5iKndSu1XI74U2OZePzP8L").unwrap()),
PlayableId::Episode(EpisodeId::from_uri("spotify/episode/381XrGKkcdNkLwfsQ4Mh5y").unwrap()),
PlayableId::Episode(EpisodeId::from_uri("spotify/episode/6O63eWrfWPvN41CsSyDXve").unwrap()),
];

// Firstly adding some tracks
client
.playlist_add_items(
playlist.id.as_ref(),
tracks.iter().map(PlayableId::as_ref),
None,
)
.await
.unwrap();
check_num_tracks(client, playlist.id.as_ref(), tracks.len() as i32).await;

// Reordering some tracks
client
.playlist_reorder_items(playlist.id.as_ref(), Some(0), Some(3), Some(2), None)
.await
.unwrap();
// Making sure the number of tracks is the same
check_num_tracks(client, playlist.id.as_ref(), tracks.len() as i32).await;

// Replacing the tracks
let replaced_tracks = [
PlayableId::Track(TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap()),
PlayableId::Track(TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap()),
PlayableId::Track(TrackId::from_uri("spotify:track:1301WleyT98MSxVHPZCA6M").unwrap()),
PlayableId::Episode(EpisodeId::from_id("0lbiy3LKzIY2fnyjioC11p").unwrap()),
PlayableId::Track(TrackId::from_uri("spotify:track:5m2en2ndANCPembKOYr1xL").unwrap()),
PlayableId::Episode(EpisodeId::from_id("4zugY5eJisugQj9rj8TYuh").unwrap()),
PlayableId::Track(TrackId::from_uri("spotify:track:5m2en2ndANCPembKOYr1xL").unwrap()),
];
client
.playlist_replace_items(
playlist.id.as_ref(),
replaced_tracks.iter().map(|t| t.as_ref()),
)
.await
.unwrap();
// Making sure the number of tracks is updated
check_num_tracks(client, playlist.id.as_ref(), replaced_tracks.len() as i32).await;

// Removes a few specific tracks
let tracks = [
ItemPositions {
id: PlayableId::Track(
TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(),
),
positions: &[0],
},
ItemPositions {
id: PlayableId::Track(
TrackId::from_uri("spotify:track:5m2en2ndANCPembKOYr1xL").unwrap(),
),
positions: &[4, 6],
},
];
client
.playlist_remove_specific_occurrences_of_items(playlist.id.as_ref(), tracks, None)
.await
.unwrap();
// Making sure three tracks were removed
check_num_tracks(
client,
playlist.id.as_ref(),
replaced_tracks.len() as i32 - 3,
)
.await;

// Removes all occurrences of two tracks
let to_remove = vec![
PlayableId::Track(TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap()),
PlayableId::Episode(EpisodeId::from_id("0lbiy3LKzIY2fnyjioC11p").unwrap()),
];
client
.playlist_remove_all_occurrences_of_items(playlist.id.as_ref(), to_remove, None)
.await
.unwrap();
// Making sure two more tracks were removed
check_num_tracks(
client,
playlist.id.as_ref(),
replaced_tracks.len() as i32 - 5,
)
.await;
}

#[maybe_async]
async fn check_playlist_follow(client: &AuthCodeSpotify, playlist: &FullPlaylist) {
let user_ids = [
UserId::from_id("possan").unwrap(),
UserId::from_id("elogain").unwrap(),
];

// It's a new playlist, so it shouldn't have any followers
let following = client
.playlist_check_follow(playlist.id.as_ref(), &user_ids)
.await
.unwrap();
assert_eq!(following, vec![false, false]);

// Finally unfollowing the playlist in order to clean it up
client
.playlist_unfollow(playlist.id.as_ref())
.await
.unwrap();
}

async fn test_playlist(client: AuthCodeSpotify) {

let playlist = check_playlist_create(&client).await;
check_playlist_tracks(&client, &playlist).await;
check_playlist_cover(&client, &playlist.id).await;
check_playlist_follow(&client, &playlist).await;
}

#[tokio::main]
async fn main() {
// You can use any logger for debugging.
env_logger::init();

// The credentials must be available in the environment. Enable the
// `env-file` feature in order to read them from an `.env` file.
let creds = Credentials::from_env().unwrap();

// Using every possible scope
let scopes = scopes!(
"user-read-email",
"user-read-private",
"user-top-read",
"user-library-read",
"playlist-read-collaborative",
"playlist-read-private",
"ugc-image-upload",
"playlist-modify-public",
"playlist-modify-private"
);

let oauth = OAuth::from_env(scopes).unwrap();

let spotify = AuthCodeSpotify::new(creds, oauth);

let url = spotify.get_authorize_url(false).unwrap();
// This function requires the `cli` feature enabled.
spotify.prompt_for_token(&url).await.unwrap();
test_playlist(spotify).await;
}
16 changes: 16 additions & 0 deletions src/clients/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,22 @@ where
convert_result(&result)
}

/// Get cover image of a playlist.
///
/// Parameters:
/// - playlist_id - the playlist ID, URI or URL
///
/// [reference](https://developer.spotify.com/documentation/web-api/reference/get-playlist-cover)
async fn playlist_cover_image(
&self,
playlist_id: PlaylistId<'_>,
) -> ClientResult<Image> {
let params = build_map([]);
let url = format!("playlists/{}/images", playlist_id.id());
let result = self.api_get(&url, &params).await?;
convert_result(&result)
}

/// Get Spotify catalog information for a single show identified by its unique Spotify ID.
///
/// Path Parameters:
Expand Down
30 changes: 29 additions & 1 deletion src/clients/oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@
util::{build_map, JsonBuilder},
ClientError, ClientResult, OAuth, Token,
};

use std::collections::HashMap;
use std::{
collections::HashMap,

Check warning on line 15 in src/clients/oauth.rs

View workflow job for this annotation

GitHub Actions / Building

unused import: `collections::HashMap`

Check failure on line 15 in src/clients/oauth.rs

View workflow job for this annotation

GitHub Actions / Building

the name `HashMap` is defined multiple times

Check failure on line 15 in src/clients/oauth.rs

View workflow job for this annotation

GitHub Actions / Test and Lint for each Client (rspotify/cli,rspotify/env-file,rspotify/client-ureq,rspotify/ureq-...

unused import: `collections::HashMap`

Check failure on line 15 in src/clients/oauth.rs

View workflow job for this annotation

GitHub Actions / Test and Lint for each Client (rspotify/cli,rspotify/env-file,rspotify/client-ureq,rspotify/ureq-...

the name `HashMap` is defined multiple times

Check failure on line 15 in src/clients/oauth.rs

View workflow job for this annotation

GitHub Actions / Test and Lint for each Client (rspotify/cli,rspotify/env-file,rspotify/client-ureq,rspotify/ureq-...

unused import: `collections::HashMap`

Check failure on line 15 in src/clients/oauth.rs

View workflow job for this annotation

GitHub Actions / Test and Lint for each Client (rspotify/cli,rspotify/env-file,rspotify/client-ureq,rspotify/ureq-...

the name `HashMap` is defined multiple times
io::{BufRead, BufReader, Write},
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener},
};

use maybe_async::maybe_async;
use rspotify_model::idtypes::{PlayContextId, PlayableId};
use rspotify_http::BaseHttpClient;
use serde_json::{json, Map};
use url::Url;

Expand Down Expand Up @@ -333,6 +334,33 @@
convert_result(&result)
}

/// Replace the image used to represent a specific playlist
///
/// Parameters:
/// - playlist_id - the id of the playlist
/// - image - Base64 encoded JPEG image data, maximum payload size is 256 KB.
/// [Reference] (https://developer.spotify.com/documentation/web-api/reference/upload-custom-playlist-cover)
async fn playlist_upload_cover_image(
&self,
playlist_id: PlaylistId<'_>,
image: &str,
) -> ClientResult<String> {
let url = format!("playlists/{}/images", playlist_id.id());
let url = self.api_url(&url);

let mut headers = self.auth_headers().await?;
let content_type= "Content-Type".to_owned();
let value = String::from("image/jpeg");
headers.insert(content_type, value);

let payload= JsonBuilder::new()
.required("playlist_id", playlist_id.id())
.required("body", image.to_string())
.build();

Ok(self.get_http().put(&url, Some(&headers), &payload).await?)
}

/// Changes a playlist's name and/or public/private state.
///
/// Parameters:
Expand Down
18 changes: 18 additions & 0 deletions tests/test_with_oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,23 @@ async fn check_playlist_create(client: &AuthCodeSpotify) -> FullPlaylist {
playlist
}

#[maybe_async]
async fn check_playlist_cover(client: &AuthCodeSpotify, playlist_id: PlaylistId<'_>) {
// add playlist cover image
let image = "/9j/2wCEABoZGSccJz4lJT5CLy8vQkc9Ozs9R0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0cBHCcnMyYzPSYmPUc9Mj1HR0dEREdHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR//dAAQAAf/uAA5BZG9iZQBkwAAAAAH/wAARCAABAAEDACIAAREBAhEB/8QASwABAQAAAAAAAAAAAAAAAAAAAAYBAQAAAAAAAAAAAAAAAAAAAAAQAQAAAAAAAAAAAAAAAAAAAAARAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwAAARECEQA/AJgAH//Z";
client
.playlist_upload_cover_image(playlist_id.as_ref(), image)
.await
.unwrap();

// check cover image
let cover_res = client
.playlist_cover_image(playlist_id.as_ref())
.await
.unwrap();
assert_eq!(cover_res.url, image);
}

#[maybe_async]
async fn check_num_tracks(client: &AuthCodeSpotify, playlist_id: PlaylistId<'_>, num: i32) {
let fetched_tracks = fetch_all(client.playlist_items(playlist_id, None, None)).await;
Expand Down Expand Up @@ -896,6 +913,7 @@ async fn test_playlist() {
let playlist = check_playlist_create(&client).await;
check_playlist_tracks(&client, &playlist).await;
check_playlist_follow(&client, &playlist).await;
check_playlist_cover(&client, playlist.id).await;
}

#[maybe_async::test(
Expand Down
Loading