rspotify_s/clients/base.rs
1use crate::{
2 auth_urls,
3 clients::{
4 convert_result,
5 pagination::{paginate, paginate_with_ctx, Paginator},
6 },
7 http::{BaseHttpClient, Form, Headers, HttpClient, Query},
8 join_ids,
9 model::*,
10 sync::Mutex,
11 util::build_map,
12 ClientError, ClientResult, Config, Credentials, Token,
13};
14
15use std::{collections::HashMap, fmt, ops::Not, sync::Arc};
16
17use chrono::Utc;
18use maybe_async::maybe_async;
19use serde_json::Value;
20
21/// This trait implements the basic endpoints from the Spotify API that may be
22/// accessed without user authorization, including parts of the authentication
23/// flow that are shared, and the endpoints.
24#[cfg_attr(target_arch = "wasm32", maybe_async(?Send))]
25#[cfg_attr(not(target_arch = "wasm32"), maybe_async)]
26pub trait BaseClient
27where
28 Self: Send + Sync + Default + Clone + fmt::Debug,
29{
30 fn get_config(&self) -> &Config;
31 fn get_http(&self) -> &HttpClient;
32 fn get_creds(&self) -> &Credentials;
33
34 /// Note that the token is wrapped by a `Mutex` in order to allow interior
35 /// mutability. This is required so that the entire client doesn't have to
36 /// be mutable (the token is accessed to from every endpoint).
37 fn get_token(&self) -> Arc<Mutex<Option<Token>>>;
38
39 /// Returns the absolute URL for an endpoint in the API.
40 fn api_url(&self, url: &str) -> String {
41 let mut base = self.get_config().api_base_url.clone();
42 if !base.ends_with('/') {
43 base.push('/');
44 }
45 base + url
46 }
47
48 /// Returns the absolute URL for an authentication step in the API.
49 fn auth_url(&self, url: &str) -> String {
50 let mut base = self.get_config().auth_base_url.clone();
51 if !base.ends_with('/') {
52 base.push('/');
53 }
54 base + url
55 }
56
57 /// Refetch the current access token given a refresh token.
58 async fn refetch_token(&self) -> ClientResult<Option<Token>>;
59
60 /// Re-authenticate the client automatically if it's configured to do so,
61 /// which uses the refresh token to obtain a new access token.
62 async fn auto_reauth(&self) -> ClientResult<()> {
63 if !self.get_config().token_refreshing {
64 return Ok(());
65 }
66
67 // NOTE: It's important to not leave the token locked, or else a
68 // deadlock when calling `refresh_token` will occur.
69 let should_reauth = self
70 .get_token()
71 .lock()
72 .await
73 .unwrap()
74 .as_ref()
75 .map_or(false, Token::is_expired);
76
77 if should_reauth {
78 self.refresh_token().await
79 } else {
80 Ok(())
81 }
82 }
83
84 /// Refreshes the current access token given a refresh token. The obtained
85 /// token will be saved internally.
86 async fn refresh_token(&self) -> ClientResult<()> {
87 let token = self.refetch_token().await?;
88 *self.get_token().lock().await.unwrap() = token;
89 self.write_token_cache().await
90 }
91
92 /// The headers required for authenticated requests to the API.
93 ///
94 /// Since this is accessed by authenticated requests always, it's where the
95 /// automatic reauthentication takes place, if enabled.
96 #[doc(hidden)]
97 async fn auth_headers(&self) -> ClientResult<Headers> {
98 self.auto_reauth().await?;
99
100 Ok(self
101 .get_token()
102 .lock()
103 .await
104 .unwrap()
105 .as_ref()
106 .ok_or(ClientError::InvalidToken)?
107 .auth_headers())
108 }
109
110 // HTTP-related methods for the Spotify client. They wrap up the basic HTTP
111 // client with its specific usage for endpoints or authentication.
112
113 /// Convenience method to send GET requests related to an endpoint in the
114 /// API.
115 #[doc(hidden)]
116 #[inline]
117 async fn api_get(&self, url: &str, payload: &Query<'_>) -> ClientResult<String> {
118 let url = self.api_url(url);
119 let headers = self.auth_headers().await?;
120 Ok(self.get_http().get(&url, Some(&headers), payload).await?)
121 }
122
123 /// Convenience method to send POST requests related to an endpoint in the
124 /// API.
125 #[doc(hidden)]
126 #[inline]
127 async fn api_post(&self, url: &str, payload: &Value) -> ClientResult<String> {
128 let url = self.api_url(url);
129 let headers = self.auth_headers().await?;
130 Ok(self.get_http().post(&url, Some(&headers), payload).await?)
131 }
132
133 /// Convenience method to send PUT requests related to an endpoint in the
134 /// API.
135 #[doc(hidden)]
136 #[inline]
137 async fn api_put(&self, url: &str, payload: &Value) -> ClientResult<String> {
138 let url = self.api_url(url);
139 let headers = self.auth_headers().await?;
140 Ok(self.get_http().put(&url, Some(&headers), payload).await?)
141 }
142
143 /// Convenience method to send DELETE requests related to an endpoint in the
144 /// API.
145 #[doc(hidden)]
146 #[inline]
147 async fn api_delete(&self, url: &str, payload: &Value) -> ClientResult<String> {
148 let url = self.api_url(url);
149 let headers = self.auth_headers().await?;
150 Ok(self
151 .get_http()
152 .delete(&url, Some(&headers), payload)
153 .await?)
154 }
155
156 /// Convenience method to send POST requests related to the authentication
157 /// process.
158 #[doc(hidden)]
159 #[inline]
160 async fn auth_post(
161 &self,
162 url: &str,
163 headers: Option<&Headers>,
164 payload: &Form<'_>,
165 ) -> ClientResult<String> {
166 let url = self.auth_url(url);
167 Ok(self.get_http().post_form(&url, headers, payload).await?)
168 }
169
170 /// Updates the cache file at the internal cache path.
171 ///
172 /// This should be used whenever it's possible to, even if the cached token
173 /// isn't configured, because this will already check `Config::token_cached`
174 /// and do nothing in that case already.
175 async fn write_token_cache(&self) -> ClientResult<()> {
176 if !self.get_config().token_cached {
177 log::info!("Token cache write ignored (not configured)");
178 return Ok(());
179 }
180
181 log::info!("Writing token cache");
182 if let Some(tok) = self.get_token().lock().await.unwrap().as_ref() {
183 tok.write_cache(&self.get_config().cache_path)?;
184 }
185
186 Ok(())
187 }
188
189 /// Sends a request to Spotify for an access token.
190 async fn fetch_access_token(
191 &self,
192 payload: &Form<'_>,
193 headers: Option<&Headers>,
194 ) -> ClientResult<Token> {
195 let response = self.auth_post(auth_urls::TOKEN, headers, payload).await?;
196
197 let mut tok = serde_json::from_str::<Token>(&response)?;
198 tok.expires_at = Utc::now().checked_add_signed(tok.expires_in);
199 Ok(tok)
200 }
201
202 /// Returns a single track given the track's ID, URI or URL.
203 ///
204 /// Parameters:
205 /// - track_id - a spotify URI, URL or ID
206 ///
207 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-track)
208 async fn track(
209 &self,
210 track_id: TrackId<'_>,
211 market: Option<Market>,
212 ) -> ClientResult<FullTrack> {
213 let params = build_map([("market", market.map(Into::into))]);
214
215 let url = format!("tracks/{}", track_id.id());
216 let result = self.api_get(&url, ¶ms).await?;
217 convert_result(&result)
218 }
219
220 /// Returns a list of tracks given a list of track IDs, URIs, or URLs.
221 ///
222 /// Parameters:
223 /// - track_ids - a list of spotify URIs, URLs or IDs
224 /// - market - an ISO 3166-1 alpha-2 country code or the string from_token.
225 ///
226 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-several-tracks)
227 async fn tracks<'a>(
228 &self,
229 track_ids: impl IntoIterator<Item = TrackId<'a>> + Send + 'a,
230 market: Option<Market>,
231 ) -> ClientResult<Vec<FullTrack>> {
232 let ids = join_ids(track_ids);
233 let params = build_map([("market", market.map(Into::into))]);
234
235 let url = format!("tracks/?ids={ids}");
236 let result = self.api_get(&url, ¶ms).await?;
237 convert_result::<FullTracks>(&result).map(|x| x.tracks)
238 }
239
240 /// Returns a single artist given the artist's ID, URI or URL.
241 ///
242 /// Parameters:
243 /// - artist_id - an artist ID, URI or URL
244 ///
245 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-an-artist)
246 async fn artist(&self, artist_id: ArtistId<'_>) -> ClientResult<FullArtist> {
247 let url = format!("artists/{}", artist_id.id());
248 let result = self.api_get(&url, &Query::new()).await?;
249 convert_result(&result)
250 }
251
252 /// Returns a list of artists given the artist IDs, URIs, or URLs.
253 ///
254 /// Parameters:
255 /// - artist_ids - a list of artist IDs, URIs or URLs
256 ///
257 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-multiple-artists)
258 async fn artists<'a>(
259 &self,
260 artist_ids: impl IntoIterator<Item = ArtistId<'a>> + Send + 'a,
261 ) -> ClientResult<Vec<FullArtist>> {
262 let ids = join_ids(artist_ids);
263 let url = format!("artists/?ids={ids}");
264 let result = self.api_get(&url, &Query::new()).await?;
265
266 convert_result::<FullArtists>(&result).map(|x| x.artists)
267 }
268
269 /// Get Spotify catalog information about an artist's albums.
270 ///
271 /// Parameters:
272 /// - artist_id - the artist ID, URI or URL
273 /// - include_groups - a list of album type like 'album', 'single' that will be used to filter response. if not supplied, all album types will be returned.
274 /// - market - limit the response to one particular country.
275 /// - limit - the number of albums to return
276 /// - offset - the index of the first album to return
277 ///
278 /// See [`Self::artist_albums_manual`] for a manually paginated version of
279 /// this.
280 ///
281 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-an-artists-albums)
282 fn artist_albums<'a>(
283 &'a self,
284 artist_id: ArtistId<'a>,
285 include_groups: impl IntoIterator<Item = AlbumType> + Send + Copy + 'a,
286 market: Option<Market>,
287 ) -> Paginator<'_, ClientResult<SimplifiedAlbum>> {
288 paginate_with_ctx(
289 (self, artist_id),
290 move |(slf, artist_id), limit, offset| {
291 slf.artist_albums_manual(
292 artist_id.as_ref(),
293 include_groups,
294 market,
295 Some(limit),
296 Some(offset),
297 )
298 },
299 self.get_config().pagination_chunks,
300 )
301 }
302
303 /// The manually paginated version of [`Self::artist_albums`].
304 async fn artist_albums_manual<'a>(
305 &self,
306 artist_id: ArtistId<'_>,
307 include_groups: impl IntoIterator<Item = AlbumType> + Send + 'a,
308 market: Option<Market>,
309 limit: Option<u32>,
310 offset: Option<u32>,
311 ) -> ClientResult<Page<SimplifiedAlbum>> {
312 let limit = limit.map(|x| x.to_string());
313 let offset = offset.map(|x| x.to_string());
314 let include_groups_vec = include_groups
315 .into_iter()
316 .map(|t| t.into())
317 .collect::<Vec<&'static str>>();
318 let include_groups_opt = include_groups_vec
319 .is_empty()
320 .not()
321 .then_some(include_groups_vec)
322 .map(|t| t.join(","));
323
324 let params = build_map([
325 ("include_groups", include_groups_opt.as_deref()),
326 ("market", market.map(Into::into)),
327 ("limit", limit.as_deref()),
328 ("offset", offset.as_deref()),
329 ]);
330
331 let url = format!("artists/{}/albums", artist_id.id());
332 let result = self.api_get(&url, ¶ms).await?;
333 convert_result(&result)
334 }
335
336 /// Get Spotify catalog information about an artist's top 10 tracks by
337 /// country.
338 ///
339 /// Parameters:
340 /// - artist_id - the artist ID, URI or URL
341 /// - market - limit the response to one particular country.
342 ///
343 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-an-artists-top-tracks)
344 async fn artist_top_tracks(
345 &self,
346 artist_id: ArtistId<'_>,
347 market: Option<Market>,
348 ) -> ClientResult<Vec<FullTrack>> {
349 let params = build_map([("market", market.map(Into::into))]);
350
351 let url = format!("artists/{}/top-tracks", artist_id.id());
352 let result = self.api_get(&url, ¶ms).await?;
353 convert_result::<FullTracks>(&result).map(|x| x.tracks)
354 }
355
356 /// Get Spotify catalog information about artists similar to an identified
357 /// artist. Similarity is based on analysis of the Spotify community's
358 /// listening history.
359 ///
360 /// Parameters:
361 /// - artist_id - the artist ID, URI or URL
362 ///
363 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-an-artists-related-artists)
364 async fn artist_related_artists(
365 &self,
366 artist_id: ArtistId<'_>,
367 ) -> ClientResult<Vec<FullArtist>> {
368 let url = format!("artists/{}/related-artists", artist_id.id());
369 let result = self.api_get(&url, &Query::new()).await?;
370 convert_result::<FullArtists>(&result).map(|x| x.artists)
371 }
372
373 /// Returns a single album given the album's ID, URIs or URL.
374 ///
375 /// Parameters:
376 /// - album_id - the album ID, URI or URL
377 ///
378 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-an-album)
379 async fn album(
380 &self,
381 album_id: AlbumId<'_>,
382 market: Option<Market>,
383 ) -> ClientResult<FullAlbum> {
384 let params = build_map([("market", market.map(Into::into))]);
385
386 let url = format!("albums/{}", album_id.id());
387 let result = self.api_get(&url, ¶ms).await?;
388 convert_result(&result)
389 }
390
391 /// Returns a list of albums given the album IDs, URIs, or URLs.
392 ///
393 /// Parameters:
394 /// - albums_ids - a list of album IDs, URIs or URLs
395 ///
396 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-multiple-albums)
397 async fn albums<'a>(
398 &self,
399 album_ids: impl IntoIterator<Item = AlbumId<'a>> + Send + 'a,
400 market: Option<Market>,
401 ) -> ClientResult<Vec<FullAlbum>> {
402 let params = build_map([("market", market.map(Into::into))]);
403
404 let ids = join_ids(album_ids);
405 let url = format!("albums/?ids={ids}");
406 let result = self.api_get(&url, ¶ms).await?;
407 convert_result::<FullAlbums>(&result).map(|x| x.albums)
408 }
409
410 /// Search for an Item. Get Spotify catalog information about artists,
411 /// albums, tracks or playlists that match a keyword string.
412 ///
413 /// Parameters:
414 /// - q - the search query
415 /// - limit - the number of items to return
416 /// - offset - the index of the first item to return
417 /// - type - the type of item to return. One of 'artist', 'album', 'track',
418 /// 'playlist', 'show' or 'episode'
419 /// - market - An ISO 3166-1 alpha-2 country code or the string from_token.
420 /// - include_external: Optional.Possible values: audio. If
421 /// include_external=audio is specified the response will include any
422 /// relevant audio content that is hosted externally.
423 ///
424 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/search)
425 async fn search(
426 &self,
427 q: &str,
428 _type: SearchType,
429 market: Option<Market>,
430 include_external: Option<IncludeExternal>,
431 limit: Option<u32>,
432 offset: Option<u32>,
433 ) -> ClientResult<SearchResult> {
434 let limit = limit.map(|s| s.to_string());
435 let offset = offset.map(|s| s.to_string());
436 let params = build_map([
437 ("q", Some(q)),
438 ("type", Some(_type.into())),
439 ("market", market.map(Into::into)),
440 ("include_external", include_external.map(Into::into)),
441 ("limit", limit.as_deref()),
442 ("offset", offset.as_deref()),
443 ]);
444
445 let result = self.api_get("search", ¶ms).await?;
446 convert_result(&result)
447 }
448
449 /// Get Spotify catalog information about an album's tracks.
450 ///
451 /// Parameters:
452 /// - album_id - the album ID, URI or URL
453 /// - limit - the number of items to return
454 /// - offset - the index of the first item to return
455 ///
456 /// See [`Self::album_track_manual`] for a manually paginated version of
457 /// this.
458 ///
459 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-an-albums-tracks)
460 fn album_track<'a>(
461 &'a self,
462 album_id: AlbumId<'a>,
463 market: Option<Market>,
464 ) -> Paginator<'_, ClientResult<SimplifiedTrack>> {
465 paginate_with_ctx(
466 (self, album_id),
467 move |(slf, album_id), limit, offset| {
468 slf.album_track_manual(album_id.as_ref(), market, Some(limit), Some(offset))
469 },
470 self.get_config().pagination_chunks,
471 )
472 }
473
474 /// The manually paginated version of [`Self::album_track`].
475 async fn album_track_manual(
476 &self,
477 album_id: AlbumId<'_>,
478 market: Option<Market>,
479 limit: Option<u32>,
480 offset: Option<u32>,
481 ) -> ClientResult<Page<SimplifiedTrack>> {
482 let limit = limit.map(|s| s.to_string());
483 let offset = offset.map(|s| s.to_string());
484 let params = build_map([
485 ("limit", limit.as_deref()),
486 ("offset", offset.as_deref()),
487 ("market", market.map(Into::into)),
488 ]);
489
490 let url = format!("albums/{}/tracks", album_id.id());
491 let result = self.api_get(&url, ¶ms).await?;
492 convert_result(&result)
493 }
494
495 /// Gets basic profile information about a Spotify User.
496 ///
497 /// Parameters:
498 /// - user - the id of the usr
499 ///
500 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-users-profile)
501 async fn user(&self, user_id: UserId<'_>) -> ClientResult<PublicUser> {
502 let url = format!("users/{}", user_id.id());
503 let result = self.api_get(&url, &Query::new()).await?;
504 convert_result(&result)
505 }
506
507 /// Get full details about Spotify playlist.
508 ///
509 /// Parameters:
510 /// - playlist_id - the id of the playlist
511 /// - market - an ISO 3166-1 alpha-2 country code or the string from_token.
512 ///
513 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-playlist)
514 async fn playlist(
515 &self,
516 playlist_id: PlaylistId<'_>,
517 fields: Option<&str>,
518 market: Option<Market>,
519 ) -> ClientResult<FullPlaylist> {
520 let params = build_map([("fields", fields), ("market", market.map(Into::into))]);
521
522 let url = format!("playlists/{}", playlist_id.id());
523 let result = self.api_get(&url, ¶ms).await?;
524 convert_result(&result)
525 }
526
527 /// Gets playlist of a user.
528 ///
529 /// Parameters:
530 /// - user_id - the id of the user
531 /// - playlist_id - the id of the playlist
532 /// - fields - which fields to return
533 ///
534 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-list-users-playlists)
535 async fn user_playlist(
536 &self,
537 user_id: UserId<'_>,
538 playlist_id: Option<PlaylistId<'_>>,
539 fields: Option<&str>,
540 ) -> ClientResult<FullPlaylist> {
541 let params = build_map([("fields", fields)]);
542
543 let url = match playlist_id {
544 Some(playlist_id) => format!("users/{}/playlists/{}", user_id.id(), playlist_id.id()),
545 None => format!("users/{}/starred", user_id.id()),
546 };
547 let result = self.api_get(&url, ¶ms).await?;
548 convert_result(&result)
549 }
550
551 /// Check to see if the given users are following the given playlist.
552 ///
553 /// Parameters:
554 /// - playlist_id - the id of the playlist
555 /// - user_ids - the ids of the users that you want to check to see if they
556 /// follow the playlist. Maximum: 5 ids.
557 ///
558 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/check-if-user-follows-playlist)
559 async fn playlist_check_follow(
560 &self,
561 playlist_id: PlaylistId<'_>,
562 user_ids: &[UserId<'_>],
563 ) -> ClientResult<Vec<bool>> {
564 debug_assert!(
565 user_ids.len() <= 5,
566 "The maximum length of user ids is limited to 5 :-)"
567 );
568 let url = format!(
569 "playlists/{}/followers/contains?ids={}",
570 playlist_id.id(),
571 user_ids.iter().map(Id::id).collect::<Vec<_>>().join(","),
572 );
573 let result = self.api_get(&url, &Query::new()).await?;
574 convert_result(&result)
575 }
576
577 /// Get Spotify catalog information for a single show identified by its unique Spotify ID.
578 ///
579 /// Path Parameters:
580 /// - id: The Spotify ID for the show.
581 ///
582 /// Query Parameters
583 /// - market(Optional): An ISO 3166-1 alpha-2 country code or the string from_token.
584 ///
585 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-a-show)
586 async fn get_a_show(&self, id: ShowId<'_>, market: Option<Market>) -> ClientResult<FullShow> {
587 let params = build_map([("market", market.map(Into::into))]);
588
589 let url = format!("shows/{}", id.id());
590 let result = self.api_get(&url, ¶ms).await?;
591 convert_result(&result)
592 }
593
594 /// Get Spotify catalog information for multiple shows based on their
595 /// Spotify IDs.
596 ///
597 /// Query Parameters
598 /// - ids(Required) A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs.
599 /// - market(Optional) An ISO 3166-1 alpha-2 country code or the string from_token.
600 ///
601 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-multiple-shows)
602 async fn get_several_shows<'a>(
603 &self,
604 ids: impl IntoIterator<Item = ShowId<'a>> + Send + 'a,
605 market: Option<Market>,
606 ) -> ClientResult<Vec<SimplifiedShow>> {
607 let ids = join_ids(ids);
608 let params = build_map([("ids", Some(&ids)), ("market", market.map(Into::into))]);
609
610 let result = self.api_get("shows", ¶ms).await?;
611 convert_result::<SeversalSimplifiedShows>(&result).map(|x| x.shows)
612 }
613
614 /// Get Spotify catalog information about an show’s episodes. Optional
615 /// parameters can be used to limit the number of episodes returned.
616 ///
617 /// Path Parameters
618 /// - id: The Spotify ID for the show.
619 ///
620 /// Query Parameters
621 /// - limit: Optional. The maximum number of episodes to return. Default: 20. Minimum: 1. Maximum: 50.
622 /// - offset: Optional. The index of the first episode to return. Default: 0 (the first object). Use with limit to get the next set of episodes.
623 /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token.
624 ///
625 /// See [`Self::get_shows_episodes_manual`] for a manually paginated version
626 /// of this.
627 ///
628 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-a-shows-episodes)
629 fn get_shows_episodes<'a>(
630 &'a self,
631 id: ShowId<'a>,
632 market: Option<Market>,
633 ) -> Paginator<'_, ClientResult<SimplifiedEpisode>> {
634 paginate_with_ctx(
635 (self, id),
636 move |(slf, id), limit, offset| {
637 slf.get_shows_episodes_manual(id.as_ref(), market, Some(limit), Some(offset))
638 },
639 self.get_config().pagination_chunks,
640 )
641 }
642
643 /// The manually paginated version of [`Self::get_shows_episodes`].
644 async fn get_shows_episodes_manual(
645 &self,
646 id: ShowId<'_>,
647 market: Option<Market>,
648 limit: Option<u32>,
649 offset: Option<u32>,
650 ) -> ClientResult<Page<SimplifiedEpisode>> {
651 let limit = limit.map(|x| x.to_string());
652 let offset = offset.map(|x| x.to_string());
653 let params = build_map([
654 ("market", market.map(Into::into)),
655 ("limit", limit.as_deref()),
656 ("offset", offset.as_deref()),
657 ]);
658
659 let url = format!("shows/{}/episodes", id.id());
660 let result = self.api_get(&url, ¶ms).await?;
661 convert_result(&result)
662 }
663
664 /// Get Spotify catalog information for a single episode identified by its unique Spotify ID.
665 ///
666 /// Path Parameters
667 /// - id: The Spotify ID for the episode.
668 ///
669 /// Query Parameters
670 /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token.
671 ///
672 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-an-episode)
673 async fn get_an_episode(
674 &self,
675 id: EpisodeId<'_>,
676 market: Option<Market>,
677 ) -> ClientResult<FullEpisode> {
678 let url = format!("episodes/{}", id.id());
679 let params = build_map([("market", market.map(Into::into))]);
680
681 let result = self.api_get(&url, ¶ms).await?;
682 convert_result(&result)
683 }
684
685 /// Get Spotify catalog information for multiple episodes based on their Spotify IDs.
686 ///
687 /// Query Parameters
688 /// - ids: Required. A comma-separated list of the Spotify IDs for the episodes. Maximum: 50 IDs.
689 /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token.
690 ///
691 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-multiple-episodes)
692 async fn get_several_episodes<'a>(
693 &self,
694 ids: impl IntoIterator<Item = EpisodeId<'a>> + Send + 'a,
695 market: Option<Market>,
696 ) -> ClientResult<Vec<FullEpisode>> {
697 let ids = join_ids(ids);
698 let params = build_map([("ids", Some(&ids)), ("market", market.map(Into::into))]);
699
700 let result = self.api_get("episodes", ¶ms).await?;
701 convert_result::<EpisodesPayload>(&result).map(|x| x.episodes)
702 }
703
704 /// Get audio features for a track
705 ///
706 /// Parameters:
707 /// - track - track URI, URL or ID
708 ///
709 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-audio-features)
710 async fn track_features(&self, track_id: TrackId<'_>) -> ClientResult<AudioFeatures> {
711 let url = format!("audio-features/{}", track_id.id());
712 let result = self.api_get(&url, &Query::new()).await?;
713 convert_result(&result)
714 }
715
716 /// Get Audio Features for Several Tracks
717 ///
718 /// Parameters:
719 /// - tracks a list of track URIs, URLs or IDs
720 ///
721 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-several-audio-features)
722 async fn tracks_features<'a>(
723 &self,
724 track_ids: impl IntoIterator<Item = TrackId<'a>> + Send + 'a,
725 ) -> ClientResult<Option<Vec<AudioFeatures>>> {
726 let url = format!("audio-features/?ids={}", join_ids(track_ids));
727
728 let result = self.api_get(&url, &Query::new()).await?;
729 if result.is_empty() {
730 Ok(None)
731 } else if let Some(payload) = convert_result::<Option<AudioFeaturesPayload>>(&result)? {
732 let audio_features = payload.audio_features.into_iter().flatten().collect();
733 Ok(Some(audio_features))
734 } else {
735 Ok(None)
736 }
737 }
738
739 /// Get Audio Analysis for a Track
740 ///
741 /// Parameters:
742 /// - track_id - a track URI, URL or ID
743 ///
744 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-audio-analysis)
745 async fn track_analysis(&self, track_id: TrackId<'_>) -> ClientResult<AudioAnalysis> {
746 let url = format!("audio-analysis/{}", track_id.id());
747 let result = self.api_get(&url, &Query::new()).await?;
748 convert_result(&result)
749 }
750
751 /// Get a list of new album releases featured in Spotify
752 ///
753 /// Parameters:
754 /// - country - An ISO 3166-1 alpha-2 country code or string from_token.
755 /// - locale - The desired language, consisting of an ISO 639 language code
756 /// and an ISO 3166-1 alpha-2 country code, joined by an underscore.
757 /// - limit - The maximum number of items to return. Default: 20.
758 /// Minimum: 1. Maximum: 50
759 /// - offset - The index of the first item to return. Default: 0 (the first
760 /// object). Use with limit to get the next set of items.
761 ///
762 /// See [`Self::categories_manual`] for a manually paginated version of
763 /// this.
764 ///
765 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-categories)
766 fn categories<'a>(
767 &'a self,
768 locale: Option<&'a str>,
769 country: Option<Market>,
770 ) -> Paginator<'_, ClientResult<Category>> {
771 paginate(
772 move |limit, offset| self.categories_manual(locale, country, Some(limit), Some(offset)),
773 self.get_config().pagination_chunks,
774 )
775 }
776
777 /// The manually paginated version of [`Self::categories`].
778 async fn categories_manual(
779 &self,
780 locale: Option<&str>,
781 country: Option<Market>,
782 limit: Option<u32>,
783 offset: Option<u32>,
784 ) -> ClientResult<Page<Category>> {
785 let limit = limit.map(|x| x.to_string());
786 let offset = offset.map(|x| x.to_string());
787 let params = build_map([
788 ("locale", locale),
789 ("country", country.map(Into::into)),
790 ("limit", limit.as_deref()),
791 ("offset", offset.as_deref()),
792 ]);
793 let result = self.api_get("browse/categories", ¶ms).await?;
794 convert_result::<PageCategory>(&result).map(|x| x.categories)
795 }
796
797 /// Get a list of playlists in a category in Spotify
798 ///
799 /// Parameters:
800 /// - category_id - The category id to get playlists from.
801 /// - country - An ISO 3166-1 alpha-2 country code or the string from_token.
802 /// - limit - The maximum number of items to return. Default: 20.
803 /// Minimum: 1. Maximum: 50
804 /// - offset - The index of the first item to return. Default: 0 (the first
805 /// object). Use with limit to get the next set of items.
806 ///
807 /// See [`Self::category_playlists_manual`] for a manually paginated version
808 /// of this.
809 ///
810 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-a-categories-playlists)
811 fn category_playlists<'a>(
812 &'a self,
813 category_id: &'a str,
814 country: Option<Market>,
815 ) -> Paginator<'_, ClientResult<SimplifiedPlaylist>> {
816 paginate(
817 move |limit, offset| {
818 self.category_playlists_manual(category_id, country, Some(limit), Some(offset))
819 },
820 self.get_config().pagination_chunks,
821 )
822 }
823
824 /// The manually paginated version of [`Self::category_playlists`].
825 async fn category_playlists_manual(
826 &self,
827 category_id: &str,
828 country: Option<Market>,
829 limit: Option<u32>,
830 offset: Option<u32>,
831 ) -> ClientResult<Page<SimplifiedPlaylist>> {
832 let limit = limit.map(|x| x.to_string());
833 let offset = offset.map(|x| x.to_string());
834 let params = build_map([
835 ("country", country.map(Into::into)),
836 ("limit", limit.as_deref()),
837 ("offset", offset.as_deref()),
838 ]);
839
840 let url = format!("browse/categories/{category_id}/playlists");
841 let result = self.api_get(&url, ¶ms).await?;
842 convert_result::<CategoryPlaylists>(&result).map(|x| x.playlists)
843 }
844
845 /// Get a list of Spotify featured playlists.
846 ///
847 /// Parameters:
848 /// - locale - The desired language, consisting of a lowercase ISO 639
849 /// language code and an uppercase ISO 3166-1 alpha-2 country code,
850 /// joined by an underscore.
851 /// - country - An ISO 3166-1 alpha-2 country code or the string from_token.
852 /// - timestamp - A timestamp in ISO 8601 format: yyyy-MM-ddTHH:mm:ss. Use
853 /// this parameter to specify the user's local time to get results
854 /// tailored for that specific date and time in the day
855 /// - limit - The maximum number of items to return. Default: 20.
856 /// Minimum: 1. Maximum: 50
857 /// - offset - The index of the first item to return. Default: 0
858 /// (the first object). Use with limit to get the next set of
859 /// items.
860 ///
861 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-featured-playlists)
862 async fn featured_playlists(
863 &self,
864 locale: Option<&str>,
865 country: Option<Market>,
866 timestamp: Option<chrono::DateTime<chrono::Utc>>,
867 limit: Option<u32>,
868 offset: Option<u32>,
869 ) -> ClientResult<FeaturedPlaylists> {
870 let limit = limit.map(|x| x.to_string());
871 let offset = offset.map(|x| x.to_string());
872 let timestamp = timestamp.map(|x| x.to_rfc3339());
873 let params = build_map([
874 ("locale", locale),
875 ("country", country.map(Into::into)),
876 ("timestamp", timestamp.as_deref()),
877 ("limit", limit.as_deref()),
878 ("offset", offset.as_deref()),
879 ]);
880
881 let result = self.api_get("browse/featured-playlists", ¶ms).await?;
882 convert_result(&result)
883 }
884
885 /// Get a list of new album releases featured in Spotify.
886 ///
887 /// Parameters:
888 /// - country - An ISO 3166-1 alpha-2 country code or string from_token.
889 /// - limit - The maximum number of items to return. Default: 20.
890 /// Minimum: 1. Maximum: 50
891 /// - offset - The index of the first item to return. Default: 0 (the first
892 /// object). Use with limit to get the next set of items.
893 ///
894 /// See [`Self::new_releases_manual`] for a manually paginated version of
895 /// this.
896 ///
897 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-new-releases)
898 fn new_releases(
899 &self,
900 country: Option<Market>,
901 ) -> Paginator<'_, ClientResult<SimplifiedAlbum>> {
902 paginate(
903 move |limit, offset| self.new_releases_manual(country, Some(limit), Some(offset)),
904 self.get_config().pagination_chunks,
905 )
906 }
907
908 /// The manually paginated version of [`Self::new_releases`].
909 async fn new_releases_manual(
910 &self,
911 country: Option<Market>,
912 limit: Option<u32>,
913 offset: Option<u32>,
914 ) -> ClientResult<Page<SimplifiedAlbum>> {
915 let limit = limit.map(|x| x.to_string());
916 let offset = offset.map(|x| x.to_string());
917 let params = build_map([
918 ("country", country.map(Into::into)),
919 ("limit", limit.as_deref()),
920 ("offset", offset.as_deref()),
921 ]);
922
923 let result = self.api_get("browse/new-releases", ¶ms).await?;
924 convert_result::<PageSimplifiedAlbums>(&result).map(|x| x.albums)
925 }
926
927 /// Get Recommendations Based on Seeds
928 ///
929 /// Parameters:
930 /// - attributes - restrictions on attributes for the selected tracks, such
931 /// as `min_acousticness` or `target_duration_ms`.
932 /// - seed_artists - a list of artist IDs, URIs or URLs
933 /// - seed_tracks - a list of artist IDs, URIs or URLs
934 /// - seed_genres - a list of genre names. Available genres for
935 /// - market - An ISO 3166-1 alpha-2 country code or the string from_token.
936 /// If provided, all results will be playable in this country.
937 /// - limit - The maximum number of items to return. Default: 20.
938 /// Minimum: 1. Maximum: 100
939 /// - `min/max/target_<attribute>` - For the tuneable track attributes
940 /// listed in the documentation, these values provide filters and
941 /// targeting on results.
942 ///
943 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-recommendations)
944 async fn recommendations<'a>(
945 &self,
946 attributes: impl IntoIterator<Item = RecommendationsAttribute> + Send + 'a,
947 seed_artists: Option<impl IntoIterator<Item = ArtistId<'a>> + Send + 'a>,
948 seed_genres: Option<impl IntoIterator<Item = &'a str> + Send + 'a>,
949 seed_tracks: Option<impl IntoIterator<Item = TrackId<'a>> + Send + 'a>,
950 market: Option<Market>,
951 limit: Option<u32>,
952 ) -> ClientResult<Recommendations> {
953 let seed_artists = seed_artists.map(join_ids);
954 let seed_genres = seed_genres.map(|x| x.into_iter().collect::<Vec<_>>().join(","));
955 let seed_tracks = seed_tracks.map(join_ids);
956 let limit = limit.map(|x| x.to_string());
957 let mut params = build_map([
958 ("seed_artists", seed_artists.as_deref()),
959 ("seed_genres", seed_genres.as_deref()),
960 ("seed_tracks", seed_tracks.as_deref()),
961 ("market", market.map(Into::into)),
962 ("limit", limit.as_deref()),
963 ]);
964
965 // First converting the attributes into owned `String`s
966 let owned_attributes = attributes
967 .into_iter()
968 .map(|attr| (<&str>::from(attr).to_owned(), attr.value_string()))
969 .collect::<HashMap<_, _>>();
970 // Afterwards converting the values into `&str`s; otherwise they
971 // wouldn't live long enough
972 let borrowed_attributes = owned_attributes
973 .iter()
974 .map(|(key, value)| (key.as_str(), value.as_str()));
975 // And finally adding all of them to the payload
976 params.extend(borrowed_attributes);
977
978 let result = self.api_get("recommendations", ¶ms).await?;
979 convert_result(&result)
980 }
981
982 /// Get full details of the items of a playlist owned by a user.
983 ///
984 /// Parameters:
985 /// - playlist_id - the id of the playlist
986 /// - fields - which fields to return
987 /// - limit - the maximum number of tracks to return
988 /// - offset - the index of the first track to return
989 /// - market - an ISO 3166-1 alpha-2 country code or the string from_token.
990 ///
991 /// See [`Self::playlist_items_manual`] for a manually paginated version of
992 /// this.
993 ///
994 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-playlists-tracks)
995 fn playlist_items<'a>(
996 &'a self,
997 playlist_id: PlaylistId<'a>,
998 fields: Option<&'a str>,
999 market: Option<Market>,
1000 ) -> Paginator<'_, ClientResult<PlaylistItem>> {
1001 paginate_with_ctx(
1002 (self, playlist_id, fields),
1003 move |(slf, playlist_id, fields), limit, offset| {
1004 slf.playlist_items_manual(
1005 playlist_id.as_ref(),
1006 *fields,
1007 market,
1008 Some(limit),
1009 Some(offset),
1010 )
1011 },
1012 self.get_config().pagination_chunks,
1013 )
1014 }
1015
1016 /// The manually paginated version of [`Self::playlist_items`].
1017 async fn playlist_items_manual(
1018 &self,
1019 playlist_id: PlaylistId<'_>,
1020 fields: Option<&str>,
1021 market: Option<Market>,
1022 limit: Option<u32>,
1023 offset: Option<u32>,
1024 ) -> ClientResult<Page<PlaylistItem>> {
1025 let limit = limit.map(|s| s.to_string());
1026 let offset = offset.map(|s| s.to_string());
1027 let params = build_map([
1028 ("fields", fields),
1029 ("market", market.map(Into::into)),
1030 ("limit", limit.as_deref()),
1031 ("offset", offset.as_deref()),
1032 ]);
1033
1034 let url = format!("playlists/{}/tracks", playlist_id.id());
1035 let result = self.api_get(&url, ¶ms).await?;
1036 convert_result(&result)
1037 }
1038
1039 /// Gets playlists of a user.
1040 ///
1041 /// Parameters:
1042 /// - user_id - the id of the usr
1043 /// - limit - the number of items to return
1044 /// - offset - the index of the first item to return
1045 ///
1046 /// See [`Self::user_playlists_manual`] for a manually paginated version of
1047 /// this.
1048 ///
1049 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-list-users-playlists)
1050 fn user_playlists<'a>(
1051 &'a self,
1052 user_id: UserId<'a>,
1053 ) -> Paginator<'_, ClientResult<SimplifiedPlaylist>> {
1054 paginate_with_ctx(
1055 (self, user_id),
1056 move |(slf, user_id), limit, offset| {
1057 slf.user_playlists_manual(user_id.as_ref(), Some(limit), Some(offset))
1058 },
1059 self.get_config().pagination_chunks,
1060 )
1061 }
1062
1063 /// The manually paginated version of [`Self::user_playlists`].
1064 async fn user_playlists_manual(
1065 &self,
1066 user_id: UserId<'_>,
1067 limit: Option<u32>,
1068 offset: Option<u32>,
1069 ) -> ClientResult<Page<SimplifiedPlaylist>> {
1070 let limit = limit.map(|s| s.to_string());
1071 let offset = offset.map(|s| s.to_string());
1072 let params = build_map([("limit", limit.as_deref()), ("offset", offset.as_deref())]);
1073
1074 let url = format!("users/{}/playlists", user_id.id());
1075 let result = self.api_get(&url, ¶ms).await?;
1076 convert_result(&result)
1077 }
1078}