diff --git a/CHANGELOG.md b/CHANGELOG.md index be4a485b..f42e549f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ If we missed any change or there's something you'd like to discuss about this ve + Remove `itertools` dependency by using the standard library. + Remove `rand` in place of `getrandom` to [reduce total dependencies and compile times](https://github.com/ramsayleung/rspotify/issues/108#issuecomment-673587185). + Cleanup, reduced repetitive code and boilerplate internally in several places ([#117](https://github.com/ramsayleung/rspotify/pull/117), [#113](https://github.com/ramsayleung/rspotify/pull/113), [#107](https://github.com/ramsayleung/rspotify/pull/107), [#106](https://github.com/ramsayleung/rspotify/pull/106)). + + Added internal zero-copy type for Spotify ids, reduced number of allocations/clones ([#161](https://github.com/ramsayleung/rspotify/pull/161)). + Updated dependencies to the latest versions, integrated Dependabot to keep track of them ([#105](https://github.com/ramsayleung/rspotify/pull/105), [#111](https://github.com/ramsayleung/rspotify/pull/111)). - ([#145](https://github.com/ramsayleung/rspotify/pull/145)) Mark `SimplifiedEpisode.language` as deprecated. - ([#145](https://github.com/ramsayleung/rspotify/pull/145)) Derive `PartialEq` and `Eq` for models: @@ -106,15 +107,15 @@ If we missed any change or there's something you'd like to discuss about this ve + The `ClientError::CLI` variant, for whenever user interaction goes wrong - Fix typo in `user_playlist_remove_specific_occurrenes_of_tracks`, now it's `user_playlist_remove_specific_occurrences_of_tracks`. - ([#123](https://github.com/ramsayleung/rspotify/pull/123))All fallible calls in the client return a `ClientError` rather than using `failure`. -- ([#128](https://github.com/ramsayleung/rspotify/pull/128)) Endpoints take `Vec/&[String]` as parameter have changed to `impl IntoIterator`, which is backward compatibility. - + The endpoints which changes parameter from `Vec` to `impl IntoIterator`: +- ([#161](https://github.com/ramsayleung/rspotify/pull/161)) Endpoints taking `Vec/&[String]` as parameter have changed to `impl IntoIterator>`. + + The endpoints which changes parameter from `Vec` to `impl IntoIterator>`: - `artists` - `albums` - `save_shows` - `get_several_episodes` - `check_users_saved_shows` - `remove_users_saved_shows` - + The endpoints which changes parameter from `&[String]` to `impl IntoIterator`: + + The endpoints which changes parameter from `&[String]` to `impl IntoIterator>`: - `user_playlist_add_tracks` - `user_playlist_replace_tracks` - `user_playlist_remove_all_occurrences_of_tracks` @@ -130,6 +131,13 @@ If we missed any change or there's something you'd like to discuss about this ve - `user_follow_users` - `user_unfollow_users` - `audios_features` + + The endpoints which changes parameter from `String` to `&Id`: + - `get_a_show` + - `get_an_episode` + - `get_shows_episodes` + + The endpoint which changes parameter from `Vec>` to `Vec`: + - `playlist_remove_specific_occurrences_of_tracks` +- The `Offset` type is now an enum to match API logic, `Offset::Position` is `u32` now (it's not a position in time, it's a position in a playlist, and you can't have both `position` and `uri` fields at the same time). - ([#128](https://github.com/ramsayleung/rspotify/pull/128)) Rename endpoints with more fitting name: + `audio_analysis` -> `track_analysis` + `audio_features` -> `track_features` @@ -158,6 +166,7 @@ If we missed any change or there's something you'd like to discuss about this ve + Change `{FullArtist, FullPlaylist, PublicUser, PrivateUser}::followers` from `HashMap>` to struct `Followers` + Replace `Actions::disallows` with a `Vec` by removing all entires whose value is false, which will result in a simpler API + Replace `{FullAlbum, SimplifiedEpisode, FullEpisode}::release_date_precision` from `String` to `DatePrecision` enum, makes it easier to use. + + Id and URI parameters are type-safe now everywhere, `Id` and `IdBuf` types for ids/URIs added (non-owning and owning structs). - ([#157](https://github.com/ramsayleung/rspotify/pull/157))Keep polishing models to make it easier to use: + Constrain visibility of `FullArtists` struct with `pub (in crate)`, make `artists` and `artist_related_artists` endpoints return a `Vec` instead. + Constrain visibility of `FullTracks` struct with `pub (in crate)`, make `tracks` and `artist_top_tracks` endpoints return a `Vec` instead. diff --git a/examples/album.rs b/examples/album.rs index 8331f1f3..f2213185 100644 --- a/examples/album.rs +++ b/examples/album.rs @@ -1,4 +1,5 @@ use rspotify::client::SpotifyBuilder; +use rspotify::model::Id; use rspotify::oauth2::CredentialsBuilder; #[tokio::main] @@ -34,7 +35,7 @@ async fn main() { spotify.request_client_token().await.unwrap(); // Running the requests - let birdy_uri = "spotify:album:0sNOF9WDwhWunNAHPD3Baj"; + let birdy_uri = Id::from_uri("spotify:album:0sNOF9WDwhWunNAHPD3Baj").unwrap(); let albums = spotify.album(birdy_uri).await; println!("Response: {:#?}", albums); diff --git a/examples/track.rs b/examples/track.rs index 8db653c2..66446771 100644 --- a/examples/track.rs +++ b/examples/track.rs @@ -1,4 +1,5 @@ use rspotify::client::SpotifyBuilder; +use rspotify::model::Id; use rspotify::oauth2::CredentialsBuilder; #[tokio::main] @@ -34,7 +35,7 @@ async fn main() { spotify.request_client_token().await.unwrap(); // Running the requests - let birdy_uri = "spotify:track:6rqhFgbbKwnb9MLmUQDhG6"; + let birdy_uri = Id::from_uri("spotify:track:6rqhFgbbKwnb9MLmUQDhG6").unwrap(); let track = spotify.track(birdy_uri).await; println!("Response: {:#?}", track); diff --git a/examples/tracks.rs b/examples/tracks.rs index 2a2658c1..7549ecd7 100644 --- a/examples/tracks.rs +++ b/examples/tracks.rs @@ -1,4 +1,5 @@ use rspotify::client::SpotifyBuilder; +use rspotify::model::Id; use rspotify::oauth2::CredentialsBuilder; #[tokio::main] @@ -33,8 +34,8 @@ async fn main() { // so `...` is used instead of `prompt_for_user_token`. spotify.request_client_token().await.unwrap(); - let birdy_uri1 = "spotify:track:3n3Ppam7vgaVa1iaRUc9Lp"; - let birdy_uri2 = "spotify:track:3twNvmDtFQtAd5gMKedhLD"; + let birdy_uri1 = Id::from_uri("spotify:track:3n3Ppam7vgaVa1iaRUc9Lp").unwrap(); + let birdy_uri2 = Id::from_uri("spotify:track:3twNvmDtFQtAd5gMKedhLD").unwrap(); let track_uris = vec![birdy_uri1, birdy_uri2]; let tracks = spotify.tracks(track_uris, None).await; println!("Response: {:?}", tracks); diff --git a/examples/with_refresh_token.rs b/examples/with_refresh_token.rs index 479765fc..8106943c 100644 --- a/examples/with_refresh_token.rs +++ b/examples/with_refresh_token.rs @@ -16,15 +16,16 @@ //! so in the case of Spotify it doesn't seem to revoke them at all. use rspotify::client::{Spotify, SpotifyBuilder}; +use rspotify::model::Id; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; // Sample request that will follow some artists, print the user's // followed artists, and then unfollow the artists. async fn do_things(spotify: Spotify) { let artists = vec![ - "3RGLhK1IP9jnYFH4BRFJBS", // The Clash - "0yNLKJebCb8Aueb54LYya3", // New Order - "2jzc5TC5TVFLXQlBNiIUzE", // a-ha + Id::from_id("3RGLhK1IP9jnYFH4BRFJBS").unwrap(), // The Clash + Id::from_id("0yNLKJebCb8Aueb54LYya3").unwrap(), // New Order + Id::from_id("2jzc5TC5TVFLXQlBNiIUzE").unwrap(), // a-ha ]; spotify .user_follow_artists(artists.clone()) diff --git a/src/client.rs b/src/client.rs index ece26882..27781d1a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -15,6 +15,7 @@ use super::http::{HTTPClient, Query}; use super::json_insert; use super::model::*; use super::oauth2::{Credentials, OAuth, Token}; +use crate::model::idtypes::{IdType, PlayContextIdType}; /// Possible errors returned from the `rspotify` client. #[derive(Debug, Error)] @@ -135,51 +136,11 @@ impl Spotify { .ok_or_else(|| ClientError::InvalidAuth("no oauth configured".to_string())) } - /// TODO: should be moved into a custom type - fn get_uri(&self, _type: Type, _id: &str) -> String { - format!("spotify:{}:{}", _type.to_string(), self.get_id(_type, _id)) - } - /// Converts a JSON response from Spotify into its model. fn convert_result<'a, T: Deserialize<'a>>(&self, input: &'a str) -> ClientResult { serde_json::from_str::(input).map_err(Into::into) } - /// Get spotify id by type and id - /// TODO: should be rewritten and moved into a separate type for IDs - fn get_id(&self, _type: Type, id: &str) -> String { - let mut _id = id.to_owned(); - let fields: Vec<&str> = _id.split(':').collect(); - let len = fields.len(); - if len >= 3 { - if _type.to_string() != fields[len - 2] { - error!( - "expected id of type {:?} but found type {:?} {:?}", - _type, - fields[len - 2], - _id - ); - } else { - return fields[len - 1].to_owned(); - } - } - let sfields: Vec<&str> = _id.split('/').collect(); - let len: usize = sfields.len(); - if len >= 3 { - if _type.to_string() != sfields[len - 2] { - error!( - "expected id of type {:?} but found type {:?} {:?}", - _type, - sfields[len - 2], - _id - ); - } else { - return sfields[len - 1].to_owned(); - } - } - _id.to_owned() - } - /// Append device ID to an API path. fn append_device_id(&self, path: &str, device_id: Option) -> String { let mut new_path = path.to_string(); @@ -200,9 +161,8 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-track) #[maybe_async] - pub async fn track(&self, track_id: &str) -> ClientResult { - let trid = self.get_id(Type::Track, track_id); - let url = format!("tracks/{}", trid); + pub async fn track(&self, track_id: &TrackId) -> ClientResult { + let url = format!("tracks/{}", track_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; self.convert_result(&result) } @@ -217,21 +177,17 @@ impl Spotify { #[maybe_async] pub async fn tracks<'a>( &self, - track_ids: impl IntoIterator, + track_ids: impl IntoIterator, market: Option, ) -> ClientResult> { - // TODO: this can be improved - let mut ids: Vec = vec![]; - for track_id in track_ids { - ids.push(self.get_id(Type::Track, track_id)); - } + let ids = join_ids(track_ids); let mut params = Query::new(); if let Some(market) = market { params.insert("market".to_owned(), market.to_string()); } - let url = format!("tracks/?ids={}", ids.join(",")); + let url = format!("tracks/?ids={}", ids); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result::(&result).map(|x| x.tracks) } @@ -243,9 +199,8 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artist) #[maybe_async] - pub async fn artist(&self, artist_id: &str) -> ClientResult { - let trid = self.get_id(Type::Artist, artist_id); - let url = format!("artists/{}", trid); + pub async fn artist(&self, artist_id: &ArtistId) -> ClientResult { + let url = format!("artists/{}", artist_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; self.convert_result(&result) } @@ -259,13 +214,10 @@ impl Spotify { #[maybe_async] pub async fn artists<'a>( &self, - artist_ids: impl IntoIterator, + artist_ids: impl IntoIterator, ) -> ClientResult> { - let mut ids: Vec = vec![]; - for artist_id in artist_ids { - ids.push(self.get_id(Type::Artist, artist_id)); - } - let url = format!("artists/?ids={}", ids.join(",")); + let ids = join_ids(artist_ids); + let url = format!("artists/?ids={}", ids); let result = self.endpoint_get(&url, &Query::new()).await?; self.convert_result::(&result) @@ -285,7 +237,7 @@ impl Spotify { #[maybe_async] pub async fn artist_albums( &self, - artist_id: &str, + artist_id: &ArtistId, album_type: Option, market: Option, limit: Option, @@ -304,8 +256,7 @@ impl Spotify { if let Some(market) = market { params.insert("market".to_owned(), market.to_string()); } - let trid = self.get_id(Type::Artist, artist_id); - let url = format!("artists/{}/albums", trid); + let url = format!("artists/{}/albums", artist_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) } @@ -321,15 +272,14 @@ impl Spotify { #[maybe_async] pub async fn artist_top_tracks( &self, - artist_id: &str, + artist_id: &ArtistId, market: Market, ) -> ClientResult> { let mut params = Query::with_capacity(1); params.insert("market".to_owned(), market.to_string()); - let trid = self.get_id(Type::Artist, artist_id); - let url = format!("artists/{}/top-tracks", trid); + let url = format!("artists/{}/top-tracks", artist_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result::(&result).map(|x| x.tracks) } @@ -343,9 +293,11 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-related-artists) #[maybe_async] - pub async fn artist_related_artists(&self, artist_id: &str) -> ClientResult> { - let trid = self.get_id(Type::Artist, artist_id); - let url = format!("artists/{}/related-artists", trid); + pub async fn artist_related_artists( + &self, + artist_id: &ArtistId, + ) -> ClientResult> { + let url = format!("artists/{}/related-artists", artist_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; self.convert_result::(&result) .map(|x| x.artists) @@ -358,9 +310,8 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-album) #[maybe_async] - pub async fn album(&self, album_id: &str) -> ClientResult { - let trid = self.get_id(Type::Album, album_id); - let url = format!("albums/{}", trid); + pub async fn album(&self, album_id: &AlbumId) -> ClientResult { + let url = format!("albums/{}", album_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; self.convert_result(&result) @@ -375,13 +326,10 @@ impl Spotify { #[maybe_async] pub async fn albums<'a>( &self, - album_ids: impl IntoIterator, + album_ids: impl IntoIterator, ) -> ClientResult> { - let mut ids: Vec = vec![]; - for album_id in album_ids { - ids.push(self.get_id(Type::Album, album_id)); - } - let url = format!("albums/?ids={}", ids.join(",")); + let ids = join_ids(album_ids); + let url = format!("albums/?ids={}", ids); let result = self.endpoint_get(&url, &Query::new()).await?; self.convert_result::(&result).map(|x| x.albums) } @@ -438,15 +386,14 @@ impl Spotify { #[maybe_async] pub async fn album_track>, O: Into>>( &self, - album_id: &str, + album_id: &AlbumId, limit: L, offset: O, ) -> ClientResult> { let mut params = Query::with_capacity(2); params.insert("limit".to_owned(), limit.into().unwrap_or(50).to_string()); params.insert("offset".to_owned(), offset.into().unwrap_or(0).to_string()); - let trid = self.get_id(Type::Album, album_id); - let url = format!("albums/{}/tracks", trid); + let url = format!("albums/{}/tracks", album_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) } @@ -458,8 +405,8 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-profile) #[maybe_async] - pub async fn user(&self, user_id: &str) -> ClientResult { - let url = format!("users/{}", user_id); + pub async fn user(&self, user_id: &UserId) -> ClientResult { + let url = format!("users/{}", user_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; self.convert_result(&result) } @@ -474,7 +421,7 @@ impl Spotify { #[maybe_async] pub async fn playlist( &self, - playlist_id: &str, + playlist_id: &PlaylistId, fields: Option<&str>, market: Option, ) -> ClientResult { @@ -486,8 +433,7 @@ impl Spotify { params.insert("market".to_owned(), market.to_string()); } - let plid = self.get_id(Type::Playlist, playlist_id); - let url = format!("playlists/{}", plid); + let url = format!("playlists/{}", playlist_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) } @@ -524,14 +470,14 @@ impl Spotify { #[maybe_async] pub async fn user_playlists>, O: Into>>( &self, - user_id: &str, + user_id: &UserId, limit: L, offset: O, ) -> ClientResult> { let mut params = Query::with_capacity(2); params.insert("limit".to_owned(), limit.into().unwrap_or(50).to_string()); params.insert("offset".to_owned(), offset.into().unwrap_or(0).to_string()); - let url = format!("users/{}/playlists", user_id); + let url = format!("users/{}/playlists", user_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) } @@ -547,8 +493,8 @@ impl Spotify { #[maybe_async] pub async fn user_playlist( &self, - user_id: &str, - playlist_id: Option<&mut str>, + user_id: &UserId, + playlist_id: Option<&PlaylistId>, fields: Option<&str>, ) -> ClientResult { let mut params = Query::new(); @@ -557,13 +503,12 @@ impl Spotify { } match playlist_id { Some(playlist_id) => { - let plid = self.get_id(Type::Playlist, playlist_id); - let url = format!("users/{}/playlists/{}", user_id, plid); + let url = format!("users/{}/playlists/{}", user_id.id(), playlist_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) } None => { - let url = format!("users/{}/starred", user_id); + let url = format!("users/{}/starred", user_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) } @@ -583,7 +528,7 @@ impl Spotify { #[maybe_async] pub async fn playlist_tracks>, O: Into>>( &self, - playlist_id: &str, + playlist_id: &PlaylistId, fields: Option<&str>, limit: L, offset: O, @@ -598,8 +543,7 @@ impl Spotify { if let Some(fields) = fields { params.insert("fields".to_owned(), fields.to_owned()); } - let plid = self.get_id(Type::Playlist, playlist_id); - let url = format!("playlists/{}/tracks", plid); + let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) } @@ -616,7 +560,7 @@ impl Spotify { #[maybe_async] pub async fn user_playlist_create>, D: Into>>( &self, - user_id: &str, + user_id: &UserId, name: &str, public: P, description: D, @@ -628,7 +572,7 @@ impl Spotify { "public": public, "description": description }); - let url = format!("users/{}/playlists", user_id); + let url = format!("users/{}/playlists", user_id.id()); let result = self.endpoint_post(&url, ¶ms).await?; self.convert_result(&result) } @@ -692,20 +636,17 @@ impl Spotify { #[maybe_async] pub async fn playlist_add_tracks<'a>( &self, - playlist_id: &str, - track_ids: impl IntoIterator, + playlist_id: &PlaylistId, + track_ids: impl IntoIterator, position: Option, ) -> ClientResult { - let plid = self.get_id(Type::Playlist, playlist_id); - let uris: Vec = track_ids - .into_iter() - .map(|id| self.get_uri(Type::Track, id)) - .collect(); + let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); + let mut params = json!({ "uris": uris }); if let Some(position) = position { json_insert!(params, "position", position); } - let url = format!("playlists/{}/tracks", plid); + let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.endpoint_post(&url, ¶ms).await?; self.convert_result(&result) } @@ -721,18 +662,13 @@ impl Spotify { #[maybe_async] pub async fn playlist_replace_tracks<'a>( &self, - playlist_id: &str, - track_ids: impl IntoIterator, + playlist_id: &PlaylistId, + track_ids: impl IntoIterator, ) -> ClientResult<()> { - let plid = self.get_id(Type::Playlist, playlist_id); - let uris: Vec = track_ids - .into_iter() - .map(|id| self.get_uri(Type::Track, id)) - .collect(); - // let mut params = Map::new(); - // params.insert("uris".to_owned(), uris.into()); + let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); + let params = json!({ "uris": uris }); - let url = format!("playlists/{}/tracks", plid); + let url = format!("playlists/{}/tracks", playlist_id.id()); self.endpoint_put(&url, ¶ms).await?; Ok(()) @@ -752,13 +688,12 @@ impl Spotify { #[maybe_async] pub async fn playlist_reorder_tracks>>( &self, - playlist_id: &str, + playlist_id: &PlaylistId, range_start: i32, range_length: R, insert_before: i32, snapshot_id: Option, ) -> ClientResult { - let plid = self.get_id(Type::Playlist, playlist_id); let mut params = json! ({ "range_start": range_start, "range_length": range_length.into().unwrap_or(1), @@ -768,7 +703,7 @@ impl Spotify { json_insert!(params, "snapshot_id", snapshot_id); } - let url = format!("playlists/{}/tracks", plid); + let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.endpoint_put(&url, ¶ms).await?; self.convert_result(&result) } @@ -784,28 +719,26 @@ impl Spotify { #[maybe_async] pub async fn playlist_remove_all_occurrences_of_tracks<'a>( &self, - playlist_id: &str, - track_ids: impl IntoIterator, + playlist_id: &PlaylistId, + track_ids: impl IntoIterator, snapshot_id: Option, ) -> ClientResult { - let plid = self.get_id(Type::Playlist, playlist_id); - let uris: Vec = track_ids + let tracks = track_ids .into_iter() - .map(|id| self.get_uri(Type::Track, id)) - .collect(); - - // TODO: this can be improved - let mut tracks: Vec> = vec![]; - for uri in uris { - let mut map = Map::new(); - map.insert("uri".to_owned(), uri.into()); - tracks.push(map); - } + .map(|id| { + let mut map = Map::with_capacity(1); + map.insert("uri".to_owned(), id.uri().into()); + map + }) + .collect::>(); + let mut params = json!({ "tracks": tracks }); + if let Some(snapshot_id) = snapshot_id { json_insert!(params, "snapshot_id", snapshot_id); } - let url = format!("playlists/{}/tracks", plid); + + let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.endpoint_delete(&url, ¶ms).await?; self.convert_result(&result) } @@ -842,30 +775,25 @@ impl Spotify { #[maybe_async] pub async fn playlist_remove_specific_occurrences_of_tracks( &self, - playlist_id: &str, - tracks: Vec>, + playlist_id: &PlaylistId, + tracks: Vec>, snapshot_id: Option, ) -> ClientResult { - // TODO: this can be improved - let plid = self.get_id(Type::Playlist, playlist_id); - let mut ftracks: Vec> = vec![]; - for track in tracks { - let mut map = Map::new(); - if let Some(_uri) = track.get("uri") { - let uri = self.get_uri(Type::Track, &_uri.as_str().unwrap().to_owned()); - map.insert("uri".to_owned(), uri.into()); - } - if let Some(_position) = track.get("position") { - map.insert("position".to_owned(), _position.to_owned()); - } - ftracks.push(map); - } + let ftracks = tracks + .into_iter() + .map(|track| { + let mut map = Map::new(); + map.insert("uri".to_owned(), track.id.uri().into()); + map.insert("positions".to_owned(), track.positions.into()); + map + }) + .collect::>(); let mut params = json!({ "tracks": ftracks }); if let Some(snapshot_id) = snapshot_id { json_insert!(params, "snapshot_id", snapshot_id); } - let url = format!("playlists/{}/tracks", plid); + let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.endpoint_delete(&url, ¶ms).await?; self.convert_result(&result) } @@ -879,10 +807,10 @@ impl Spotify { #[maybe_async] pub async fn playlist_follow>>( &self, - playlist_id: &str, + playlist_id: &PlaylistId, public: P, ) -> ClientResult<()> { - let url = format!("playlists/{}/followers", playlist_id); + let url = format!("playlists/{}/followers", playlist_id.id()); self.endpoint_put( &url, @@ -904,18 +832,22 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-if-user-follows-playlist) #[maybe_async] - pub async fn playlist_check_follow( + pub async fn playlist_check_follow<'a>( &self, - playlist_id: &str, - user_ids: &[String], + playlist_id: &PlaylistId, + user_ids: &'a [&'a UserId], ) -> ClientResult> { if user_ids.len() > 5 { error!("The maximum length of user ids is limited to 5 :-)"); } let url = format!( "playlists/{}/followers/contains?ids={}", - playlist_id, - user_ids.join(",") + playlist_id.id(), + user_ids + .iter() + .map(|id| id.id()) + .collect::>() + .join(","), ); let result = self.endpoint_get(&url, &Query::new()).await?; self.convert_result(&result) @@ -1035,13 +967,9 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_delete<'a>( &self, - track_ids: impl IntoIterator, + track_ids: impl IntoIterator, ) -> ClientResult<()> { - let uris: Vec = track_ids - .into_iter() - .map(|id| self.get_id(Type::Track, id)) - .collect(); - let url = format!("me/tracks/?ids={}", uris.join(",")); + let url = format!("me/tracks/?ids={}", join_ids(track_ids)); self.endpoint_delete(&url, &json!({})).await?; Ok(()) @@ -1057,13 +985,9 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_contains<'a>( &self, - track_ids: impl IntoIterator, + track_ids: impl IntoIterator, ) -> ClientResult> { - let uris: Vec = track_ids - .into_iter() - .map(|id| self.get_id(Type::Track, id)) - .collect(); - let url = format!("me/tracks/contains/?ids={}", uris.join(",")); + let url = format!("me/tracks/contains/?ids={}", join_ids(track_ids)); let result = self.endpoint_get(&url, &Query::new()).await?; self.convert_result(&result) } @@ -1077,13 +1001,9 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_tracks_add<'a>( &self, - track_ids: impl IntoIterator, + track_ids: impl IntoIterator, ) -> ClientResult<()> { - let uris: Vec = track_ids - .into_iter() - .map(|id| self.get_id(Type::Track, id)) - .collect(); - let url = format!("me/tracks/?ids={}", uris.join(",")); + let url = format!("me/tracks/?ids={}", join_ids(track_ids)); self.endpoint_put(&url, &json!({})).await?; Ok(()) @@ -1183,13 +1103,9 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_albums_add<'a>( &self, - album_ids: impl IntoIterator, + album_ids: impl IntoIterator, ) -> ClientResult<()> { - let uris: Vec = album_ids - .into_iter() - .map(|id| self.get_id(Type::Album, id)) - .collect(); - let url = format!("me/albums/?ids={}", uris.join(",")); + let url = format!("me/albums/?ids={}", join_ids(album_ids)); self.endpoint_put(&url, &json!({})).await?; Ok(()) @@ -1204,13 +1120,9 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_albums_delete<'a>( &self, - album_ids: impl IntoIterator, + album_ids: impl IntoIterator, ) -> ClientResult<()> { - let uris: Vec = album_ids - .into_iter() - .map(|id| self.get_id(Type::Album, id)) - .collect(); - let url = format!("me/albums/?ids={}", uris.join(",")); + let url = format!("me/albums/?ids={}", join_ids(album_ids)); self.endpoint_delete(&url, &json!({})).await?; Ok(()) @@ -1226,13 +1138,9 @@ impl Spotify { #[maybe_async] pub async fn current_user_saved_albums_contains<'a>( &self, - album_ids: impl IntoIterator, + album_ids: impl IntoIterator, ) -> ClientResult> { - let uris: Vec = album_ids - .into_iter() - .map(|id| self.get_id(Type::Album, id)) - .collect(); - let url = format!("me/albums/contains/?ids={}", uris.join(",")); + let url = format!("me/albums/contains/?ids={}", join_ids(album_ids)); let result = self.endpoint_get(&url, &Query::new()).await?; self.convert_result(&result) } @@ -1246,12 +1154,9 @@ impl Spotify { #[maybe_async] pub async fn user_follow_artists<'a>( &self, - artist_ids: impl IntoIterator, + artist_ids: impl IntoIterator, ) -> ClientResult<()> { - let url = format!( - "me/following?type=artist&ids={}", - artist_ids.into_iter().collect::>().join(",") - ); + let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); self.endpoint_put(&url, &json!({})).await?; Ok(()) @@ -1266,12 +1171,9 @@ impl Spotify { #[maybe_async] pub async fn user_unfollow_artists<'a>( &self, - artist_ids: impl IntoIterator, + artist_ids: impl IntoIterator, ) -> ClientResult<()> { - let url = format!( - "me/following?type=artist&ids={}", - artist_ids.into_iter().collect::>().join(",") - ); + let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); self.endpoint_delete(&url, &json!({})).await?; Ok(()) @@ -1287,11 +1189,11 @@ impl Spotify { #[maybe_async] pub async fn user_artist_check_follow<'a>( &self, - artsit_ids: impl IntoIterator, + artist_ids: impl IntoIterator, ) -> ClientResult> { let url = format!( "me/following/contains?type=artist&ids={}", - artsit_ids.into_iter().collect::>().join(",") + join_ids(artist_ids) ); let result = self.endpoint_get(&url, &Query::new()).await?; self.convert_result(&result) @@ -1306,12 +1208,9 @@ impl Spotify { #[maybe_async] pub async fn user_follow_users<'a>( &self, - user_ids: impl IntoIterator, + user_ids: impl IntoIterator, ) -> ClientResult<()> { - let url = format!( - "me/following?type=user&ids={}", - user_ids.into_iter().collect::>().join(",") - ); + let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); self.endpoint_put(&url, &json!({})).await?; Ok(()) @@ -1326,12 +1225,9 @@ impl Spotify { #[maybe_async] pub async fn user_unfollow_users<'a>( &self, - user_ids: impl IntoIterator, + user_ids: impl IntoIterator, ) -> ClientResult<()> { - let url = format!( - "me/following?type=user&ids={}", - user_ids.into_iter().collect::>().join(",") - ); + let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); self.endpoint_delete(&url, &json!({})).await?; Ok(()) @@ -1494,9 +1390,9 @@ impl Spotify { #[maybe_async] pub async fn recommendations>>( &self, - seed_artists: Option>, + seed_artists: Option>, seed_genres: Option>, - seed_tracks: Option>, + seed_tracks: Option>, limit: L, market: Option, payload: &Map, @@ -1533,21 +1429,15 @@ impl Spotify { } if let Some(seed_artists) = seed_artists { - let seed_artists_ids = seed_artists - .iter() - .map(|id| self.get_id(Type::Artist, id)) - .collect::>(); - params.insert("seed_artists".to_owned(), seed_artists_ids.join(",")); + params.insert("seed_artists".to_owned(), join_ids(seed_artists)); } + if let Some(seed_genres) = seed_genres { params.insert("seed_genres".to_owned(), seed_genres.join(",")); } + if let Some(seed_tracks) = seed_tracks { - let seed_tracks_ids = seed_tracks - .iter() - .map(|id| self.get_id(Type::Track, id)) - .collect::>(); - params.insert("seed_tracks".to_owned(), seed_tracks_ids.join(",")); + params.insert("seed_tracks".to_owned(), join_ids(seed_tracks)); } if let Some(market) = market { params.insert("market".to_owned(), market.to_string()); @@ -1563,9 +1453,8 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-audio-features) #[maybe_async] - pub async fn track_features(&self, track: &str) -> ClientResult { - let track_id = self.get_id(Type::Track, track); - let url = format!("audio-features/{}", track_id); + pub async fn track_features(&self, track_id: &TrackId) -> ClientResult { + let url = format!("audio-features/{}", track_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; self.convert_result(&result) } @@ -1579,13 +1468,9 @@ impl Spotify { #[maybe_async] pub async fn tracks_features<'a>( &self, - tracks: impl IntoIterator, + track_ids: impl IntoIterator, ) -> ClientResult>> { - let ids: Vec = tracks - .into_iter() - .map(|track| self.get_id(Type::Track, track)) - .collect(); - let url = format!("audio-features/?ids={}", ids.join(",")); + let url = format!("audio-features/?ids={}", join_ids(track_ids)); let result = self.endpoint_get(&url, &Query::new()).await?; if result.is_empty() { @@ -1603,9 +1488,8 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-audio-analysis) #[maybe_async] - pub async fn track_analysis(&self, track: &str) -> ClientResult { - let trid = self.get_id(Type::Track, track); - let url = format!("audio-analysis/{}", trid); + pub async fn track_analysis(&self, track_id: &TrackId) -> ClientResult { + let url = format!("audio-analysis/{}", track_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; self.convert_result(&result) } @@ -1745,29 +1629,60 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-start-a-users-playback) #[maybe_async] - pub async fn start_playback( + pub async fn start_context_playback( &self, + context_uri: &Id, device_id: Option, - context_uri: Option, - uris: Option>, - offset: Option, - position_ms: Option, + offset: Option>, + position_ms: Option, ) -> ClientResult<()> { - if context_uri.is_some() && uris.is_some() { - error!("specify either contexxt uri or uris, not both"); - } + use super::model::Offset; + let mut params = json!({}); - if let Some(context_uri) = context_uri { - json_insert!(params, "context_uri", context_uri); - } - if let Some(uris) = uris { - json_insert!(params, "uris", uris); + json_insert!(params, "context_uri", context_uri.uri()); + if let Some(offset) = offset { + match offset { + Offset::Position(position) => { + json_insert!(params, "offset", json!({ "position": position })); + } + Offset::Uri(uri) => { + json_insert!(params, "offset", json!({ "uri": uri.uri() })); + } + } } + if let Some(position_ms) = position_ms { + json_insert!(params, "position_ms", position_ms); + }; + let url = self.append_device_id("me/player/play", device_id); + self.put(&url, None, ¶ms).await?; + + Ok(()) + } + + #[maybe_async] + pub async fn start_uris_playback( + &self, + uris: &[&Id], + device_id: Option, + offset: Option>, + position_ms: Option, + ) -> ClientResult<()> { + use super::model::Offset; + + let mut params = json!({}); + json_insert!( + params, + "uris", + uris.iter().map(|id| id.uri()).collect::>() + ); if let Some(offset) = offset { - if let Some(position) = offset.position { - json_insert!(params, "offset", json!({ "position": position })); - } else if let Some(uri) = offset.uri { - json_insert!(params, "offset", json!({ "uri": uri })); + match offset { + Offset::Position(position) => { + json_insert!(params, "offset", json!({ "position": position })); + } + Offset::Uri(uri) => { + json_insert!(params, "offset", json!({ "uri": uri.uri() })); + } } } if let Some(position_ms) = position_ms { @@ -1907,12 +1822,12 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-add-to-queue) #[maybe_async] - pub async fn add_item_to_queue( + pub async fn add_item_to_queue( &self, - item: String, + item: &Id, device_id: Option, ) -> ClientResult<()> { - let url = self.append_device_id(&format!("me/player/queue?uri={}", &item), device_id); + let url = self.append_device_id(&format!("me/player/queue?uri={}", item), device_id); self.endpoint_post(&url, &json!({})).await?; Ok(()) @@ -1926,9 +1841,11 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-shows-user) #[maybe_async] - pub async fn save_shows<'a>(&self, ids: impl IntoIterator) -> ClientResult<()> { - let joined_ids = ids.into_iter().collect::>().join(","); - let url = format!("me/shows/?ids={}", joined_ids); + pub async fn save_shows<'a>( + &self, + show_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/shows/?ids={}", join_ids(show_ids)); self.endpoint_put(&url, &json!({})).await?; Ok(()) @@ -1967,12 +1884,12 @@ impl Spotify { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-show) #[maybe_async] - pub async fn get_a_show(&self, id: String, market: Option) -> ClientResult { + pub async fn get_a_show(&self, id: &ShowId, market: Option) -> ClientResult { let mut params = Query::new(); if let Some(market) = market { params.insert("market".to_owned(), market.to_string()); } - let url = format!("shows/{}", id); + let url = format!("shows/{}", id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) } @@ -1988,15 +1905,11 @@ impl Spotify { #[maybe_async] pub async fn get_several_shows<'a>( &self, - ids: impl IntoIterator, + ids: impl IntoIterator, market: Option, ) -> ClientResult> { - // TODO: This can probably be better let mut params = Query::with_capacity(1); - params.insert( - "ids".to_owned(), - ids.into_iter().collect::>().join(","), - ); + params.insert("ids".to_owned(), join_ids(ids)); if let Some(market) = market { params.insert("market".to_owned(), market.to_string()); } @@ -2020,7 +1933,7 @@ impl Spotify { #[maybe_async] pub async fn get_shows_episodes>, O: Into>>( &self, - id: String, + id: &ShowId, limit: L, offset: O, market: Option, @@ -2031,7 +1944,7 @@ impl Spotify { if let Some(market) = market { params.insert("market".to_owned(), market.to_string()); } - let url = format!("shows/{}/episodes", id); + let url = format!("shows/{}/episodes", id.id()); let result = self.endpoint_get(&url, ¶ms).await?; self.convert_result(&result) } @@ -2048,10 +1961,10 @@ impl Spotify { #[maybe_async] pub async fn get_an_episode( &self, - id: String, + id: &EpisodeId, market: Option, ) -> ClientResult { - let url = format!("episodes/{}", id); + let url = format!("episodes/{}", id.id()); let mut params = Query::new(); if let Some(market) = market { params.insert("market".to_owned(), market.to_string()); @@ -2071,14 +1984,11 @@ impl Spotify { #[maybe_async] pub async fn get_several_episodes<'a>( &self, - ids: impl IntoIterator, + ids: impl IntoIterator, market: Option, ) -> ClientResult { let mut params = Query::with_capacity(1); - params.insert( - "ids".to_owned(), - ids.into_iter().collect::>().join(","), - ); + params.insert("ids".to_owned(), join_ids(ids)); if let Some(market) = market { params.insert("market".to_owned(), market.to_string()); } @@ -2095,13 +2005,10 @@ impl Spotify { #[maybe_async] pub async fn check_users_saved_shows<'a>( &self, - ids: impl IntoIterator, + ids: impl IntoIterator, ) -> ClientResult> { let mut params = Query::with_capacity(1); - params.insert( - "ids".to_owned(), - ids.into_iter().collect::>().join(","), - ); + params.insert("ids".to_owned(), join_ids(ids)); let result = self.endpoint_get("me/shows/contains", ¶ms).await?; self.convert_result(&result) } @@ -2117,11 +2024,10 @@ impl Spotify { #[maybe_async] pub async fn remove_users_saved_shows<'a>( &self, - ids: impl IntoIterator, + show_ids: impl IntoIterator, market: Option, ) -> ClientResult<()> { - let joined_ids = ids.into_iter().collect::>().join(","); - let url = format!("me/shows?ids={}", joined_ids); + let url = format!("me/shows?ids={}", join_ids(show_ids)); let mut params = json!({}); if let Some(market) = market { json_insert!(params, "country", market.to_string()); @@ -2132,6 +2038,11 @@ impl Spotify { } } +#[inline] +fn join_ids<'a, T: 'a + IdType>(ids: impl IntoIterator>) -> String { + ids.into_iter().collect::>().join(",") +} + #[cfg(test)] mod tests { use super::*; @@ -2143,51 +2054,4 @@ mod tests { let code = spotify.parse_response_code(url).unwrap(); assert_eq!(code, "AQD0yXvFEOvw"); } - - #[test] - fn test_get_id() { - // Assert artist - let spotify = SpotifyBuilder::default().build().unwrap(); - let artist_id = "spotify:artist:2WX2uTcsvV5OnS0inACecP"; - let id = spotify.get_id(Type::Artist, artist_id); - assert_eq!("2WX2uTcsvV5OnS0inACecP", &id); - - // Assert album - let artist_id_a = "spotify/album/2WX2uTcsvV5OnS0inACecP"; - assert_eq!( - "2WX2uTcsvV5OnS0inACecP", - &spotify.get_id(Type::Album, artist_id_a) - ); - - // Mismatch type - let artist_id_b = "spotify:album:2WX2uTcsvV5OnS0inACecP"; - assert_eq!( - "spotify:album:2WX2uTcsvV5OnS0inACecP", - &spotify.get_id(Type::Artist, artist_id_b) - ); - - // Could not split - let artist_id_c = "spotify-album-2WX2uTcsvV5OnS0inACecP"; - assert_eq!( - "spotify-album-2WX2uTcsvV5OnS0inACecP", - &spotify.get_id(Type::Artist, artist_id_c) - ); - - let playlist_id = "spotify:playlist:59ZbFPES4DQwEjBpWHzrtC"; - assert_eq!( - "59ZbFPES4DQwEjBpWHzrtC", - &spotify.get_id(Type::Playlist, playlist_id) - ); - } - - #[test] - fn test_get_uri() { - let spotify = SpotifyBuilder::default().build().unwrap(); - let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; - let track_id2 = "1301WleyT98MSxVHPZCA6M"; - let uri1 = spotify.get_uri(Type::Track, track_id1); - let uri2 = spotify.get_uri(Type::Track, track_id2); - assert_eq!(track_id1, uri1); - assert_eq!("spotify:track:1301WleyT98MSxVHPZCA6M", &uri2); - } } diff --git a/src/model/enums/types.rs b/src/model/enums/types.rs index ac150eb5..5ffb066e 100644 --- a/src/model/enums/types.rs +++ b/src/model/enums/types.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use strum::ToString; +use strum::{Display, EnumString, ToString}; /// Copyright type: `C` = the copyright, `P` = the sound recording (performance) /// copyright. @@ -29,7 +29,7 @@ pub enum AlbumType { } /// Type: `artist`, `album`, `track`, `playlist`, `show` or `episode` -#[derive(Clone, Serialize, Deserialize, Copy, PartialEq, Eq, Debug, ToString)] +#[derive(Clone, Serialize, Deserialize, Copy, PartialEq, Eq, Debug, Display, EnumString)] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] pub enum Type { diff --git a/src/model/idtypes.rs b/src/model/idtypes.rs new file mode 100644 index 00000000..c2091ca7 --- /dev/null +++ b/src/model/idtypes.rs @@ -0,0 +1,301 @@ +use crate::model::Type; +use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; +use std::marker::PhantomData; +use std::ops::Deref; +use strum::Display; +use thiserror::Error; + +// This is a sealed trait pattern implementation, it stops external code from +// implementing the `IdType` trait. The `Sealed` trait must be in a private mod, +// so external code can not see and implement it. +// See also: https://rust-lang.github.io/api-guidelines/future-proofing.html +mod private { + pub trait Sealed {} +} + +pub trait IdType: private::Sealed { + const TYPE: Type; +} +pub trait PlayableIdType: IdType {} +pub trait PlayContextIdType: IdType {} + +macro_rules! sealed_types { + ($($name:ident),+) => { + $( + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub enum $name {} + impl private::Sealed for $name {} + impl IdType for $name { + const TYPE: Type = Type::$name; + } + )+ + } +} + +sealed_types!(Artist, Album, Track, Playlist, User, Show, Episode); + +impl PlayContextIdType for Artist {} +impl PlayContextIdType for Album {} +impl PlayableIdType for Track {} +impl PlayContextIdType for Playlist {} +impl PlayContextIdType for Show {} +impl PlayableIdType for Episode {} + +pub type ArtistId = Id; +pub type AlbumId = Id; +pub type TrackId = Id; +pub type PlaylistId = Id; +pub type UserId = Id; +pub type ShowId = Id; +pub type EpisodeId = Id; + +pub type ArtistIdBuf = IdBuf; +pub type AlbumIdBuf = IdBuf; +pub type TrackIdBuf = IdBuf; +pub type PlaylistIdBuf = IdBuf; +pub type UserIdBuf = IdBuf; +pub type ShowIdBuf = IdBuf; +pub type EpisodeIdBuf = IdBuf; + +/// A Spotify object id of given [type](crate::model::enums::types::Type) +/// +/// This is a not-owning type, it stores a &str only. +/// See [IdBuf](crate::model::idtypes::IdBuf) for owned version of the type. +#[derive(Debug, PartialEq, Eq, Serialize)] +pub struct Id { + #[serde(default)] + _type: PhantomData, + #[serde(flatten)] + id: str, +} + +/// A Spotify object id of given [type](crate::model::enums::types::Type) +/// +/// This is an owning type, it stores a String. +/// See [Id](crate::model::idtypes::Id) for light-weight non-owning type. +/// +/// Use `Id::from_id(val).to_owned()`, `Id::from_uri(val).to_owned()` or +/// `Id::from_id_or_uri(val).to_owned()` to construct an instance of this type. +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] +pub struct IdBuf { + #[serde(default)] + _type: PhantomData, + #[serde(flatten)] + id: String, +} + +impl AsRef> for IdBuf { + fn as_ref(&self) -> &Id { + // Safe, b/c of the same T between types, IdBuf can't be constructed + // from invalid id, and Id is just a wrapped str with ZST type tag + unsafe { &*(&*self.id as *const str as *const Id) } + } +} + +impl Borrow> for IdBuf { + fn borrow(&self) -> &Id { + self.as_ref() + } +} + +impl Deref for IdBuf { + type Target = Id; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl IdBuf { + /// Get a [`Type`](crate::model::enums::types::Type) of the id + pub fn _type(&self) -> Type { + T::TYPE + } + + /// Spotify object id (guaranteed to be a string of alphanumeric characters) + pub fn id(&self) -> &str { + &self.id + } + + /// Spotify object URI in a well-known format: spotify:type:id + pub fn uri(&self) -> String { + self.as_ref().uri() + } + + /// Full Spotify object URL, can be opened in a browser + pub fn url(&self) -> String { + self.as_ref().url() + } +} + +/// Spotify id or URI parsing error +/// +/// See also [`Id`](crate::model::idtypes::Id) for details. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Display, Error)] +pub enum IdError { + /// Spotify URI prefix is not `spotify:` or `spotify/` + InvalidPrefix, + /// Spotify URI can't be split into type and id parts + /// (e.g. it has invalid separator) + InvalidFormat, + /// Spotify URI has invalid type name, or id has invalid type in a given + /// context (e.g. a method expects a track id, but artist id is provided) + InvalidType, + /// Spotify id is invalid (empty or contains non-alphanumeric characters) + InvalidId, +} + +impl std::fmt::Display for &Id { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "spotify:{}:{}", T::TYPE, &self.id) + } +} + +impl AsRef for &Id { + fn as_ref(&self) -> &str { + &self.id + } +} + +impl Borrow for &Id { + fn borrow(&self) -> &str { + &self.id + } +} + +impl std::str::FromStr for IdBuf { + type Err = IdError; + + fn from_str(s: &str) -> Result { + Id::from_id_or_uri(s).map(|id| id.to_owned()) + } +} + +impl Id { + /// Owned version of the id [`IdBuf`](crate::model::idtypes::IdBuf) + pub fn to_owned(&self) -> IdBuf { + IdBuf { + _type: PhantomData, + id: (&self.id).to_owned(), + } + } + + /// Spotify object type + pub fn _type(&self) -> Type { + T::TYPE + } + + /// Spotify object id (guaranteed to be a string of alphanumeric characters) + pub fn id(&self) -> &str { + &self.id + } + + /// Spotify object URI in a well-known format: spotify:type:id + /// + /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, + /// `spotify:track:4y4VO05kYgUTo2bzbox1an`. + pub fn uri(&self) -> String { + format!("spotify:{}:{}", T::TYPE, &self.id) + } + + /// Full Spotify object URL, can be opened in a browser + /// + /// Examples: https://open.spotify.com/track/4y4VO05kYgUTo2bzbox1an, + /// https://open.spotify.com/artist/2QI8e2Vwgg9KXOz2zjcrkI + pub fn url(&self) -> String { + format!("https://open.spotify.com/{}/{}", T::TYPE, &self.id) + } + + /// Parse Spotify id or URI from string slice + /// + /// Spotify URI must be in one of the following formats: + /// `spotify:{type}:{id}` or `spotify/{type}/{id}`. + /// Where `{type}` is one of `artist`, `album`, `track`, `playlist`, + /// `user`, `show`, or `episode`, and `{id}` is a non-empty + /// alphanumeric string. + /// The URI must be of given `T`ype, otherwise `IdError::InvalidType` + /// error is returned. + /// + /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, + /// `spotify/track/4y4VO05kYgUTo2bzbox1an`. + /// + /// If input string is not a valid Spotify URI (it's not started with + /// `spotify:` or `spotify/`), it must be a valid Spotify object id, + /// i.e. a non-empty alphanumeric string. + /// + /// # Errors: + /// + /// - `IdError::InvalidType` - if `id_or_uri` is an URI, and it's type part + /// is not equal to `T`, + /// - `IdError::InvalidId` - either if `id_or_uri` is an URI with invalid id + /// part, or it's an invalid id (id is invalid if it contains + /// non-alphanumeric characters), + /// - `IdError::InvalidFormat` - if `id_or_uri` is an URI, and it can't be + /// split into type and id parts. + pub fn from_id_or_uri<'a, 'b: 'a>(id_or_uri: &'b str) -> Result<&'a Id, IdError> { + match Id::::from_uri(id_or_uri) { + Ok(id) => Ok(id), + Err(IdError::InvalidPrefix) => Id::::from_id(id_or_uri), + Err(error) => Err(error), + } + } + + /// Parse Spotify id from string slice + /// + /// A valid Spotify object id must be a non-empty alphanumeric string. + /// + /// # Errors: + /// + /// - `IdError::InvalidId` - if `id` contains non-alphanumeric characters. + pub fn from_id<'a, 'b: 'a>(id: &'b str) -> Result<&'a Id, IdError> { + if id.chars().all(|ch| ch.is_ascii_alphanumeric()) { + // Safe, b/c Id is just a str with ZST type tag, and id is proved + // to be a valid id at this point + Ok(unsafe { &*(id as *const str as *const Id) }) + } else { + Err(IdError::InvalidId) + } + } + + /// Parse Spotify URI from string slice + /// + /// Spotify URI must be in one of the following formats: + /// `spotify:{type}:{id}` or `spotify/{type}/{id}`. + /// Where `{type}` is one of `artist`, `album`, `track`, `playlist`, `user`, + /// `show`, or `episode`, and `{id}` is a non-empty alphanumeric string. + /// + /// Examples: `spotify:album:6IcGNaXFRf5Y1jc7QsE9O2`, + /// `spotify/track/4y4VO05kYgUTo2bzbox1an`. + /// + /// # Errors: + /// + /// - `IdError::InvalidPrefix` - if `uri` is not started with `spotify:` + /// or `spotify/`, + /// - `IdError::InvalidType` - if type part of an `uri` is not a valid + /// Spotify type `T`, + /// - `IdError::InvalidId` - if id part of an `uri` is not a valid id, + /// - `IdError::InvalidFormat` - if it can't be splitted into type and + /// id parts. + pub fn from_uri<'a, 'b: 'a>(uri: &'b str) -> Result<&'a Id, IdError> { + let mut chars = uri + .strip_prefix("spotify") + .ok_or(IdError::InvalidPrefix)? + .chars(); + let sep = match chars.next() { + Some(ch) if ch == '/' || ch == ':' => ch, + _ => return Err(IdError::InvalidPrefix), + }; + let rest = chars.as_str(); + + let (tpe, id) = rest + .rfind(sep) + .map(|mid| rest.split_at(mid)) + .ok_or(IdError::InvalidFormat)?; + + match tpe.parse::() { + Ok(tpe) if tpe == T::TYPE => Id::::from_id(&id[1..]), + _ => Err(IdError::InvalidType), + } + } +} diff --git a/src/model/mod.rs b/src/model/mod.rs index 7295b408..edd9390d 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -6,6 +6,7 @@ pub mod category; pub mod context; pub mod device; pub mod enums; +pub mod idtypes; pub mod image; pub mod offset; pub mod page; @@ -220,8 +221,67 @@ pub enum PlayingItem { Episode(show::FullEpisode), } +pub use idtypes::{ + AlbumId, AlbumIdBuf, ArtistId, ArtistIdBuf, EpisodeId, EpisodeIdBuf, Id, IdBuf, IdError, + PlayableIdType, PlaylistId, PlaylistIdBuf, ShowId, ShowIdBuf, TrackId, TrackIdBuf, UserId, + UserIdBuf, +}; pub use { album::*, artist::*, audio::*, category::*, context::*, device::*, enums::*, image::*, offset::*, page::*, playing::*, playlist::*, recommend::*, search::*, show::*, track::*, user::*, }; + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::{Id, IdError}; + + #[test] + fn test_get_id() { + // Assert artist + let artist_id = "spotify:artist:2WX2uTcsvV5OnS0inACecP"; + let id = Id::::from_id_or_uri(artist_id).unwrap(); + assert_eq!("2WX2uTcsvV5OnS0inACecP", id.id()); + + // Assert album + let album_id_a = "spotify/album/2WX2uTcsvV5OnS0inACecP"; + assert_eq!( + "2WX2uTcsvV5OnS0inACecP", + Id::::from_id_or_uri(album_id_a) + .unwrap() + .id() + ); + + // Mismatch type + assert_eq!( + Err(IdError::InvalidType), + Id::::from_id_or_uri(album_id_a) + ); + + // Could not split + let artist_id_c = "spotify-album-2WX2uTcsvV5OnS0inACecP"; + assert_eq!( + Err(IdError::InvalidId), + Id::::from_id_or_uri(artist_id_c) + ); + + let playlist_id = "spotify:playlist:59ZbFPES4DQwEjBpWHzrtC"; + assert_eq!( + "59ZbFPES4DQwEjBpWHzrtC", + Id::::from_id_or_uri(playlist_id) + .unwrap() + .id() + ); + } + + #[test] + fn test_get_uri() { + let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; + let track_id2 = "1301WleyT98MSxVHPZCA6M"; + let id1 = Id::::from_id_or_uri(track_id1).unwrap(); + let id2 = Id::::from_id_or_uri(track_id2).unwrap(); + assert_eq!(track_id1, &id1.uri()); + assert_eq!("spotify:track:1301WleyT98MSxVHPZCA6M", &id2.uri()); + } +} diff --git a/src/model/offset.rs b/src/model/offset.rs index 732e40a5..80908062 100644 --- a/src/model/offset.rs +++ b/src/model/offset.rs @@ -1,29 +1,20 @@ //! Offset object -use crate::model::option_duration_ms; -use serde::{Deserialize, Serialize}; -use std::time::Duration; +use crate::model::{Id, IdBuf, PlayableIdType}; /// Offset object /// -/// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-start-a-users-playback) -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct Offset { - #[serde(default)] - #[serde(with = "option_duration_ms")] - pub position: Option, - pub uri: Option, +/// [Reference](https://developer.spotify.com/documentation/web-api/reference/player/start-a-users-playback/) +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Offset { + Position(u32), + Uri(IdBuf), } -pub fn for_position(position: u64) -> Option { - Some(Offset { - position: Some(Duration::from_millis(position)), - uri: None, - }) -} - -pub fn for_uri(uri: String) -> Option { - Some(Offset { - position: None, - uri: Some(uri), - }) +impl Offset { + pub fn for_position(position: u32) -> Offset { + Offset::Position(position) + } + pub fn for_uri(uri: &Id) -> Offset { + Offset::Uri(uri.to_owned()) + } } diff --git a/src/model/track.rs b/src/model/track.rs index c89dc860..fde2dd50 100644 --- a/src/model/track.rs +++ b/src/model/track.rs @@ -7,8 +7,7 @@ use std::{collections::HashMap, time::Duration}; use super::album::SimplifiedAlbum; use super::artist::SimplifiedArtist; use super::Restriction; -use crate::model::duration_ms; -use crate::model::Type; +use crate::model::{duration_ms, TrackId, Type}; /// Full track object /// @@ -102,3 +101,16 @@ pub struct SavedTrack { pub added_at: DateTime, pub track: FullTrack, } + +/// Track id with specific positions track in a playlist +pub struct TrackPositions<'id> { + pub id: &'id TrackId, + pub positions: Vec, +} + +impl<'id> TrackPositions<'id> { + /// Track in a playlist by an id + pub fn new(id: &'id TrackId, positions: Vec) -> Self { + Self { id, positions } + } +} diff --git a/tests/test_models.rs b/tests/test_models.rs index 9d4c59a2..891dc5bf 100644 --- a/tests/test_models.rs +++ b/tests/test_models.rs @@ -815,27 +815,6 @@ fn test_current_playback_context() { assert!(current_playback_context.progress.is_none()); } -#[test] -fn test_offset() { - let json = r#" - { - "position": 5, - "uri": "spotify:track:1301WleyT98MSxVHPZCA6M" - } - "#; - let offset: Offset = serde_json::from_str(&json).unwrap(); - let duration = Duration::from_millis(5); - assert_eq!(offset.position, Some(duration)); - - let empty_json = r#" - { - - } - "#; - let empty_offset: Offset = serde_json::from_str(&empty_json).unwrap(); - assert!(empty_offset.position.is_none()); -} - #[test] fn test_audio_analysis_track() { let json = r#" diff --git a/tests/test_with_credential.rs b/tests/test_with_credential.rs index 70acfa05..a85f49b3 100644 --- a/tests/test_with_credential.rs +++ b/tests/test_with_credential.rs @@ -1,11 +1,10 @@ mod common; use common::maybe_async_test; -use rspotify::model::{AlbumType, Country}; use rspotify::oauth2::CredentialsBuilder; use rspotify::{ client::{Spotify, SpotifyBuilder}, - model::Market, + model::{AlbumType, Country, Id, Market}, }; use maybe_async::maybe_async; @@ -35,16 +34,16 @@ pub async fn creds_client() -> Spotify { #[maybe_async] #[maybe_async_test] async fn test_album() { - let birdy_uri = "spotify:album:0sNOF9WDwhWunNAHPD3Baj"; + let birdy_uri = Id::from_uri("spotify:album:0sNOF9WDwhWunNAHPD3Baj").unwrap(); creds_client().await.album(birdy_uri).await.unwrap(); } #[maybe_async] #[maybe_async_test] async fn test_albums() { - let birdy_uri1 = "spotify:album:41MnTivkwTO3UUJ8DrqEJJ"; - let birdy_uri2 = "spotify:album:6JWc4iAiJ9FjyK0B59ABb4"; - let birdy_uri3 = "spotify:album:6UXCm6bOO4gFlDQZV5yL37"; + let birdy_uri1 = Id::from_uri("spotify:album:41MnTivkwTO3UUJ8DrqEJJ").unwrap(); + let birdy_uri2 = Id::from_uri("spotify:album:6JWc4iAiJ9FjyK0B59ABb4").unwrap(); + let birdy_uri3 = Id::from_uri("spotify:album:6UXCm6bOO4gFlDQZV5yL37").unwrap(); let track_uris = vec![birdy_uri1, birdy_uri2, birdy_uri3]; creds_client().await.albums(track_uris).await.unwrap(); } @@ -52,7 +51,7 @@ async fn test_albums() { #[maybe_async] #[maybe_async_test] async fn test_album_tracks() { - let birdy_uri = "spotify:album:6akEvsycLGftJxYudPjmqK"; + let birdy_uri = Id::from_uri("spotify:album:6akEvsycLGftJxYudPjmqK").unwrap(); creds_client() .await .album_track(birdy_uri, Some(2), None) @@ -63,7 +62,7 @@ async fn test_album_tracks() { #[maybe_async] #[maybe_async_test] async fn test_artist_related_artists() { - let birdy_uri = "spotify:artist:43ZHCT0cAZBISjO8DG9PnE"; + let birdy_uri = Id::from_uri("spotify:artist:43ZHCT0cAZBISjO8DG9PnE").unwrap(); creds_client() .await .artist_related_artists(birdy_uri) @@ -74,14 +73,14 @@ async fn test_artist_related_artists() { #[maybe_async] #[maybe_async_test] async fn test_artist() { - let birdy_uri = "spotify:artist:2WX2uTcsvV5OnS0inACecP"; + let birdy_uri = Id::from_uri("spotify:artist:2WX2uTcsvV5OnS0inACecP").unwrap(); creds_client().await.artist(birdy_uri).await.unwrap(); } #[maybe_async] #[maybe_async_test] async fn test_artists_albums() { - let birdy_uri = "spotify:artist:2WX2uTcsvV5OnS0inACecP"; + let birdy_uri = Id::from_uri("spotify:artist:2WX2uTcsvV5OnS0inACecP").unwrap(); creds_client() .await .artist_albums( @@ -98,8 +97,8 @@ async fn test_artists_albums() { #[maybe_async] #[maybe_async_test] async fn test_artists() { - let birdy_uri1 = "spotify:artist:0oSGxfWSnnOXhD2fKuz2Gy"; - let birdy_uri2 = "spotify:artist:3dBVyJ7JuOMt4GE9607Qin"; + let birdy_uri1 = Id::from_uri("spotify:artist:0oSGxfWSnnOXhD2fKuz2Gy").unwrap(); + let birdy_uri2 = Id::from_uri("spotify:artist:3dBVyJ7JuOMt4GE9607Qin").unwrap(); let artist_uris = vec![birdy_uri1, birdy_uri2]; creds_client().await.artists(artist_uris).await.unwrap(); } @@ -107,7 +106,7 @@ async fn test_artists() { #[maybe_async] #[maybe_async_test] async fn test_artist_top_tracks() { - let birdy_uri = "spotify:artist:2WX2uTcsvV5OnS0inACecP"; + let birdy_uri = Id::from_uri("spotify:artist:2WX2uTcsvV5OnS0inACecP").unwrap(); creds_client() .await .artist_top_tracks(birdy_uri, Market::Country(Country::UnitedStates)) @@ -118,14 +117,14 @@ async fn test_artist_top_tracks() { #[maybe_async] #[maybe_async_test] async fn test_audio_analysis() { - let track = "06AKEBrKUckW0KREUWRnvT"; + let track = Id::from_id("06AKEBrKUckW0KREUWRnvT").unwrap(); creds_client().await.track_analysis(track).await.unwrap(); } #[maybe_async] #[maybe_async_test] async fn test_audio_features() { - let track = "spotify:track:06AKEBrKUckW0KREUWRnvT"; + let track = Id::from_uri("spotify:track:06AKEBrKUckW0KREUWRnvT").unwrap(); creds_client().await.track_features(track).await.unwrap(); } @@ -133,9 +132,9 @@ async fn test_audio_features() { #[maybe_async_test] async fn test_audios_features() { let mut tracks_ids = vec![]; - let track_id1 = "spotify:track:4JpKVNYnVcJ8tuMKjAj50A"; + let track_id1 = Id::from_uri("spotify:track:4JpKVNYnVcJ8tuMKjAj50A").unwrap(); tracks_ids.push(track_id1); - let track_id2 = "spotify:track:24JygzOLM0EmRQeGtFcIcG"; + let track_id2 = Id::from_uri("spotify:track:24JygzOLM0EmRQeGtFcIcG").unwrap(); tracks_ids.push(track_id2); creds_client() .await @@ -147,22 +146,22 @@ async fn test_audios_features() { #[maybe_async] #[maybe_async_test] async fn test_user() { - let birdy_uri = String::from("tuggareutangranser"); - creds_client().await.user(&birdy_uri).await.unwrap(); + let birdy_uri = Id::from_id("tuggareutangranser").unwrap(); + creds_client().await.user(birdy_uri).await.unwrap(); } #[maybe_async] #[maybe_async_test] async fn test_track() { - let birdy_uri = "spotify:track:6rqhFgbbKwnb9MLmUQDhG6"; + let birdy_uri = Id::from_uri("spotify:track:6rqhFgbbKwnb9MLmUQDhG6").unwrap(); creds_client().await.track(birdy_uri).await.unwrap(); } #[maybe_async] #[maybe_async_test] async fn test_tracks() { - let birdy_uri1 = "spotify:track:3n3Ppam7vgaVa1iaRUc9Lp"; - let birdy_uri2 = "spotify:track:3twNvmDtFQtAd5gMKedhLD"; + let birdy_uri1 = Id::from_uri("spotify:track:3n3Ppam7vgaVa1iaRUc9Lp").unwrap(); + let birdy_uri2 = Id::from_uri("spotify:track:3twNvmDtFQtAd5gMKedhLD").unwrap(); let track_uris = vec![birdy_uri1, birdy_uri2]; creds_client().await.tracks(track_uris, None).await.unwrap(); } @@ -172,7 +171,7 @@ async fn test_tracks() { async fn test_existing_playlist() { creds_client() .await - .playlist("37i9dQZF1DZ06evO45P0Eo", None, None) + .playlist(Id::from_id("37i9dQZF1DZ06evO45P0Eo").unwrap(), None, None) .await .unwrap(); } @@ -180,6 +179,9 @@ async fn test_existing_playlist() { #[maybe_async] #[maybe_async_test] async fn test_fake_playlist() { - let playlist = creds_client().await.playlist("fake_id", None, None).await; + let playlist = creds_client() + .await + .playlist(Id::from_id("fakeid").unwrap(), None, None) + .await; assert!(!playlist.is_ok()); } diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 6806ddfd..28b68ba5 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -17,12 +17,13 @@ mod common; use common::maybe_async_test; -use rspotify::model::offset::for_position; -use rspotify::model::{Country, RepeatState, SearchType, TimeRange}; +use rspotify::model::offset::Offset; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder, TokenBuilder}; use rspotify::{ client::{Spotify, SpotifyBuilder}, - model::Market, + model::{ + Country, Id, Market, RepeatState, SearchType, ShowId, TimeRange, TrackId, TrackPositions, + }, }; use chrono::prelude::*; @@ -178,8 +179,8 @@ async fn test_current_user_saved_albums_add() { let mut album_ids = vec![]; let album_id1 = "6akEvsycLGftJxYudPjmqK"; let album_id2 = "628oezqK2qfmCjC6eXNors"; - album_ids.push(album_id2); - album_ids.push(album_id1); + album_ids.push(Id::from_id(album_id2).unwrap()); + album_ids.push(Id::from_id(album_id1).unwrap()); oauth_client() .await .current_user_saved_albums_add(album_ids) @@ -194,8 +195,8 @@ async fn test_current_user_saved_albums_delete() { let mut album_ids = vec![]; let album_id1 = "6akEvsycLGftJxYudPjmqK"; let album_id2 = "628oezqK2qfmCjC6eXNors"; - album_ids.push(album_id2); - album_ids.push(album_id1); + album_ids.push(Id::from_id(album_id2).unwrap()); + album_ids.push(Id::from_id(album_id1).unwrap()); oauth_client() .await .current_user_saved_albums_delete(album_ids) @@ -221,8 +222,8 @@ async fn test_current_user_saved_tracks_add() { let mut tracks_ids = vec![]; let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; let track_id2 = "spotify:track:1301WleyT98MSxVHPZCA6M"; - tracks_ids.push(track_id2); - tracks_ids.push(track_id1); + tracks_ids.push(Id::from_uri(track_id2).unwrap()); + tracks_ids.push(Id::from_uri(track_id1).unwrap()); oauth_client() .await .current_user_saved_tracks_add(tracks_ids) @@ -237,8 +238,8 @@ async fn test_current_user_saved_tracks_contains() { let mut tracks_ids = vec![]; let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; let track_id2 = "spotify:track:1301WleyT98MSxVHPZCA6M"; - tracks_ids.push(track_id2); - tracks_ids.push(track_id1); + tracks_ids.push(Id::from_uri(track_id2).unwrap()); + tracks_ids.push(Id::from_uri(track_id1).unwrap()); oauth_client() .await .current_user_saved_tracks_contains(tracks_ids) @@ -253,8 +254,8 @@ async fn test_current_user_saved_tracks_delete() { let mut tracks_ids = vec![]; let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; let track_id2 = "spotify:track:1301WleyT98MSxVHPZCA6M"; - tracks_ids.push(track_id2); - tracks_ids.push(track_id1); + tracks_ids.push(Id::from_uri(track_id2).unwrap()); + tracks_ids.push(Id::from_uri(track_id1).unwrap()); oauth_client() .await .current_user_saved_tracks_delete(tracks_ids) @@ -384,8 +385,8 @@ async fn test_previous_playback() { #[ignore] async fn test_recommendations() { let mut payload = Map::new(); - let seed_artists = vec!["4NHQUGzhtTLFvgF5SZesLK".to_owned()]; - let seed_tracks = vec!["0c6xIDDpzE81m2q797ordA".to_owned()]; + let seed_artists = vec![Id::from_id("4NHQUGzhtTLFvgF5SZesLK").unwrap()]; + let seed_tracks = vec![Id::from_id("0c6xIDDpzE81m2q797ordA").unwrap()]; payload.insert("min_energy".to_owned(), 0.4.into()); payload.insert("min_popularity".to_owned(), 50.into()); oauth_client() @@ -501,10 +502,10 @@ async fn test_shuffle() { #[ignore] async fn test_start_playback() { let device_id = String::from("74ASZWbe4lXaubB36ztrGX"); - let uris = vec!["spotify:track:4iV5W9uYEdYUVa79Axb7Rh".to_owned()]; + let uris = vec![TrackId::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap()]; oauth_client() .await - .start_playback(Some(device_id), None, Some(uris), for_position(0), None) + .start_uris_playback(&uris, Some(device_id), Some(Offset::for_position(0)), None) .await .unwrap(); } @@ -528,8 +529,8 @@ async fn test_user_follow_artist() { let mut artists = vec![]; let artist_id1 = "74ASZWbe4lXaubB36ztrGX"; let artist_id2 = "08td7MxkoHQkXnWAYD8d6Q"; - artists.push(artist_id2); - artists.push(artist_id1); + artists.push(Id::from_id(artist_id2).unwrap()); + artists.push(Id::from_id(artist_id1).unwrap()); oauth_client() .await .user_follow_artists(artists) @@ -544,8 +545,8 @@ async fn test_user_unfollow_artist() { let mut artists = vec![]; let artist_id1 = "74ASZWbe4lXaubB36ztrGX"; let artist_id2 = "08td7MxkoHQkXnWAYD8d6Q"; - artists.push(artist_id2); - artists.push(artist_id1); + artists.push(Id::from_id(artist_id2).unwrap()); + artists.push(Id::from_id(artist_id1).unwrap()); oauth_client() .await .user_unfollow_artists(artists) @@ -558,7 +559,7 @@ async fn test_user_unfollow_artist() { #[ignore] async fn test_user_follow_users() { let mut users = vec![]; - let user_id1 = "exampleuser01"; + let user_id1 = Id::from_id("exampleuser01").unwrap(); users.push(user_id1); oauth_client().await.user_follow_users(users).await.unwrap(); } @@ -568,7 +569,7 @@ async fn test_user_follow_users() { #[ignore] async fn test_user_unfollow_users() { let mut users = vec![]; - let user_id1 = "exampleuser01"; + let user_id1 = Id::from_id("exampleuser01").unwrap(); users.push(user_id1); oauth_client() .await @@ -581,11 +582,11 @@ async fn test_user_unfollow_users() { #[maybe_async_test] #[ignore] async fn test_playlist_add_tracks() { - let playlist_id = "5jAOgWXCBKuinsGiZxjDQ5"; + let playlist_id = Id::from_id("5jAOgWXCBKuinsGiZxjDQ5").unwrap(); let mut tracks_ids = vec![]; - let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; + let track_id1 = Id::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(); tracks_ids.push(track_id1); - let track_id2 = "spotify:track:1301WleyT98MSxVHPZCA6M"; + let track_id2 = Id::from_uri("spotify:track:1301WleyT98MSxVHPZCA6M").unwrap(); tracks_ids.push(track_id2); oauth_client() .await @@ -611,11 +612,11 @@ async fn test_playlist_change_detail() { #[maybe_async_test] #[ignore] async fn test_playlist_check_follow() { - let playlist_id = "2v3iNvBX8Ay1Gt2uXtUKUT"; - let mut user_ids: Vec = vec![]; - let user_id1 = String::from("possan"); + let playlist_id = Id::from_id("2v3iNvBX8Ay1Gt2uXtUKUT").unwrap(); + let mut user_ids: Vec<_> = vec![]; + let user_id1 = Id::from_id("possan").unwrap(); user_ids.push(user_id1); - let user_id2 = String::from("elogain"); + let user_id2 = Id::from_id("elogain").unwrap(); user_ids.push(user_id2); oauth_client() .await @@ -628,7 +629,7 @@ async fn test_playlist_check_follow() { #[maybe_async_test] #[ignore] async fn test_user_playlist_create() { - let user_id = "2257tjys2e2u2ygfke42niy2q"; + let user_id = Id::from_id("2257tjys2e2u2ygfke42niy2q").unwrap(); let playlist_name = "A New Playlist"; oauth_client() .await @@ -641,7 +642,7 @@ async fn test_user_playlist_create() { #[maybe_async_test] #[ignore] async fn test_playlist_follow_playlist() { - let playlist_id = "2v3iNvBX8Ay1Gt2uXtUKUT"; + let playlist_id = Id::from_id("2v3iNvBX8Ay1Gt2uXtUKUT").unwrap(); oauth_client() .await .playlist_follow(playlist_id, true) @@ -653,7 +654,7 @@ async fn test_playlist_follow_playlist() { #[maybe_async_test] #[ignore] async fn test_playlist_recorder_tracks() { - let playlist_id = "5jAOgWXCBKuinsGiZxjDQ5"; + let playlist_id = Id::from_id("5jAOgWXCBKuinsGiZxjDQ5").unwrap(); let range_start = 0; let insert_before = 1; let range_length = 1; @@ -668,10 +669,10 @@ async fn test_playlist_recorder_tracks() { #[maybe_async_test] #[ignore] async fn test_playlist_remove_all_occurrences_of_tracks() { - let playlist_id = "5jAOgWXCBKuinsGiZxjDQ5"; + let playlist_id = Id::from_id("5jAOgWXCBKuinsGiZxjDQ5").unwrap(); let mut tracks_ids = vec![]; - let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; - let track_id2 = "spotify:track:1301WleyT98MSxVHPZCA6M"; + let track_id1 = Id::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(); + let track_id2 = Id::from_uri("spotify:track:1301WleyT98MSxVHPZCA6M").unwrap(); tracks_ids.push(track_id2); tracks_ids.push(track_id1); oauth_client() @@ -685,30 +686,20 @@ async fn test_playlist_remove_all_occurrences_of_tracks() { #[maybe_async_test] #[ignore] async fn test_playlist_remove_specific_occurrences_of_tracks() { - let playlist_id = String::from("5jAOgWXCBKuinsGiZxjDQ5"); - let mut tracks = vec![]; - let mut map1 = Map::new(); - let mut position1 = vec![]; - position1.push(0); - position1.push(3); - map1.insert( - "uri".to_string(), - "spotify:track:4iV5W9uYEdYUVa79Axb7Rh".into(), - ); - map1.insert("position".to_string(), position1.into()); - tracks.push(map1); - let mut map2 = Map::new(); - let mut position2 = vec![]; - position2.push(7); - map2.insert( - "uri".to_string(), - "spotify:track:1301WleyT98MSxVHPZCA6M".into(), - ); - map2.insert("position".to_string(), position2.into()); - tracks.push(map2); + let playlist_id = Id::from_id("5jAOgWXCBKuinsGiZxjDQ5").unwrap(); + let tracks = vec![ + TrackPositions::new( + Id::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(), + vec![0, 3], + ), + TrackPositions::new( + Id::from_uri("spotify:track:1301WleyT98MSxVHPZCA6M").unwrap(), + vec![7], + ), + ]; oauth_client() .await - .playlist_remove_specific_occurrences_of_tracks(&playlist_id, tracks, None) + .playlist_remove_specific_occurrences_of_tracks(playlist_id, tracks, None) .await .unwrap(); } @@ -717,10 +708,10 @@ async fn test_playlist_remove_specific_occurrences_of_tracks() { #[maybe_async_test] #[ignore] async fn test_playlist_replace_tracks() { - let playlist_id = "5jAOgWXCBKuinsGiZxjDQ5"; + let playlist_id = Id::from_id("5jAOgWXCBKuinsGiZxjDQ5").unwrap(); let mut tracks_ids = vec![]; - let track_id1 = "spotify:track:4iV5W9uYEdYUVa79Axb7Rh"; - let track_id2 = "spotify:track:1301WleyT98MSxVHPZCA6M"; + let track_id1 = Id::from_uri("spotify:track:4iV5W9uYEdYUVa79Axb7Rh").unwrap(); + let track_id2 = Id::from_uri("spotify:track:1301WleyT98MSxVHPZCA6M").unwrap(); tracks_ids.push(track_id2); tracks_ids.push(track_id1); oauth_client() @@ -734,11 +725,11 @@ async fn test_playlist_replace_tracks() { #[maybe_async_test] #[ignore] async fn test_user_playlist() { - let user_id = "spotify"; - let mut playlist_id = String::from("59ZbFPES4DQwEjBpWHzrtC"); + let user_id = Id::from_id("spotify").unwrap(); + let playlist_id = Id::from_id("59ZbFPES4DQwEjBpWHzrtC").unwrap(); oauth_client() .await - .user_playlist(user_id, Some(&mut playlist_id), None) + .user_playlist(user_id, Some(playlist_id), None) .await .unwrap(); } @@ -747,7 +738,7 @@ async fn test_user_playlist() { #[maybe_async_test] #[ignore] async fn test_user_playlists() { - let user_id = "2257tjys2e2u2ygfke42niy2q"; + let user_id = Id::from_id("2257tjys2e2u2ygfke42niy2q").unwrap(); oauth_client() .await .user_playlists(user_id, Some(10), None) @@ -759,10 +750,10 @@ async fn test_user_playlists() { #[maybe_async_test] #[ignore] async fn test_playlist_tracks() { - let playlist_id = String::from("spotify:playlist:59ZbFPES4DQwEjBpWHzrtC"); + let playlist_id = Id::from_uri("spotify:playlist:59ZbFPES4DQwEjBpWHzrtC").unwrap(); oauth_client() .await - .playlist_tracks(&playlist_id, None, Some(2), None, None) + .playlist_tracks(playlist_id, None, Some(2), None, None) .await .unwrap(); } @@ -790,7 +781,7 @@ async fn test_volume() { #[maybe_async_test] #[ignore] async fn test_add_queue() { - let birdy_uri = String::from("spotify:track:6rqhFgbbKwnb9MLmUQDhG6"); + let birdy_uri = TrackId::from_uri("spotify:track:6rqhFgbbKwnb9MLmUQDhG6").unwrap(); oauth_client() .await .add_item_to_queue(birdy_uri, None) @@ -805,7 +796,10 @@ async fn test_get_several_shows() { oauth_client() .await .get_several_shows( - vec!["5CfCWKI5pZ28U0uOzXkDHe", "5as3aKmN2k11yfDDDSrvaZ"], + vec![ + ShowId::from_id("5CfCWKI5pZ28U0uOzXkDHe").unwrap(), + ShowId::from_id("5as3aKmN2k11yfDDDSrvaZ").unwrap(), + ], None, ) .await