Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Getter and Setter of playlist api endpoint #439

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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
@@ -1,5 +1,6 @@
## 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
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ webbrowser = { version = "0.8.0", optional = true }
[dev-dependencies]
env_logger = { version = "0.10.0", default-features = false }
tokio = { version = "1.11.0", features = ["rt-multi-thread", "macros"] }
reqwest = { version = "0.11.18", features = ["blocking"] }
futures-util = "0.3.17"

[features]
Expand Down Expand Up @@ -164,3 +165,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"
255 changes: 255 additions & 0 deletions examples/playlist_with_oauth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
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()
.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()
.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_follow(&client, &playlist).await;
check_playlist_cover(&client, playlist.id).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;
}
19 changes: 19 additions & 0 deletions src/clients/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,25 @@ 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<Option<Image>> {
let url = format!("playlists/{}/images", playlist_id);
let result = self.api_get(&url, &Query::new()).await?;
// if result.is_empty() {
// Ok(None)
// } else {
convert_result(&result)
// }
}

/// Get Spotify catalog information for a single show identified by its unique Spotify ID.
///
/// Path Parameters:
Expand Down
16 changes: 16 additions & 0 deletions src/clients/oauth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,22 @@ pub trait OAuthClient: BaseClient {
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 params = JsonBuilder::new().required("image", image).build();
self.api_put(&url, &params).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 @@ -640,6 +640,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.unwrap().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 @@ -769,6 +786,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(feature = "__sync", async(feature = "__async", tokio::test))]
Expand Down
Loading