use crate::*;
use serde::Deserialize;
use std::fs;
use std::path::Path;
#[derive(Deserialize)]
struct SnapshotId {
snapshot_id: String,
}
pub async fn add_to_playlist(
token: &AccessToken,
id: &str,
tracks: &[&str],
position: Option<usize>,
) -> Result<String, EndpointError<Error>> {
Ok(request!(
token,
POST "/v1/playlists/{}/tracks",
path_params = [id],
header_params = {"Content-Type": "application/json"},
body = serde_json::json!({
"uris": tracks.iter().map(|track| format!("spotify:track:{}", track)).collect::<Vec<_>>(),
"position": position,
}).to_string(),
ret = SnapshotId
).snapshot_id)
}
pub async fn change_playlist(
token: &AccessToken,
id: &str,
name: Option<&str>,
public: Option<bool>,
collaborative: Option<bool>,
description: Option<&str>,
) -> Result<(), EndpointError<Error>> {
request!(
token,
PUT "/v1/playlists/{}",
path_params = [id],
header_params = {"Content-Type": "application/json"},
body = serde_json::json!({
"name": name,
"public": public,
"collaborative": collaborative,
"description": description,
}).to_string()
);
Ok(())
}
pub async fn create_playlist(
token: &AccessToken,
name: &str,
public: bool,
collaborative: bool,
description: &str,
) -> Result<Playlist, EndpointError<Error>> {
Ok(request!(
token,
POST "/v1/me/playlists",
header_params = {"Content-Type": "application/json"},
body = serde_json::json!({
"name": name,
"public": public,
"collaborative": collaborative,
"description": description,
}).to_string(),
ret = Playlist
))
}
pub async fn current_users_playlists(
token: &AccessToken,
limit: usize,
offset: usize,
) -> Result<Page<PlaylistSimplified>, EndpointError<Error>> {
Ok(request!(
token,
GET "/v1/me/playlists",
query_params = {"limit": limit.to_string(), "offset": offset.to_string()},
ret = Page<PlaylistSimplified>
))
}
pub async fn get_users_playlists(
token: &AccessToken,
id: &str,
limit: usize,
offset: usize,
) -> Result<Page<PlaylistSimplified>, EndpointError<Error>> {
Ok(request!(
token,
GET "/v1/users/{}/playlists",
path_params = [id],
query_params = {"limit": limit.to_string(), "offset": offset.to_string()},
ret = Page<PlaylistSimplified>
))
}
pub async fn get_playlist(
token: &AccessToken,
id: &str,
market: Option<Market>,
) -> Result<Playlist, EndpointError<Error>> {
Ok(request!(
token,
GET "/v1/playlists/{}",
path_params = [id],
optional_query_params = {"market": market.map(|m| m.as_str())},
ret = Playlist
))
}
pub async fn get_playlists_images(
token: &AccessToken,
id: &str,
) -> Result<Vec<Image>, EndpointError<Error>> {
Ok(request!(
token,
GET "/v1/playlists/{}/images",
path_params = [id],
ret = Vec<Image>
))
}
pub async fn get_playlists_tracks(
token: &AccessToken,
id: &str,
limit: usize,
offset: usize,
market: Option<Market>,
) -> Result<Page<PlaylistTrack>, EndpointError<Error>> {
Ok(request!(
token,
GET "/v1/playlists/{}/tracks",
path_params = [id],
query_params = {"limit": limit.to_string(), "offset": offset.to_string()},
optional_query_params = {"market": market.map(|m| m.as_str())},
ret = Page<PlaylistTrack>
))
}
pub async fn remove_from_playlist(
token: &AccessToken,
id: &str,
tracks: &[(&str, Option<&[usize]>)],
snapshot_id: &str,
) -> Result<String, EndpointError<Error>> {
if tracks.is_empty() {
return Ok(String::from(snapshot_id));
}
Ok(request!(
token,
DELETE "/v1/playlists/{}/tracks",
path_params = [id],
header_params = {"Content-Type": "application/json"},
body = serde_json::json!({
"tracks": tracks.iter().map(|(id, positions)| if let Some(position) = positions {
serde_json::json!({
"uri": format!("spotify:track:{}", id),
"positions": position,
})
} else {
serde_json::json!({
"uri": format!("spotify:track:{}", id),
})
}).collect::<Vec<_>>(),
"snapshot_id": snapshot_id,
}).to_string(),
ret = SnapshotId
)
.snapshot_id)
}
pub async fn reorder_playlist(
token: &AccessToken,
id: &str,
range_start: usize,
range_length: usize,
insert_before: usize,
snapshot_id: &str,
) -> Result<String, EndpointError<Error>> {
if range_length == 0 || range_start + range_length == insert_before {
return Ok(String::from(snapshot_id));
}
Ok(request!(
token,
PUT "/v1/playlists/{}/tracks",
path_params = [id],
header_params = {"Content-Type": "application/json"},
body = format!(
r#"{{"range_start":{},"range_length":{},"insert_before":{},"snapshot_id":"{}"}}"#,
range_start, range_length, insert_before, snapshot_id
),
ret = SnapshotId
)
.snapshot_id)
}
pub async fn replace_playlists_tracks(
token: &AccessToken,
id: &str,
tracks: &[&str],
) -> Result<String, EndpointError<Error>> {
Ok(request!(
token,
PUT "/v1/playlists/{}/tracks",
path_params = [id],
header_params = {"Content-Type": "application/json"},
body = serde_json::json!({
"uris": tracks.iter().map(|id| format!("spotify:track:{}", id)).collect::<Vec<_>>(),
}).to_string(),
ret = SnapshotId
)
.snapshot_id)
}
pub async fn upload_playlist_cover(
token: &AccessToken,
id: &str,
image: &str,
) -> Result<(), EndpointError<Error>> {
request!(
token,
PUT "/v1/playlists/{}/images",
path_params = [id],
header_params = {"Content-Type": "image/jpeg"},
body = String::from(image)
);
Ok(())
}
pub async fn upload_playlist_cover_jpeg<T: ?Sized + AsRef<[u8]>>(
token: &AccessToken,
id: &str,
image: &T,
) -> Result<(), EndpointError<Error>> {
upload_playlist_cover(token, id, &base64::encode(image)).await
}
pub async fn upload_playlist_cover_file<P: AsRef<Path>>(
token: &AccessToken,
id: &str,
image: P,
) -> Result<(), EndpointError<Error>> {
upload_playlist_cover_jpeg(token, id, &fs::read(image)?).await
}
#[cfg(test)]
mod tests {
use crate::endpoints::token;
use crate::*;
use std::time::Duration;
use tokio::time;
#[tokio::test]
async fn test() {
let token = token().await;
let mut playlist =
create_playlist(&token, "Testing Playlist", true, false, "Test Description")
.await
.unwrap();
assert_eq!(playlist.name, "Testing Playlist");
assert_eq!(playlist.public, Some(true));
assert_eq!(playlist.collaborative, false);
assert_eq!(playlist.description.as_ref().unwrap(), "Test Description");
assert_eq!(playlist.followers, Followers { total: 0 });
assert!(playlist.images.is_empty());
assert_eq!(playlist.tracks.total, 0);
let got_playlist = get_playlist(&token, &playlist.id, None).await.unwrap();
playlist.snapshot_id = got_playlist.snapshot_id.clone();
assert_eq!(playlist, got_playlist);
let playlists = current_users_playlists(&token, 50, 0).await.unwrap();
if playlists.total <= 50 {
assert!(playlists.items.iter().any(|p| p.id == playlist.id));
}
change_playlist(
&token,
&playlist.id,
Some("New Name"),
Some(false),
Some(true),
Some("New Description"),
)
.await
.unwrap();
let playlist = get_playlist(&token, &playlist.id, None).await.unwrap();
assert_eq!(playlist.name, "New Name");
assert_eq!(playlist.public, Some(false));
assert_eq!(playlist.collaborative, true);
assert_eq!(playlist.description.unwrap(), "New Description");
assert_eq!(playlist.followers, Followers { total: 0 });
assert!(playlist.images.is_empty());
assert_eq!(playlist.tracks.total, 0);
let snapshot = add_to_playlist(
&token,
&playlist.id,
&["0vjYxBDAcflD0358arIVZG", "6GG73Jik4jUlQCkKg9JuGO"],
None,
)
.await
.unwrap();
assert_ne!(playlist.snapshot_id, snapshot);
let playlist = get_playlist(&token, &playlist.id, None).await.unwrap();
assert_eq!(playlist.snapshot_id, snapshot);
assert_eq!(playlist.tracks.total, 2);
let tracks = get_playlists_tracks(&token, &playlist.id, 1, 1, None)
.await
.unwrap();
assert_eq!(tracks.items.len(), 1);
assert_eq!(tracks.items[0].is_local, false);
assert_eq!(tracks.items[0].track.id, "6GG73Jik4jUlQCkKg9JuGO");
assert_eq!(tracks.limit, 1);
assert_eq!(tracks.offset, 1);
assert_eq!(tracks.total, 2);
let tracks = &[
"22wRQVOHzHAppfKsDs38nj",
"4QlzkaRHtU8gAdwqjWmO8n",
"7d8GetOsjbxYnlo6Y9e5Kw",
];
async fn assert_playlist_order(token: &AccessToken, id: &str, order: &[&str]) {
let tracks = get_playlists_tracks(token, id, order.len(), 0, None)
.await
.unwrap();
assert_eq!(tracks.total, order.len());
assert_eq!(
tracks
.items
.iter()
.map(|track| &track.track.id)
.collect::<Vec<_>>(),
order
);
}
let mut snapshot = replace_playlists_tracks(&token, &playlist.id, tracks)
.await
.unwrap();
assert_playlist_order(&token, &playlist.id, &[tracks[0], tracks[1], tracks[2]]).await;
snapshot = reorder_playlist(&token, &playlist.id, 1, 1, 0, &snapshot)
.await
.unwrap();
assert_playlist_order(&token, &playlist.id, &[tracks[1], tracks[0], tracks[2]]).await;
reorder_playlist(&token, &playlist.id, 0, 2, 3, &snapshot)
.await
.unwrap();
assert_playlist_order(&token, &playlist.id, &[tracks[2], tracks[1], tracks[0]]).await;
snapshot = add_to_playlist(&token, &playlist.id, &[tracks[0], tracks[1]], Some(1))
.await
.unwrap();
assert_playlist_order(
&token,
&playlist.id,
&[tracks[2], tracks[0], tracks[1], tracks[1], tracks[0]],
)
.await;
remove_from_playlist(
&token,
&playlist.id,
&[
(tracks[0], None),
(tracks[2], Some(&[0])),
(tracks[1], Some(&[2, 3])),
],
&snapshot,
)
.await
.unwrap();
let playlist = get_playlist(&token, &playlist.id, None).await.unwrap();
assert_eq!(playlist.tracks.items, &[]);
upload_playlist_cover_file(&token, &playlist.id, "example_image.jpeg")
.await
.unwrap();
time::delay_for(Duration::from_secs(5)).await;
let images = get_playlists_images(&token, &playlist.id).await.unwrap();
assert_eq!(images.len(), 1);
if let Some(height) = images[0].height {
assert_eq!(height, 512);
}
if let Some(width) = images[0].width {
assert_eq!(width, 512);
}
unfollow_playlist(&token, &playlist.id).await.unwrap();
}
#[tokio::test]
async fn test_get_users_playlists() {
get_users_playlists(&token().await, "wizzler", 2, 1)
.await
.unwrap();
}
}