spotify_rs/
model.rs

1use std::time::Duration;
2
3use crate::{
4    auth::AuthFlow,
5    client::{self, Client},
6    endpoint::Endpoint,
7    error::Result,
8    Error, Token,
9};
10use serde::{de::DeserializeOwned, Deserialize, Deserializer};
11
12pub mod album;
13pub mod artist;
14pub mod audio;
15pub mod audiobook;
16pub mod category;
17pub mod market;
18pub mod player;
19pub mod playlist;
20pub mod recommendation;
21pub mod search;
22pub mod show;
23pub mod track;
24pub mod user;
25
26const PAGE_MAX_LIMIT: u32 = 50;
27const PAGINATION_INTERVAL: Duration = Duration::from_millis(100);
28
29/// This represents a page of items, which is a segment of data returned by the
30/// Spotify API.
31///
32/// To get the rest of the data, the fields of this struct, or, preferably,
33/// some methods can be used to get the
34/// [next](Self::get_next) or [previous](Self::get_previous) page, or
35/// the [remaining](Self::get_remaining) or [all](Self::get_all) items.
36#[derive(Clone, Debug, Deserialize, PartialEq)]
37pub struct Page<T: Clone> {
38    /// The URL to the API endpoint returning this page.
39    pub href: String,
40    /// The maximum amount of items in the response.
41    pub limit: u32,
42    /// The URL to the next page.
43    /// For pagination, see [`get_next`](Self::get_next).
44    pub next: Option<String>,
45    /// The offset of the returned items.
46    pub offset: u32,
47    /// The URL to the previous page.
48    /// For pagination, see [`get_previous`](Self::get_previous).
49    pub previous: Option<String>,
50    /// The amount of returned items.
51    pub total: u32,
52    /// A list of the items, which includes `null` values.
53    /// To get only the `Some` values, use [`filtered_items`](Self::filtered_items).
54    pub items: Vec<Option<T>>,
55}
56
57impl<T: Clone + DeserializeOwned> Page<T> {
58    /// Get a list of only the `Some` values from a Page's items.
59    pub fn filtered_items(&self) -> Vec<T> {
60        self.items.clone().into_iter().flatten().collect()
61    }
62
63    /// Get the next page.
64    ///
65    /// If there is no next page, this will return an
66    /// [`Error::NoRemainingPages`](crate::error::Error::NoRemainingPages).
67    pub async fn get_next(&self, spotify: &Client<Token, impl AuthFlow>) -> Result<Self> {
68        let Some(next) = self.next.as_ref() else {
69            return Err(Error::NoRemainingPages);
70        };
71
72        // Remove `API_URL`from the string, as spotify.get()
73        // (or rather spotify.request) appends it already.
74        let next = next.replace(client::API_URL, "");
75
76        spotify.get(next, [("limit", self.limit)]).await
77    }
78
79    /// Get the previous page.
80    ///
81    /// If there is no previous page, this will return an
82    /// [`Error::NoRemainingPages`](crate::error::Error::NoRemainingPages).
83    pub async fn get_previous(&self, spotify: &Client<Token, impl AuthFlow>) -> Result<Self> {
84        let Some(previous) = self.previous.as_ref() else {
85            return Err(Error::NoRemainingPages);
86        };
87
88        // Remove `API_URL`from the string, as spotify.get()
89        // (or rather spotify.request) appends it already.
90        let previous = previous.replace(client::API_URL, "");
91
92        spotify.get(previous, [("limit", self.limit)]).await
93    }
94
95    /// Get the items of all the remaining pages - that is, all the pages found
96    /// after the current one.
97    pub async fn get_remaining(
98        mut self,
99        spotify: &Client<Token, impl AuthFlow>,
100    ) -> Result<Vec<Option<T>>> {
101        let mut items = std::mem::take(&mut self.items);
102        self.limit = PAGE_MAX_LIMIT;
103        let mut page = self;
104
105        // Get all the next pages (if any)
106        if page.next.is_some() {
107            loop {
108                let next_page = page.get_next(spotify).await;
109
110                match next_page {
111                    Ok(mut p) => {
112                        items.append(&mut p.items);
113                        page = p;
114                    }
115
116                    Err(err) => match err {
117                        Error::NoRemainingPages => break,
118                        _ => return Err(err),
119                    },
120                };
121
122                tokio::time::sleep(PAGINATION_INTERVAL).await;
123            }
124        }
125
126        Ok(items)
127    }
128
129    /// Get the items of all of the pages - that is, all the pages found both before and
130    /// after the current one.
131    pub async fn get_all(
132        mut self,
133        spotify: &Client<Token, impl AuthFlow>,
134    ) -> Result<Vec<Option<T>>> {
135        let mut items = std::mem::take(&mut self.items);
136        self.limit = PAGE_MAX_LIMIT;
137
138        // Get all the previous pages (if any)
139        if self.previous.is_some() {
140            let mut page = self.clone();
141
142            loop {
143                let previous_page = page.get_previous(spotify).await;
144
145                match previous_page {
146                    Ok(mut p) => {
147                        items.append(&mut p.items);
148                        page = p;
149                    }
150                    Err(err) => match err {
151                        Error::NoRemainingPages => break,
152                        _ => return Err(err),
153                    },
154                };
155
156                tokio::time::sleep(PAGINATION_INTERVAL).await;
157            }
158        }
159
160        // Get all the next pages (if any)
161        if self.next.is_some() {
162            let mut page = self;
163
164            loop {
165                let next_page = page.get_next(spotify).await;
166
167                match next_page {
168                    Ok(mut p) => {
169                        items.append(&mut p.items);
170                        page = p;
171                    }
172
173                    Err(err) => match err {
174                        Error::NoRemainingPages => break,
175                        _ => return Err(err),
176                    },
177                };
178
179                tokio::time::sleep(PAGINATION_INTERVAL).await;
180            }
181        }
182
183        Ok(items)
184    }
185}
186
187/// This represents a page of items, which is a segment of data returned by the
188/// Spotify API.
189///
190/// It's similar to [`Page`], except that it uses a different approach for
191/// pagination - instead of using a `next` and `previous` field to get another
192/// page, it uses a Unix timestamp (in miliseconds).
193///
194/// To get the rest of the data, the `cursors` field (and others), or, preferably,
195/// the [get_before](Self::get_before) and [get_after](Self::get_after) methods can be used.
196// (and possibly in other situations)
197// it happens because some fields are null when they shouldn't be
198#[derive(Clone, Debug, Deserialize, PartialEq)]
199pub struct CursorPage<T: Clone, E: Endpoint + Default> {
200    /// The URL to the API endpoint returning this page.
201    pub href: String,
202    /// The maximum amount of items in the response.
203    pub limit: u32,
204    /// The URL to the next page.
205    pub next: Option<String>,
206    /// The cursor object used to get the previous/next page.
207    pub cursors: Option<Cursor>,
208    /// The amount of returned items.
209    pub total: Option<u32>,
210    /// A list of the items, which includes `null` values.
211    pub items: Vec<Option<T>>,
212    // Used to keep track of which endpoint should be called to
213    // get subsequent pages.
214    #[serde(skip)]
215    endpoint: E,
216}
217
218impl<T: Clone + DeserializeOwned, E: Endpoint + Default + Clone> CursorPage<T, E> {
219    /// Get a list of only the `Some` values from a Cursor Page's items.
220    pub fn filtered_items(&self) -> Vec<T> {
221        self.items.clone().into_iter().flatten().collect()
222    }
223
224    /// Get the page chronologically before the current one.
225    ///
226    /// If there is no previous page, this will return an
227    /// [`Error::NoRemainingPages`](crate::error::Error::NoRemainingPages).
228    pub async fn get_before(&self, spotify: &Client<Token, impl AuthFlow>) -> Result<Self> {
229        let Some(ref cursors) = self.cursors else {
230            return Err(Error::NoRemainingPages);
231        };
232
233        let Some(before) = cursors.before.as_ref() else {
234            return Err(Error::NoRemainingPages);
235        };
236
237        spotify
238            .get(
239                self.endpoint.endpoint_url().to_owned(),
240                [("before", before), ("limit", &(self.limit.to_string()))],
241            )
242            .await
243    }
244
245    /// Get the page chronologically after the current one.
246    ///
247    /// If there is no next page, this will return an
248    /// [`Error::NoRemainingPages`](crate::error::Error::NoRemainingPages).
249    pub async fn get_after(&self, spotify: &Client<Token, impl AuthFlow>) -> Result<Self> {
250        let Some(ref cursors) = self.cursors else {
251            return Err(Error::NoRemainingPages);
252        };
253
254        let Some(after) = cursors.after.as_ref() else {
255            return Err(Error::NoRemainingPages);
256        };
257
258        spotify
259            .get(
260                self.endpoint.endpoint_url().to_owned(),
261                [("after", after), ("limit", &(self.limit.to_string()))],
262            )
263            .await
264    }
265
266    /// Get the items of all the remaining pages - that is, all the pages found
267    /// after the current one.
268    pub async fn get_remaining(
269        mut self,
270        spotify: &Client<Token, impl AuthFlow>,
271    ) -> Result<Vec<Option<T>>> {
272        let mut items = std::mem::take(&mut self.items);
273        self.limit = PAGE_MAX_LIMIT;
274        let mut page = self;
275
276        // Get all the next pages (if any)
277        if let Some(ref cursors) = page.cursors {
278            if cursors.after.is_some() {
279                loop {
280                    let next_page = page.get_after(spotify).await;
281
282                    match next_page {
283                        Ok(mut p) => {
284                            items.append(&mut p.items);
285                            page = p;
286                        }
287                        Err(err) => match err {
288                            Error::NoRemainingPages => break,
289                            _ => return Err(err),
290                        },
291                    }
292
293                    tokio::time::sleep(PAGINATION_INTERVAL).await;
294                }
295            }
296        }
297
298        Ok(items)
299    }
300
301    pub async fn get_all(
302        mut self,
303        spotify: &Client<Token, impl AuthFlow>,
304    ) -> Result<Vec<Option<T>>> {
305        let mut items = std::mem::take(&mut self.items);
306        self.limit = PAGE_MAX_LIMIT;
307
308        // Get all the previous pages (if any)
309        if let Some(ref cursors) = self.cursors {
310            if cursors.before.is_some() {
311                let mut page = self.clone();
312
313                loop {
314                    let previous_page = page.get_before(spotify).await;
315
316                    match previous_page {
317                        Ok(mut p) => {
318                            items.append(&mut p.items);
319                            page = p;
320                        }
321                        Err(err) => match err {
322                            Error::NoRemainingPages => break,
323                            _ => return Err(err),
324                        },
325                    }
326
327                    tokio::time::sleep(PAGINATION_INTERVAL).await;
328                }
329            }
330        }
331
332        // Get all the next pages (if any)
333        if let Some(ref cursors) = self.cursors {
334            if cursors.after.is_some() {
335                let mut page = self;
336
337                loop {
338                    let next_page = page.get_after(spotify).await;
339
340                    match next_page {
341                        Ok(mut p) => {
342                            items.append(&mut p.items);
343                            page = p;
344                        }
345                        Err(err) => match err {
346                            Error::NoRemainingPages => break,
347                            _ => return Err(err),
348                        },
349                    }
350
351                    tokio::time::sleep(PAGINATION_INTERVAL).await;
352                }
353            }
354        }
355
356        Ok(items)
357    }
358}
359
360/// A cursor used to paginate results returned as a [`CursorPage`].
361#[derive(Clone, Debug, Deserialize, PartialEq)]
362pub struct Cursor {
363    pub after: Option<String>,
364    pub before: Option<String>,
365}
366
367/// An image used in various contexts.
368#[derive(Clone, Debug, Deserialize, PartialEq)]
369pub struct Image {
370    /// The URL of the image.
371    pub url: String,
372    /// The height in pixels.
373    pub height: Option<u32>,
374    /// The width in pixels.
375    pub width: Option<u32>,
376}
377
378/// A copyright statement.
379#[derive(Clone, Debug, Deserialize, PartialEq)]
380pub struct Copyright {
381    /// The copyright text.
382    pub text: String,
383    /// The copyright type.
384    pub r#type: CopyrightType,
385}
386
387/// A content restriction.
388#[derive(Clone, Debug, Deserialize, PartialEq)]
389pub struct Restriction {
390    /// The reason for the restriction.
391    pub reason: RestrictionReason,
392}
393
394/// Contains known external IDs for content.
395#[derive(Clone, Debug, Deserialize, PartialEq)]
396pub struct ExternalIds {
397    /// The [International Standard Recording Code](https://en.wikipedia.org/wiki/International_Standard_Recording_Code)
398    /// for the content.
399    pub isrc: Option<String>,
400    /// The [International Article Number](https://en.wikipedia.org/wiki/International_Article_Number)
401    /// for the content.
402    pub ean: Option<String>,
403    /// The [Universal Product Code](https://en.wikipedia.org/wiki/Universal_Product_Code)
404    /// for the content.
405    pub upc: Option<String>,
406}
407
408/// Contains external URLs for content. Currently, it seems that only Spotify
409/// URLs are included here.
410#[derive(Clone, Debug, Deserialize, PartialEq)]
411pub struct ExternalUrls {
412    /// The [Spotify URL](https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids)
413    /// for the content.
414    pub spotify: String,
415}
416
417/// Information about the followers of an artist, playlist or user.
418#[derive(Clone, Debug, Deserialize, PartialEq)]
419pub struct Followers {
420    /// This will always be set to null, as the Web API does not support it at the moment.
421    pub href: Option<String>,
422    /// The total amount of followers.
423    pub total: u32,
424}
425
426/// The user's latest position in a chapter or episode.
427#[derive(Clone, Debug, Deserialize, PartialEq)]
428pub struct ResumePoint {
429    /// Whether or not the chapter or episode has fully been played by the user.
430    pub fully_played: bool,
431    /// The user's latest position in miliseconds.
432    pub resume_position_ms: u32,
433}
434
435/// The reason for restriction on content.
436#[derive(Clone, Debug, Deserialize, PartialEq)]
437#[serde(rename_all = "snake_case")]
438#[non_exhaustive]
439pub enum RestrictionReason {
440    /// A restriction set because of the market of a user.
441    Market,
442    /// A restriction set because of the user's subscription type.
443    Product,
444    /// A restriction set because the content is explicit, and the user settings
445    /// are set so that explicit conent can't be played.
446    Explicit,
447    #[serde(other)]
448    /// Any other type of restriction, as more may be added in the future.
449    Unknown,
450}
451
452/// The copyright type for a piece of content:
453#[derive(Clone, Debug, Deserialize, PartialEq)]
454pub enum CopyrightType {
455    #[serde(rename = "C")]
456    /// The copyright.
457    Copyright,
458    #[serde(rename = "P")]
459    /// The sound recording (performance) copyright.
460    Performance,
461}
462
463/// The precision with which a date is known.
464#[derive(Clone, Debug, Deserialize, PartialEq)]
465#[serde(rename_all = "snake_case")]
466pub enum DatePrecision {
467    /// The date is known at the year level.
468    Year,
469    /// The date is known at the month level.
470    Month,
471    /// The date is known at the day level.
472    Day,
473}
474
475/// An item that can be played.
476#[derive(Clone, Debug, Deserialize, PartialEq)]
477#[serde(untagged)]
478pub enum PlayableItem {
479    /// A Spotify track (song).
480    Track(track::Track),
481    /// An episode of a show.
482    Episode(show::Episode),
483}
484
485// A function to convert a "null" JSON value to the default of given type,
486// to make the API slightly nicer to use for people.
487fn null_to_default<'de, T, D>(deserializer: D) -> Result<T, D::Error>
488where
489    T: Default + Deserialize<'de>,
490    D: Deserializer<'de>,
491{
492    Ok(Option::deserialize(deserializer)?.unwrap_or_default())
493}