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}