use std::time::Duration;
use crate::{
auth::AuthFlow,
client::{self, Client},
endpoint::Endpoint,
error::Result,
Error, Token,
};
use serde::{de::DeserializeOwned, Deserialize, Deserializer};
pub mod album;
pub mod artist;
pub mod audio;
pub mod audiobook;
pub mod category;
pub mod market;
pub mod player;
pub mod playlist;
pub mod recommendation;
pub mod search;
pub mod show;
pub mod track;
pub mod user;
const PAGE_MAX_LIMIT: u32 = 50;
const PAGINATION_INTERVAL: Duration = Duration::from_millis(100);
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct Page<T: Clone> {
pub href: String,
pub limit: u32,
pub next: Option<String>,
pub offset: u32,
pub previous: Option<String>,
pub total: u32,
pub items: Vec<Option<T>>,
}
impl<T: Clone + DeserializeOwned> Page<T> {
pub fn filtered_items(&self) -> Vec<T> {
self.items.clone().into_iter().flatten().collect()
}
pub async fn get_next(&self, spotify: &Client<Token, impl AuthFlow>) -> Result<Self> {
let Some(next) = self.next.as_ref() else {
return Err(Error::NoRemainingPages);
};
let next = next.replace(client::API_URL, "");
spotify.get(next, [("limit", self.limit)]).await
}
pub async fn get_previous(&self, spotify: &Client<Token, impl AuthFlow>) -> Result<Self> {
let Some(previous) = self.previous.as_ref() else {
return Err(Error::NoRemainingPages);
};
let previous = previous.replace(client::API_URL, "");
spotify.get(previous, [("limit", self.limit)]).await
}
pub async fn get_remaining(
mut self,
spotify: &Client<Token, impl AuthFlow>,
) -> Result<Vec<Option<T>>> {
let mut items = std::mem::take(&mut self.items);
self.limit = PAGE_MAX_LIMIT;
let mut page = self;
if page.next.is_some() {
loop {
let next_page = page.get_next(spotify).await;
match next_page {
Ok(mut p) => {
items.append(&mut p.items);
page = p;
}
Err(err) => match err {
Error::NoRemainingPages => break,
_ => return Err(err),
},
};
tokio::time::sleep(PAGINATION_INTERVAL).await;
}
}
Ok(items)
}
pub async fn get_all(
mut self,
spotify: &Client<Token, impl AuthFlow>,
) -> Result<Vec<Option<T>>> {
let mut items = std::mem::take(&mut self.items);
self.limit = PAGE_MAX_LIMIT;
if self.previous.is_some() {
let mut page = self.clone();
loop {
let previous_page = page.get_previous(spotify).await;
match previous_page {
Ok(mut p) => {
items.append(&mut p.items);
page = p;
}
Err(err) => match err {
Error::NoRemainingPages => break,
_ => return Err(err),
},
};
tokio::time::sleep(PAGINATION_INTERVAL).await;
}
}
if self.next.is_some() {
let mut page = self;
loop {
let next_page = page.get_next(spotify).await;
match next_page {
Ok(mut p) => {
items.append(&mut p.items);
page = p;
}
Err(err) => match err {
Error::NoRemainingPages => break,
_ => return Err(err),
},
};
tokio::time::sleep(PAGINATION_INTERVAL).await;
}
}
Ok(items)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct CursorPage<T: Clone, E: Endpoint + Default> {
pub href: String,
pub limit: u32,
pub next: Option<String>,
pub cursors: Option<Cursor>,
pub total: Option<u32>,
pub items: Vec<Option<T>>,
#[serde(skip)]
endpoint: E,
}
impl<T: Clone + DeserializeOwned, E: Endpoint + Default + Clone> CursorPage<T, E> {
pub fn filtered_items(&self) -> Vec<T> {
self.items.clone().into_iter().flatten().collect()
}
pub async fn get_before(&self, spotify: &Client<Token, impl AuthFlow>) -> Result<Self> {
let Some(ref cursors) = self.cursors else {
return Err(Error::NoRemainingPages);
};
let Some(before) = cursors.before.as_ref() else {
return Err(Error::NoRemainingPages);
};
spotify
.get(
self.endpoint.endpoint_url().to_owned(),
[("before", before), ("limit", &(self.limit.to_string()))],
)
.await
}
pub async fn get_after(&self, spotify: &Client<Token, impl AuthFlow>) -> Result<Self> {
let Some(ref cursors) = self.cursors else {
return Err(Error::NoRemainingPages);
};
let Some(after) = cursors.after.as_ref() else {
return Err(Error::NoRemainingPages);
};
spotify
.get(
self.endpoint.endpoint_url().to_owned(),
[("after", after), ("limit", &(self.limit.to_string()))],
)
.await
}
pub async fn get_remaining(
mut self,
spotify: &Client<Token, impl AuthFlow>,
) -> Result<Vec<Option<T>>> {
let mut items = std::mem::take(&mut self.items);
self.limit = PAGE_MAX_LIMIT;
let mut page = self;
if let Some(ref cursors) = page.cursors {
if cursors.after.is_some() {
loop {
let next_page = page.get_after(spotify).await;
match next_page {
Ok(mut p) => {
items.append(&mut p.items);
page = p;
}
Err(err) => match err {
Error::NoRemainingPages => break,
_ => return Err(err),
},
}
tokio::time::sleep(PAGINATION_INTERVAL).await;
}
}
}
Ok(items)
}
pub async fn get_all(
mut self,
spotify: &Client<Token, impl AuthFlow>,
) -> Result<Vec<Option<T>>> {
let mut items = std::mem::take(&mut self.items);
self.limit = PAGE_MAX_LIMIT;
if let Some(ref cursors) = self.cursors {
if cursors.before.is_some() {
let mut page = self.clone();
loop {
let previous_page = page.get_before(spotify).await;
match previous_page {
Ok(mut p) => {
items.append(&mut p.items);
page = p;
}
Err(err) => match err {
Error::NoRemainingPages => break,
_ => return Err(err),
},
}
tokio::time::sleep(PAGINATION_INTERVAL).await;
}
}
}
if let Some(ref cursors) = self.cursors {
if cursors.after.is_some() {
let mut page = self;
loop {
let next_page = page.get_after(spotify).await;
match next_page {
Ok(mut p) => {
items.append(&mut p.items);
page = p;
}
Err(err) => match err {
Error::NoRemainingPages => break,
_ => return Err(err),
},
}
tokio::time::sleep(PAGINATION_INTERVAL).await;
}
}
}
Ok(items)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct Cursor {
pub after: Option<String>,
pub before: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct Image {
pub url: String,
pub height: Option<u32>,
pub width: Option<u32>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct Copyright {
pub text: String,
pub r#type: CopyrightType,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct Restriction {
pub reason: RestrictionReason,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct ExternalIds {
pub isrc: Option<String>,
pub ean: Option<String>,
pub upc: Option<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct ExternalUrls {
pub spotify: String,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct Followers {
pub href: Option<String>,
pub total: u32,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct ResumePoint {
pub fully_played: bool,
pub resume_position_ms: u32,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum RestrictionReason {
Market,
Product,
Explicit,
#[serde(other)]
Unknown,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
pub enum CopyrightType {
#[serde(rename = "C")]
Copyright,
#[serde(rename = "P")]
Performance,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum DatePrecision {
Year,
Month,
Day,
}
#[derive(Clone, Debug, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum PlayableItem {
Track(track::Track),
Episode(show::Episode),
}
fn null_to_default<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: Default + Deserialize<'de>,
D: Deserializer<'de>,
{
Ok(Option::deserialize(deserializer)?.unwrap_or_default())
}