From 609219926b0da0aafea4a1f5e2f4a775a6f36eca Mon Sep 17 00:00:00 2001 From: Thomas Ung Date: Mon, 28 Aug 2023 16:48:01 +0200 Subject: [PATCH 1/4] add playlist cover getter and setter --- src/clients/oauth.rs | 31 +++++++++++++++++++++++++++++++ tests/test_with_oauth.rs | 18 ++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/clients/oauth.rs b/src/clients/oauth.rs index 065a3a70..3c24e5ab 100644 --- a/src/clients/oauth.rs +++ b/src/clients/oauth.rs @@ -247,6 +247,37 @@ 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 { + let url = format!("playlists/{}/images", playlist_id.id()); + let params = JsonBuilder::new().required("image", image).build(); + self.api_put(&url, ¶ms).await + } + + + /// Get cover image of a playlist. + /// + /// Parameters: + /// - playlist_id - the playlist ID, URI or URL + async fn playlist_cover_image(&self, playlist_id: PlaylistId<'_>)-> ClientResult>{ + 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) + } + } + /// Changes a playlist's name and/or public/private state. /// /// Parameters: diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 18a2f0d8..208809b6 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -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; @@ -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))] From 44908092eb159b9e18b79d710a1a47715f494170 Mon Sep 17 00:00:00 2001 From: Thomas Ung Date: Wed, 30 Aug 2023 18:49:34 +0200 Subject: [PATCH 2/4] move non oauth function to base.rs --- src/clients/base.rs | 18 ++++++++++++++++++ src/clients/oauth.rs | 17 +---------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/clients/base.rs b/src/clients/base.rs index fbfd1448..0a104bee 100644 --- a/src/clients/base.rs +++ b/src/clients/base.rs @@ -574,6 +574,24 @@ 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> { + 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: diff --git a/src/clients/oauth.rs b/src/clients/oauth.rs index 3c24e5ab..41497764 100644 --- a/src/clients/oauth.rs +++ b/src/clients/oauth.rs @@ -248,7 +248,7 @@ pub trait OAuthClient: BaseClient { } /// 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. @@ -263,21 +263,6 @@ pub trait OAuthClient: BaseClient { self.api_put(&url, ¶ms).await } - - /// Get cover image of a playlist. - /// - /// Parameters: - /// - playlist_id - the playlist ID, URI or URL - async fn playlist_cover_image(&self, playlist_id: PlaylistId<'_>)-> ClientResult>{ - 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) - } - } - /// Changes a playlist's name and/or public/private state. /// /// Parameters: From 66612e48b710134a81f3f31722552916f44d243a Mon Sep 17 00:00:00 2001 From: Thomas Ung Date: Wed, 30 Aug 2023 19:02:40 +0200 Subject: [PATCH 3/4] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4e5f468..99cf0821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. From 1512ac3ffed114758318c28b0babb90910165ebf Mon Sep 17 00:00:00 2001 From: Thomas Ung Date: Fri, 8 Sep 2023 23:30:51 +0200 Subject: [PATCH 4/4] sample example for playlist --- Cargo.toml | 6 + examples/playlist_with_oauth.rs | 255 ++++++++++++++++++++++++++++++++ src/clients/base.rs | 9 +- 3 files changed, 266 insertions(+), 4 deletions(-) create mode 100644 examples/playlist_with_oauth.rs diff --git a/Cargo.toml b/Cargo.toml index 54170160..247189d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] @@ -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" diff --git a/examples/playlist_with_oauth.rs b/examples/playlist_with_oauth.rs new file mode 100644 index 00000000..d269ff4a --- /dev/null +++ b/examples/playlist_with_oauth.rs @@ -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(paginator: Paginator<'_, ClientResult>) -> Vec { + #[cfg(feature = "__async")] + { + use futures::stream::TryStreamExt; + + paginator.try_collect::>().await.unwrap() + } + + #[cfg(feature = "__sync")] + { + paginator.filter_map(|a| a.ok()).collect::>() + } +} + +#[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; +} diff --git a/src/clients/base.rs b/src/clients/base.rs index 0a104bee..24006314 100644 --- a/src/clients/base.rs +++ b/src/clients/base.rs @@ -578,6 +578,7 @@ where /// /// 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, @@ -585,11 +586,11 @@ where ) -> ClientResult> { let url = format!("playlists/{}/images", playlist_id); let result = self.api_get(&url, &Query::new()).await?; - if result.is_empty() { - Ok(None) - } else { + // if result.is_empty() { + // Ok(None) + // } else { convert_result(&result) - } + // } } /// Get Spotify catalog information for a single show identified by its unique Spotify ID.