rspotify_s/clients/oauth.rs
1use crate::{
2 clients::{
3 append_device_id, convert_result,
4 pagination::{paginate, Paginator},
5 BaseClient,
6 },
7 http::Query,
8 join_ids,
9 model::*,
10 util::{build_map, JsonBuilder},
11 ClientResult, OAuth, Token,
12};
13
14use std::collections::HashMap;
15
16use maybe_async::maybe_async;
17use rspotify_model::idtypes::{PlayContextId, PlayableId};
18use serde_json::{json, Map};
19use url::Url;
20
21/// This trait implements the methods available strictly to clients with user
22/// authorization, including some parts of the authentication flow that are
23/// shared, and the endpoints.
24///
25/// Note that the base trait [`BaseClient`](crate::clients::BaseClient) may
26/// have endpoints that conditionally require authorization like
27/// [`user_playlist`](crate::clients::BaseClient::user_playlist). This trait
28/// only separates endpoints that *always* need authorization from the base
29/// ones.
30#[cfg_attr(target_arch = "wasm32", maybe_async(?Send))]
31#[cfg_attr(not(target_arch = "wasm32"), maybe_async)]
32pub trait OAuthClient: BaseClient {
33 fn get_oauth(&self) -> &OAuth;
34
35 /// Obtains a user access token given a code, as part of the OAuth
36 /// authentication. The access token will be saved internally.
37 async fn request_token(&self, code: &str) -> ClientResult<()>;
38
39 /// Tries to read the cache file's token.
40 ///
41 /// This will return an error if the token couldn't be read (e.g. it's not
42 /// available or the JSON is malformed). It may return `Ok(None)` if:
43 ///
44 /// * The read token is expired and `allow_expired` is false
45 /// * Its scopes don't match with the current client (you will need to
46 /// re-authenticate to gain access to more scopes)
47 /// * The cached token is disabled in the config
48 ///
49 /// # Note
50 /// This function's implementation differs slightly from the implementation
51 /// in [`ClientCredsSpotify::read_token_cache`]. The boolean parameter
52 /// `allow_expired` allows users to load expired tokens from the cache.
53 /// This functionality can be used to access the refresh token and obtain
54 /// a new, valid token. This option is unavailable in the implementation of
55 /// [`ClientCredsSpotify::read_token_cache`] since the client credentials
56 /// authorization flow does not have a refresh token and instead requires
57 /// the application re-authenticate.
58 ///
59 /// [`ClientCredsSpotify::read_token_cache`]: crate::client_creds::ClientCredsSpotify::read_token_cache
60 async fn read_token_cache(&self, allow_expired: bool) -> ClientResult<Option<Token>> {
61 if !self.get_config().token_cached {
62 log::info!("Auth token cache read ignored (not configured)");
63 return Ok(None);
64 }
65
66 log::info!("Reading auth token cache");
67 let token = Token::from_cache(&self.get_config().cache_path)?;
68 if !self.get_oauth().scopes.is_subset(&token.scopes)
69 || (!allow_expired && token.is_expired())
70 {
71 // Invalid token, since it doesn't have at least the currently
72 // required scopes or it's expired.
73 Ok(None)
74 } else {
75 Ok(Some(token))
76 }
77 }
78
79 /// Parse the response code in the given response url. If the URL cannot be
80 /// parsed or the `code` parameter is not present, this will return `None`.
81 ///
82 // As the [RFC
83 // indicates](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1),
84 // the state should be the same between the request and the callback. This
85 // will also return `None` if this is not true.
86 fn parse_response_code(&self, url: &str) -> Option<String> {
87 let url = Url::parse(url).ok()?;
88 let params = url.query_pairs().collect::<HashMap<_, _>>();
89
90 let code = params.get("code")?;
91
92 // Making sure the state is the same
93 let expected_state = &self.get_oauth().state;
94 let state = params.get("state").map(AsRef::as_ref);
95 if state != Some(expected_state) {
96 log::error!("Request state doesn't match the callback state");
97 return None;
98 }
99
100 Some(code.to_string())
101 }
102
103 /// Tries to open the authorization URL in the user's browser, and returns
104 /// the obtained code.
105 ///
106 /// Note: this method requires the `cli` feature.
107 #[cfg(feature = "cli")]
108 fn get_code_from_user(&self, url: &str) -> ClientResult<String> {
109 use crate::ClientError;
110
111 log::info!("Opening brower with auth URL");
112 match webbrowser::open(url) {
113 Ok(_) => println!("Opened {} in your browser.", url),
114 Err(why) => eprintln!(
115 "Error when trying to open an URL in your browser: {:?}. \
116 Please navigate here manually: {}",
117 why, url
118 ),
119 }
120
121 log::info!("Prompting user for code");
122 println!("Please enter the URL you were redirected to: ");
123 let mut input = String::new();
124 std::io::stdin().read_line(&mut input)?;
125 let code = self
126 .parse_response_code(&input)
127 .ok_or_else(|| ClientError::Cli("unable to parse the response code".to_string()))?;
128
129 Ok(code)
130 }
131
132 /// Opens up the authorization URL in the user's browser so that it can
133 /// authenticate. It reads from the standard input the redirect URI
134 /// in order to obtain the access token information. The resulting access
135 /// token will be saved internally once the operation is successful.
136 ///
137 /// If the [`Config::token_cached`] setting is enabled for this client,
138 /// and a token exists in the cache, the token will be loaded and the client
139 /// will attempt to automatically refresh the token if it is expired. If
140 /// the token was unable to be refreshed, the client will then prompt the
141 /// user for the token as normal.
142 ///
143 /// Note: this method requires the `cli` feature.
144 ///
145 /// [`Config::token_cached`]: crate::Config::token_cached
146 #[cfg(feature = "cli")]
147 #[maybe_async]
148 async fn prompt_for_token(&self, url: &str) -> ClientResult<()> {
149 match self.read_token_cache(true).await {
150 Ok(Some(new_token)) => {
151 let expired = new_token.is_expired();
152
153 // Load token into client regardless of whether it's expired o
154 // not, since it will be refreshed later anyway.
155 *self.get_token().lock().await.unwrap() = Some(new_token);
156
157 if expired {
158 // Ensure that we actually got a token from the refetch
159 match self.refetch_token().await? {
160 Some(refreshed_token) => {
161 log::info!("Successfully refreshed expired token from token cache");
162 *self.get_token().lock().await.unwrap() = Some(refreshed_token)
163 }
164 // If not, prompt the user for it
165 None => {
166 log::info!("Unable to refresh expired token from token cache");
167 let code = self.get_code_from_user(url)?;
168 self.request_token(&code).await?;
169 }
170 }
171 }
172 }
173 // Otherwise following the usual procedure to get the token.
174 _ => {
175 let code = self.get_code_from_user(url)?;
176 self.request_token(&code).await?;
177 }
178 }
179
180 self.write_token_cache().await
181 }
182
183 /// Get current user playlists without required getting his profile.
184 ///
185 /// Parameters:
186 /// - limit - the number of items to return
187 /// - offset - the index of the first item to return
188 ///
189 /// See [`Self::current_user_playlists_manual`] for a manually paginated
190 /// version of this.
191 ///
192 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-a-list-of-current-users-playlists)
193 fn current_user_playlists(&self) -> Paginator<'_, ClientResult<SimplifiedPlaylist>> {
194 paginate(
195 move |limit, offset| self.current_user_playlists_manual(Some(limit), Some(offset)),
196 self.get_config().pagination_chunks,
197 )
198 }
199
200 /// The manually paginated version of [`Self::current_user_playlists`].
201 async fn current_user_playlists_manual(
202 &self,
203 limit: Option<u32>,
204 offset: Option<u32>,
205 ) -> ClientResult<Page<SimplifiedPlaylist>> {
206 let limit = limit.map(|s| s.to_string());
207 let offset = offset.map(|s| s.to_string());
208 let params = build_map([("limit", limit.as_deref()), ("offset", offset.as_deref())]);
209
210 let result = self.api_get("me/playlists", ¶ms).await?;
211 convert_result(&result)
212 }
213
214 /// Creates a playlist for a user.
215 ///
216 /// Parameters:
217 /// - user_id - the id of the user
218 /// - name - the name of the playlist
219 /// - public - is the created playlist public
220 /// - description - the description of the playlist
221 /// - collaborative - if the playlist will be collaborative. Note:
222 /// to create a collaborative playlist you must also set public to false
223 ///
224 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/create-playlist)
225 async fn user_playlist_create(
226 &self,
227 user_id: UserId<'_>,
228 name: &str,
229 public: Option<bool>,
230 collaborative: Option<bool>,
231 description: Option<&str>,
232 ) -> ClientResult<FullPlaylist> {
233 debug_assert!(
234 !(collaborative.unwrap_or(false) && public.unwrap_or(false)),
235 "To create a collaborative playlist you must also set public to \
236 false. See the reference for more information."
237 );
238
239 let params = JsonBuilder::new()
240 .required("name", name)
241 .optional("public", public)
242 .optional("collaborative", collaborative)
243 .optional("description", description)
244 .build();
245
246 let url = format!("users/{}/playlists", user_id.id());
247 let result = self.api_post(&url, ¶ms).await?;
248 convert_result(&result)
249 }
250
251 /// Changes a playlist's name and/or public/private state.
252 ///
253 /// Parameters:
254 /// - playlist_id - the id of the playlist
255 /// - name - optional name of the playlist
256 /// - public - optional is the playlist public
257 /// - collaborative - optional is the playlist collaborative
258 /// - description - optional description of the playlist
259 ///
260 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/change-playlist-details)
261 async fn playlist_change_detail(
262 &self,
263 playlist_id: PlaylistId<'_>,
264 name: Option<&str>,
265 public: Option<bool>,
266 description: Option<&str>,
267 collaborative: Option<bool>,
268 ) -> ClientResult<String> {
269 let params = JsonBuilder::new()
270 .optional("name", name)
271 .optional("public", public)
272 .optional("collaborative", collaborative)
273 .optional("description", description)
274 .build();
275
276 let url = format!("playlists/{}", playlist_id.id());
277 self.api_put(&url, ¶ms).await
278 }
279
280 /// Unfollows (deletes) a playlist for a user.
281 ///
282 /// Parameters:
283 /// - playlist_id - the id of the playlist
284 ///
285 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/unfollow-playlist)
286 async fn playlist_unfollow(&self, playlist_id: PlaylistId<'_>) -> ClientResult<()> {
287 let url = format!("playlists/{}/followers", playlist_id.id());
288 self.api_delete(&url, &json!({})).await?;
289
290 Ok(())
291 }
292
293 /// Adds items to a playlist.
294 ///
295 /// Parameters:
296 /// - playlist_id - the id of the playlist
297 /// - track_ids - a list of track URIs, URLs or IDs
298 /// - position - the position to add the items, a zero-based index
299 ///
300 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/add-tracks-to-playlist)
301 async fn playlist_add_items<'a>(
302 &self,
303 playlist_id: PlaylistId<'_>,
304 items: impl IntoIterator<Item = PlayableId<'a>> + Send + 'a,
305 position: Option<u32>,
306 ) -> ClientResult<PlaylistResult> {
307 let uris = items.into_iter().map(|id| id.uri()).collect::<Vec<_>>();
308 let params = JsonBuilder::new()
309 .required("uris", uris)
310 .optional("position", position)
311 .build();
312
313 let url = format!("playlists/{}/tracks", playlist_id.id());
314 let result = self.api_post(&url, ¶ms).await?;
315 convert_result(&result)
316 }
317
318 /// Replace all items in a playlist
319 ///
320 /// Parameters:
321 /// - user - the id of the user
322 /// - playlist_id - the id of the playlist
323 /// - tracks - the list of track ids to add to the playlist
324 ///
325 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/reorder-or-replace-playlists-tracks)
326 async fn playlist_replace_items<'a>(
327 &self,
328 playlist_id: PlaylistId<'_>,
329 items: impl IntoIterator<Item = PlayableId<'a>> + Send + 'a,
330 ) -> ClientResult<()> {
331 let uris = items.into_iter().map(|id| id.uri()).collect::<Vec<_>>();
332 let params = JsonBuilder::new().required("uris", uris).build();
333
334 let url = format!("playlists/{}/tracks", playlist_id.id());
335 self.api_put(&url, ¶ms).await?;
336
337 Ok(())
338 }
339
340 /// Reorder items in a playlist.
341 ///
342 /// Parameters:
343 /// - playlist_id - the id of the playlist
344 /// - uris - a list of Spotify URIs to replace or clear
345 /// - range_start - the position of the first track to be reordered
346 /// - insert_before - the position where the tracks should be inserted
347 /// - range_length - optional the number of tracks to be reordered (default:
348 /// 1)
349 /// - snapshot_id - optional playlist's snapshot ID
350 ///
351 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/reorder-or-replace-playlists-tracks)
352 async fn playlist_reorder_items(
353 &self,
354 playlist_id: PlaylistId<'_>,
355 range_start: Option<i32>,
356 insert_before: Option<i32>,
357 range_length: Option<u32>,
358 snapshot_id: Option<&str>,
359 ) -> ClientResult<PlaylistResult> {
360 let params = JsonBuilder::new()
361 .optional("range_start", range_start)
362 .optional("insert_before", insert_before)
363 .optional("range_length", range_length)
364 .optional("snapshot_id", snapshot_id)
365 .build();
366
367 let url = format!("playlists/{}/tracks", playlist_id.id());
368 let result = self.api_put(&url, ¶ms).await?;
369 convert_result(&result)
370 }
371
372 /// Removes all occurrences of the given items from the given playlist.
373 ///
374 /// Parameters:
375 /// - playlist_id - the id of the playlist
376 /// - track_ids - the list of track ids to add to the playlist
377 /// - snapshot_id - optional id of the playlist snapshot
378 ///
379 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/remove-tracks-playlist)
380 async fn playlist_remove_all_occurrences_of_items<'a>(
381 &self,
382 playlist_id: PlaylistId<'_>,
383 track_ids: impl IntoIterator<Item = PlayableId<'a>> + Send + 'a,
384 snapshot_id: Option<&str>,
385 ) -> ClientResult<PlaylistResult> {
386 let tracks = track_ids
387 .into_iter()
388 .map(|id| {
389 let mut map = Map::with_capacity(1);
390 map.insert("uri".to_owned(), id.uri().into());
391 map
392 })
393 .collect::<Vec<_>>();
394
395 let params = JsonBuilder::new()
396 .required("tracks", tracks)
397 .optional("snapshot_id", snapshot_id)
398 .build();
399
400 let url = format!("playlists/{}/tracks", playlist_id.id());
401 let result = self.api_delete(&url, ¶ms).await?;
402 convert_result(&result)
403 }
404
405 /// Removes specfic occurrences of the given items from the given playlist.
406 ///
407 /// Parameters:
408 /// - playlist_id: the id of the playlist
409 /// - tracks: an array of map containing Spotify URIs of the tracks to
410 /// remove with their current positions in the playlist. For example:
411 ///
412 /// ```json
413 /// {
414 /// "tracks":[
415 /// {
416 /// "uri":"spotify:track:4iV5W9uYEdYUVa79Axb7Rh",
417 /// "positions":[
418 /// 0,
419 /// 3
420 /// ]
421 /// },
422 /// {
423 /// "uri":"spotify:track:1301WleyT98MSxVHPZCA6M",
424 /// "positions":[
425 /// 7
426 /// ]
427 /// }
428 /// ]
429 /// }
430 /// ```
431 /// - snapshot_id: optional id of the playlist snapshot
432 ///
433 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/remove-tracks-playlist)
434 async fn playlist_remove_specific_occurrences_of_items<'a>(
435 &self,
436 playlist_id: PlaylistId<'_>,
437 items: impl IntoIterator<Item = ItemPositions<'a>> + Send + 'a,
438 snapshot_id: Option<&str>,
439 ) -> ClientResult<PlaylistResult> {
440 let tracks = items
441 .into_iter()
442 .map(|track| {
443 let mut map = Map::new();
444 map.insert("uri".to_owned(), track.id.uri().into());
445 map.insert("positions".to_owned(), json!(track.positions));
446 map
447 })
448 .collect::<Vec<_>>();
449
450 let params = JsonBuilder::new()
451 .required("tracks", tracks)
452 .optional("snapshot_id", snapshot_id)
453 .build();
454
455 let url = format!("playlists/{}/tracks", playlist_id.id());
456 let result = self.api_delete(&url, ¶ms).await?;
457 convert_result(&result)
458 }
459
460 /// Add the current authenticated user as a follower of a playlist.
461 ///
462 /// Parameters:
463 /// - playlist_id - the id of the playlist
464 ///
465 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/follow-playlist)
466 async fn playlist_follow(
467 &self,
468 playlist_id: PlaylistId<'_>,
469 public: Option<bool>,
470 ) -> ClientResult<()> {
471 let url = format!("playlists/{}/followers", playlist_id.id());
472
473 let params = JsonBuilder::new().optional("public", public).build();
474
475 self.api_put(&url, ¶ms).await?;
476
477 Ok(())
478 }
479
480 /// Get detailed profile information about the current user.
481 /// An alias for the 'current_user' method.
482 ///
483 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-current-users-profile)
484 async fn me(&self) -> ClientResult<PrivateUser> {
485 let result = self.api_get("me/", &Query::new()).await?;
486 convert_result(&result)
487 }
488
489 /// Get detailed profile information about the current user.
490 /// An alias for the 'me' method.
491 ///
492 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-current-users-profile)
493 async fn current_user(&self) -> ClientResult<PrivateUser> {
494 self.me().await
495 }
496
497 /// Get information about the current users currently playing item.
498 ///
499 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-the-users-currently-playing-track)
500 async fn current_user_playing_item(&self) -> ClientResult<Option<CurrentlyPlayingContext>> {
501 let result = self
502 .api_get("me/player/currently-playing", &Query::new())
503 .await?;
504 if result.is_empty() {
505 Ok(None)
506 } else {
507 convert_result(&result)
508 }
509 }
510
511 /// Gets a list of the albums saved in the current authorized user's
512 /// "Your Music" library
513 ///
514 /// Parameters:
515 /// - limit - the number of albums to return
516 /// - offset - the index of the first album to return
517 /// - market - Provide this parameter if you want to apply Track Relinking.
518 ///
519 /// See [`Self::current_user_saved_albums`] for a manually paginated version
520 /// of this.
521 ///
522 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-users-saved-albums)
523 fn current_user_saved_albums(
524 &self,
525 market: Option<Market>,
526 ) -> Paginator<'_, ClientResult<SavedAlbum>> {
527 paginate(
528 move |limit, offset| {
529 self.current_user_saved_albums_manual(market, Some(limit), Some(offset))
530 },
531 self.get_config().pagination_chunks,
532 )
533 }
534
535 /// The manually paginated version of [`Self::current_user_saved_albums`].
536 async fn current_user_saved_albums_manual(
537 &self,
538 market: Option<Market>,
539 limit: Option<u32>,
540 offset: Option<u32>,
541 ) -> ClientResult<Page<SavedAlbum>> {
542 let limit = limit.map(|s| s.to_string());
543 let offset = offset.map(|s| s.to_string());
544 let params = build_map([
545 ("market", market.map(Into::into)),
546 ("limit", limit.as_deref()),
547 ("offset", offset.as_deref()),
548 ]);
549
550 let result = self.api_get("me/albums", ¶ms).await?;
551 convert_result(&result)
552 }
553
554 /// Get a list of the songs saved in the current Spotify user's "Your Music"
555 /// library.
556 ///
557 /// Parameters:
558 /// - limit - the number of tracks to return
559 /// - offset - the index of the first track to return
560 /// - market - Provide this parameter if you want to apply Track Relinking.
561 ///
562 /// See [`Self::current_user_saved_tracks_manual`] for a manually paginated
563 /// version of this.
564 ///
565 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-users-saved-tracks)
566 fn current_user_saved_tracks(
567 &self,
568 market: Option<Market>,
569 ) -> Paginator<'_, ClientResult<SavedTrack>> {
570 paginate(
571 move |limit, offset| {
572 self.current_user_saved_tracks_manual(market, Some(limit), Some(offset))
573 },
574 self.get_config().pagination_chunks,
575 )
576 }
577
578 /// The manually paginated version of [`Self::current_user_saved_tracks`].
579 async fn current_user_saved_tracks_manual(
580 &self,
581 market: Option<Market>,
582 limit: Option<u32>,
583 offset: Option<u32>,
584 ) -> ClientResult<Page<SavedTrack>> {
585 let limit = limit.map(|s| s.to_string());
586 let offset = offset.map(|s| s.to_string());
587 let params = build_map([
588 ("market", market.map(Into::into)),
589 ("limit", limit.as_deref()),
590 ("offset", offset.as_deref()),
591 ]);
592
593 let result = self.api_get("me/tracks", ¶ms).await?;
594 convert_result(&result)
595 }
596
597 /// Gets a list of the artists followed by the current authorized user.
598 ///
599 /// Parameters:
600 /// - after - the last artist ID retrieved from the previous request
601 /// - limit - the number of tracks to return
602 ///
603 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-followed)
604 async fn current_user_followed_artists(
605 &self,
606 after: Option<&str>,
607 limit: Option<u32>,
608 ) -> ClientResult<CursorBasedPage<FullArtist>> {
609 let limit = limit.map(|s| s.to_string());
610 let params = build_map([
611 ("type", Some(Type::Artist.into())),
612 ("after", after),
613 ("limit", limit.as_deref()),
614 ]);
615
616 let result = self.api_get("me/following", ¶ms).await?;
617 convert_result::<CursorPageFullArtists>(&result).map(|x| x.artists)
618 }
619
620 /// Remove one or more tracks from the current user's "Your Music" library.
621 ///
622 /// Parameters:
623 /// - track_ids - a list of track URIs, URLs or IDs
624 ///
625 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/remove-tracks-user)
626 async fn current_user_saved_tracks_delete<'a>(
627 &self,
628 track_ids: impl IntoIterator<Item = TrackId<'a>> + Send + 'a,
629 ) -> ClientResult<()> {
630 let url = format!("me/tracks/?ids={}", join_ids(track_ids));
631 self.api_delete(&url, &json!({})).await?;
632
633 Ok(())
634 }
635
636 /// Check if one or more tracks is already saved in the current Spotify
637 /// user’s "Your Music" library.
638 ///
639 /// Parameters:
640 /// - track_ids - a list of track URIs, URLs or IDs
641 ///
642 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/check-users-saved-tracks)
643 async fn current_user_saved_tracks_contains<'a>(
644 &self,
645 track_ids: impl IntoIterator<Item = TrackId<'a>> + Send + 'a,
646 ) -> ClientResult<Vec<bool>> {
647 let url = format!("me/tracks/contains/?ids={}", join_ids(track_ids));
648 let result = self.api_get(&url, &Query::new()).await?;
649 convert_result(&result)
650 }
651
652 /// Save one or more tracks to the current user's "Your Music" library.
653 ///
654 /// Parameters:
655 /// - track_ids - a list of track URIs, URLs or IDs
656 ///
657 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/save-tracks-user)
658 async fn current_user_saved_tracks_add<'a>(
659 &self,
660 track_ids: impl IntoIterator<Item = TrackId<'a>> + Send + 'a,
661 ) -> ClientResult<()> {
662 let url = format!("me/tracks/?ids={}", join_ids(track_ids));
663 self.api_put(&url, &json!({})).await?;
664
665 Ok(())
666 }
667
668 /// Get the current user's top artists.
669 ///
670 /// Parameters:
671 /// - limit - the number of entities to return
672 /// - offset - the index of the first entity to return
673 /// - time_range - Over what time frame are the affinities computed
674 ///
675 /// See [`Self::current_user_top_artists_manual`] for a manually paginated
676 /// version of this.
677 ///
678 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-users-top-artists-and-tracks)
679 fn current_user_top_artists(
680 &self,
681 time_range: Option<TimeRange>,
682 ) -> Paginator<'_, ClientResult<FullArtist>> {
683 paginate(
684 move |limit, offset| {
685 self.current_user_top_artists_manual(time_range, Some(limit), Some(offset))
686 },
687 self.get_config().pagination_chunks,
688 )
689 }
690
691 /// The manually paginated version of [`Self::current_user_top_artists`].
692 async fn current_user_top_artists_manual(
693 &self,
694 time_range: Option<TimeRange>,
695 limit: Option<u32>,
696 offset: Option<u32>,
697 ) -> ClientResult<Page<FullArtist>> {
698 let limit = limit.map(|s| s.to_string());
699 let offset = offset.map(|s| s.to_string());
700 let params = build_map([
701 ("time_range", time_range.map(Into::into)),
702 ("limit", limit.as_deref()),
703 ("offset", offset.as_deref()),
704 ]);
705
706 let result = self.api_get("me/top/artists", ¶ms).await?;
707 convert_result(&result)
708 }
709
710 /// Get the current user's top tracks.
711 ///
712 /// Parameters:
713 /// - limit - the number of entities to return
714 /// - offset - the index of the first entity to return
715 /// - time_range - Over what time frame are the affinities computed
716 ///
717 /// See [`Self::current_user_top_tracks_manual`] for a manually paginated
718 /// version of this.
719 ///
720 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-users-top-artists-and-tracks)
721 fn current_user_top_tracks(
722 &self,
723 time_range: Option<TimeRange>,
724 ) -> Paginator<'_, ClientResult<FullTrack>> {
725 paginate(
726 move |limit, offset| {
727 self.current_user_top_tracks_manual(time_range, Some(limit), Some(offset))
728 },
729 self.get_config().pagination_chunks,
730 )
731 }
732
733 /// The manually paginated version of [`Self::current_user_top_tracks`].
734 async fn current_user_top_tracks_manual(
735 &self,
736 time_range: Option<TimeRange>,
737 limit: Option<u32>,
738 offset: Option<u32>,
739 ) -> ClientResult<Page<FullTrack>> {
740 let limit = limit.map(|x| x.to_string());
741 let offset = offset.map(|x| x.to_string());
742 let params = build_map([
743 ("time_range", time_range.map(Into::into)),
744 ("limit", limit.as_deref()),
745 ("offset", offset.as_deref()),
746 ]);
747
748 let result = self.api_get("me/top/tracks", ¶ms).await?;
749 convert_result(&result)
750 }
751
752 /// Get the current user's recently played tracks.
753 ///
754 /// Parameters:
755 /// - limit - the number of entities to return
756 /// - time_limit - a timestamp. The endpoint will return all items after
757 /// or before (but not including) this cursor position.
758 ///
759 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-recently-played)
760 async fn current_user_recently_played(
761 &self,
762 limit: Option<u32>,
763 time_limit: Option<TimeLimits>,
764 ) -> ClientResult<CursorBasedPage<PlayHistory>> {
765 let limit = limit.map(|x| x.to_string());
766 let mut params = build_map([("limit", limit.as_deref())]);
767
768 let time_limit = match time_limit {
769 Some(TimeLimits::Before(y)) => Some(("before", y.timestamp_millis().to_string())),
770 Some(TimeLimits::After(y)) => Some(("after", y.timestamp_millis().to_string())),
771 None => None,
772 };
773 if let Some((name, value)) = time_limit.as_ref() {
774 params.insert(name, value);
775 }
776
777 let result = self.api_get("me/player/recently-played", ¶ms).await?;
778 convert_result(&result)
779 }
780
781 /// Add one or more albums to the current user's "Your Music" library.
782 ///
783 /// Parameters:
784 /// - album_ids - a list of album URIs, URLs or IDs
785 ///
786 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/save-albums-user)
787 async fn current_user_saved_albums_add<'a>(
788 &self,
789 album_ids: impl IntoIterator<Item = AlbumId<'a>> + Send + 'a,
790 ) -> ClientResult<()> {
791 let url = format!("me/albums/?ids={}", join_ids(album_ids));
792 self.api_put(&url, &json!({})).await?;
793
794 Ok(())
795 }
796
797 /// Remove one or more albums from the current user's "Your Music" library.
798 ///
799 /// Parameters:
800 /// - album_ids - a list of album URIs, URLs or IDs
801 ///
802 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/remove-albums-user)
803 async fn current_user_saved_albums_delete<'a>(
804 &self,
805 album_ids: impl IntoIterator<Item = AlbumId<'a>> + Send + 'a,
806 ) -> ClientResult<()> {
807 let url = format!("me/albums/?ids={}", join_ids(album_ids));
808 self.api_delete(&url, &json!({})).await?;
809
810 Ok(())
811 }
812
813 /// Check if one or more albums is already saved in the current Spotify
814 /// user’s "Your Music” library.
815 ///
816 /// Parameters:
817 /// - album_ids - a list of album URIs, URLs or IDs
818 ///
819 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/check-users-saved-albums)
820 async fn current_user_saved_albums_contains<'a>(
821 &self,
822 album_ids: impl IntoIterator<Item = AlbumId<'a>> + Send + 'a,
823 ) -> ClientResult<Vec<bool>> {
824 let url = format!("me/albums/contains/?ids={}", join_ids(album_ids));
825 let result = self.api_get(&url, &Query::new()).await?;
826 convert_result(&result)
827 }
828
829 /// Follow one or more artists.
830 ///
831 /// Parameters:
832 /// - artist_ids - a list of artist IDs
833 ///
834 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/follow-artists-users)
835 async fn user_follow_artists<'a>(
836 &self,
837 artist_ids: impl IntoIterator<Item = ArtistId<'a>> + Send + 'a,
838 ) -> ClientResult<()> {
839 let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids));
840 self.api_put(&url, &json!({})).await?;
841
842 Ok(())
843 }
844
845 /// Unfollow one or more artists.
846 ///
847 /// Parameters:
848 /// - artist_ids - a list of artist IDs
849 ///
850 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/unfollow-artists-users)
851 async fn user_unfollow_artists<'a>(
852 &self,
853 artist_ids: impl IntoIterator<Item = ArtistId<'a>> + Send + 'a,
854 ) -> ClientResult<()> {
855 let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids));
856 self.api_delete(&url, &json!({})).await?;
857
858 Ok(())
859 }
860
861 /// Check to see if the current user is following one or more artists or
862 /// other Spotify users.
863 ///
864 /// Parameters:
865 /// - artist_ids - the ids of the users that you want to
866 ///
867 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/check-current-user-follows)
868 async fn user_artist_check_follow<'a>(
869 &self,
870 artist_ids: impl IntoIterator<Item = ArtistId<'a>> + Send + 'a,
871 ) -> ClientResult<Vec<bool>> {
872 let url = format!(
873 "me/following/contains?type=artist&ids={}",
874 join_ids(artist_ids)
875 );
876 let result = self.api_get(&url, &Query::new()).await?;
877 convert_result(&result)
878 }
879
880 /// Follow one or more users.
881 ///
882 /// Parameters:
883 /// - user_ids - a list of artist IDs
884 ///
885 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/follow-artists-users)
886 async fn user_follow_users<'a>(
887 &self,
888 user_ids: impl IntoIterator<Item = UserId<'a>> + Send + 'a,
889 ) -> ClientResult<()> {
890 let url = format!("me/following?type=user&ids={}", join_ids(user_ids));
891 self.api_put(&url, &json!({})).await?;
892
893 Ok(())
894 }
895
896 /// Unfollow one or more users.
897 ///
898 /// Parameters:
899 /// - user_ids - a list of artist IDs
900 ///
901 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/unfollow-artists-users)
902 async fn user_unfollow_users<'a>(
903 &self,
904 user_ids: impl IntoIterator<Item = UserId<'a>> + Send + 'a,
905 ) -> ClientResult<()> {
906 let url = format!("me/following?type=user&ids={}", join_ids(user_ids));
907 self.api_delete(&url, &json!({})).await?;
908
909 Ok(())
910 }
911
912 /// Get a User’s Available Devices
913 ///
914 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-a-users-available-devices)
915 async fn device(&self) -> ClientResult<Vec<Device>> {
916 let result = self.api_get("me/player/devices", &Query::new()).await?;
917 convert_result::<DevicePayload>(&result).map(|x| x.devices)
918 }
919
920 /// Get Information About The User’s Current Playback
921 ///
922 /// Parameters:
923 /// - market: Optional. an ISO 3166-1 alpha-2 country code or the string from_token.
924 /// - additional_types: Optional. A list of item types that your client
925 /// supports besides the default track type. Valid types are: `track` and
926 /// `episode`.
927 ///
928 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-information-about-the-users-current-playback)
929 async fn current_playback<'a>(
930 &self,
931 country: Option<Market>,
932 additional_types: Option<impl IntoIterator<Item = &'a AdditionalType> + Send + 'a>,
933 ) -> ClientResult<Option<CurrentPlaybackContext>> {
934 let additional_types = additional_types.map(|x| {
935 x.into_iter()
936 .map(Into::into)
937 .collect::<Vec<&'static str>>()
938 .join(",")
939 });
940 let params = build_map([
941 ("country", country.map(Into::into)),
942 ("additional_types", additional_types.as_deref()),
943 ]);
944
945 let result = self.api_get("me/player", ¶ms).await?;
946 if result.is_empty() {
947 Ok(None)
948 } else {
949 convert_result(&result)
950 }
951 }
952
953 /// Get the User’s Currently Playing Track
954 ///
955 /// Parameters:
956 /// - market: Optional. an ISO 3166-1 alpha-2 country code or the string from_token.
957 /// - additional_types: Optional. A comma-separated list of item types that
958 /// your client supports besides the default track type. Valid types are:
959 /// `track` and `episode`.
960 ///
961 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/get-the-users-currently-playing-track)
962 async fn current_playing<'a>(
963 &'a self,
964 market: Option<Market>,
965 additional_types: Option<impl IntoIterator<Item = &'a AdditionalType> + Send + 'a>,
966 ) -> ClientResult<Option<CurrentlyPlayingContext>> {
967 let additional_types = additional_types.map(|x| {
968 x.into_iter()
969 .map(Into::into)
970 .collect::<Vec<&'static str>>()
971 .join(",")
972 });
973 let params = build_map([
974 ("market", market.map(Into::into)),
975 ("additional_types", additional_types.as_deref()),
976 ]);
977
978 let result = self.api_get("me/player/currently-playing", ¶ms).await?;
979 if result.is_empty() {
980 Ok(None)
981 } else {
982 convert_result(&result)
983 }
984 }
985
986 /// Get the Current User’s Queue
987 ///
988 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-queue)
989 async fn current_user_queue(&self) -> ClientResult<CurrentUserQueue> {
990 let params = build_map([]);
991 let result = self.api_get("me/player/queue", ¶ms).await?;
992 convert_result(&result)
993 }
994
995 /// Transfer a User’s Playback.
996 ///
997 /// Note: Although an array is accepted, only a single device_id is
998 /// currently supported. Supplying more than one will return 400 Bad Request
999 ///
1000 /// Parameters:
1001 /// - device_id - transfer playback to this device
1002 /// - force_play - true: after transfer, play. false: keep current state.
1003 ///
1004 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/transfer-a-users-playback)
1005 async fn transfer_playback(&self, device_id: &str, play: Option<bool>) -> ClientResult<()> {
1006 let params = JsonBuilder::new()
1007 .required("device_ids", [device_id])
1008 .optional("play", play)
1009 .build();
1010
1011 self.api_put("me/player", ¶ms).await?;
1012 Ok(())
1013 }
1014
1015 /// Start/Resume a User’s Playback.
1016 ///
1017 /// Provide a `context_uri` to start playback or a album, artist, or
1018 /// playlist. Provide a `uris` list to start playback of one or more tracks.
1019 /// Provide `offset` as `{"position": <int>}` or `{"uri": "<track uri>"}` to
1020 /// start playback at a particular offset.
1021 ///
1022 /// Parameters:
1023 /// - device_id - device target for playback
1024 /// - context_uri - spotify context uri to play
1025 /// - uris - spotify track uris
1026 /// - offset - offset into context by index or track
1027 /// - position - Indicates from what position to start playback.
1028 ///
1029 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/start-a-users-playback)
1030 async fn start_context_playback(
1031 &self,
1032 context_uri: PlayContextId<'_>,
1033 device_id: Option<&str>,
1034 offset: Option<Offset>,
1035 position: Option<chrono::Duration>,
1036 ) -> ClientResult<()> {
1037 let params = JsonBuilder::new()
1038 .required("context_uri", context_uri.uri())
1039 .optional(
1040 "offset",
1041 offset.map(|x| match x {
1042 Offset::Position(position) => {
1043 json!({ "position": position.num_milliseconds() })
1044 }
1045 Offset::Uri(uri) => json!({ "uri": uri }),
1046 }),
1047 )
1048 .optional("position_ms", position.map(|p| p.num_milliseconds()))
1049 .build();
1050
1051 let url = append_device_id("me/player/play", device_id);
1052 self.api_put(&url, ¶ms).await?;
1053
1054 Ok(())
1055 }
1056
1057 /// Start a user's playback
1058 ///
1059 /// Parameters:
1060 /// - uris
1061 /// - device_id
1062 /// - offset
1063 /// - position
1064 ///
1065 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/start-a-users-playback)
1066 async fn start_uris_playback<'a>(
1067 &self,
1068 uris: impl IntoIterator<Item = PlayableId<'a>> + Send + 'a,
1069 device_id: Option<&str>,
1070 offset: Option<crate::model::Offset>,
1071 position: Option<chrono::Duration>,
1072 ) -> ClientResult<()> {
1073 let params = JsonBuilder::new()
1074 .required(
1075 "uris",
1076 uris.into_iter().map(|id| id.uri()).collect::<Vec<_>>(),
1077 )
1078 .optional("position_ms", position.map(|p| p.num_milliseconds()))
1079 .optional(
1080 "offset",
1081 offset.map(|x| match x {
1082 Offset::Position(position) => {
1083 json!({ "position": position.num_milliseconds() })
1084 }
1085 Offset::Uri(uri) => json!({ "uri": uri }),
1086 }),
1087 )
1088 .build();
1089
1090 let url = append_device_id("me/player/play", device_id);
1091 self.api_put(&url, ¶ms).await?;
1092
1093 Ok(())
1094 }
1095
1096 /// Pause a User’s Playback.
1097 ///
1098 /// Parameters:
1099 /// - device_id - device target for playback
1100 ///
1101 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/pause-a-users-playback)
1102 async fn pause_playback(&self, device_id: Option<&str>) -> ClientResult<()> {
1103 let url = append_device_id("me/player/pause", device_id);
1104 self.api_put(&url, &json!({})).await?;
1105
1106 Ok(())
1107 }
1108
1109 /// Resume a User’s Playback.
1110 ///
1111 /// Parameters:
1112 /// - device_id - device target for playback
1113 /// - position
1114 ///
1115 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/start-a-users-playback)
1116 async fn resume_playback(
1117 &self,
1118 device_id: Option<&str>,
1119 position: Option<chrono::Duration>,
1120 ) -> ClientResult<()> {
1121 let params = JsonBuilder::new()
1122 .optional("position_ms", position.map(|p| p.num_milliseconds()))
1123 .build();
1124
1125 let url = append_device_id("me/player/play", device_id);
1126 self.api_put(&url, ¶ms).await?;
1127
1128 Ok(())
1129 }
1130
1131 /// Skip User’s Playback To Next Track.
1132 ///
1133 /// Parameters:
1134 /// - device_id - device target for playback
1135 ///
1136 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/skip-users-playback-to-next-track)
1137 async fn next_track(&self, device_id: Option<&str>) -> ClientResult<()> {
1138 let url = append_device_id("me/player/next", device_id);
1139 self.api_post(&url, &json!({})).await?;
1140
1141 Ok(())
1142 }
1143
1144 /// Skip User’s Playback To Previous Track.
1145 ///
1146 /// Parameters:
1147 /// - device_id - device target for playback
1148 ///
1149 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/skip-users-playback-to-previous-track)
1150 async fn previous_track(&self, device_id: Option<&str>) -> ClientResult<()> {
1151 let url = append_device_id("me/player/previous", device_id);
1152 self.api_post(&url, &json!({})).await?;
1153
1154 Ok(())
1155 }
1156
1157 /// Seek To Position In Currently Playing Track.
1158 ///
1159 /// Parameters:
1160 /// - position - position in milliseconds to seek to
1161 /// - device_id - device target for playback
1162 ///
1163 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/seek-to-position-in-currently-playing-track)
1164 async fn seek_track(
1165 &self,
1166 position: chrono::Duration,
1167 device_id: Option<&str>,
1168 ) -> ClientResult<()> {
1169 let url = append_device_id(
1170 &format!("me/player/seek?position_ms={}", position.num_milliseconds()),
1171 device_id,
1172 );
1173 self.api_put(&url, &json!({})).await?;
1174
1175 Ok(())
1176 }
1177
1178 /// Set Repeat Mode On User’s Playback.
1179 ///
1180 /// Parameters:
1181 /// - state - `track`, `context`, or `off`
1182 /// - device_id - device target for playback
1183 ///
1184 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/set-repeat-mode-on-users-playback)
1185 async fn repeat(&self, state: RepeatState, device_id: Option<&str>) -> ClientResult<()> {
1186 let url = append_device_id(
1187 &format!("me/player/repeat?state={}", <&str>::from(state)),
1188 device_id,
1189 );
1190 self.api_put(&url, &json!({})).await?;
1191
1192 Ok(())
1193 }
1194
1195 /// Set Volume For User’s Playback.
1196 ///
1197 /// Parameters:
1198 /// - volume_percent - volume between 0 and 100
1199 /// - device_id - device target for playback
1200 ///
1201 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/set-volume-for-users-playback)
1202 async fn volume(&self, volume_percent: u8, device_id: Option<&str>) -> ClientResult<()> {
1203 debug_assert!(
1204 volume_percent <= 100u8,
1205 "volume must be between 0 and 100, inclusive"
1206 );
1207 let url = append_device_id(
1208 &format!("me/player/volume?volume_percent={volume_percent}"),
1209 device_id,
1210 );
1211 self.api_put(&url, &json!({})).await?;
1212
1213 Ok(())
1214 }
1215
1216 /// Toggle Shuffle For User’s Playback.
1217 ///
1218 /// Parameters:
1219 /// - state - true or false
1220 /// - device_id - device target for playback
1221 ///
1222 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/toggle-shuffle-for-users-playback)
1223 async fn shuffle(&self, state: bool, device_id: Option<&str>) -> ClientResult<()> {
1224 let url = append_device_id(&format!("me/player/shuffle?state={state}"), device_id);
1225 self.api_put(&url, &json!({})).await?;
1226
1227 Ok(())
1228 }
1229
1230 /// Add an item to the end of the user's playback queue.
1231 ///
1232 /// Parameters:
1233 /// - uri - The uri of the item to add, Track or Episode
1234 /// - device id - The id of the device targeting
1235 /// - If no device ID provided the user's currently active device is
1236 /// targeted
1237 ///
1238 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/add-to-queue)
1239 async fn add_item_to_queue(
1240 &self,
1241 item: PlayableId<'_>,
1242 device_id: Option<&str>,
1243 ) -> ClientResult<()> {
1244 let url = append_device_id(&format!("me/player/queue?uri={}", item.uri()), device_id);
1245 self.api_post(&url, &json!({})).await?;
1246
1247 Ok(())
1248 }
1249
1250 /// Add a show or a list of shows to a user’s library.
1251 ///
1252 /// Parameters:
1253 /// - ids(Required) A comma-separated list of Spotify IDs for the shows to
1254 /// be added to the user’s library.
1255 ///
1256 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/save-shows-user)
1257 async fn save_shows<'a>(
1258 &self,
1259 show_ids: impl IntoIterator<Item = ShowId<'a>> + Send + 'a,
1260 ) -> ClientResult<()> {
1261 let url = format!("me/shows/?ids={}", join_ids(show_ids));
1262 self.api_put(&url, &json!({})).await?;
1263
1264 Ok(())
1265 }
1266
1267 /// Get a list of shows saved in the current Spotify user’s library.
1268 /// Optional parameters can be used to limit the number of shows returned.
1269 ///
1270 /// Parameters:
1271 /// - limit(Optional). The maximum number of shows to return. Default: 20.
1272 /// Minimum: 1. Maximum: 50.
1273 /// - offset(Optional). The index of the first show to return. Default: 0
1274 /// (the first object). Use with limit to get the next set of shows.
1275 ///
1276 /// See [`Self::get_saved_show_manual`] for a manually paginated version of
1277 /// this.
1278 ///
1279 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/get-users-saved-shows)
1280 fn get_saved_show(&self) -> Paginator<'_, ClientResult<Show>> {
1281 paginate(
1282 move |limit, offset| self.get_saved_show_manual(Some(limit), Some(offset)),
1283 self.get_config().pagination_chunks,
1284 )
1285 }
1286
1287 /// The manually paginated version of [`Self::get_saved_show`].
1288 async fn get_saved_show_manual(
1289 &self,
1290 limit: Option<u32>,
1291 offset: Option<u32>,
1292 ) -> ClientResult<Page<Show>> {
1293 let limit = limit.map(|x| x.to_string());
1294 let offset = offset.map(|x| x.to_string());
1295 let params = build_map([("limit", limit.as_deref()), ("offset", offset.as_deref())]);
1296
1297 let result = self.api_get("me/shows", ¶ms).await?;
1298 convert_result(&result)
1299 }
1300
1301 /// Check if one or more shows is already saved in the current Spotify user’s library.
1302 ///
1303 /// Query Parameters
1304 /// - ids: Required. A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs.
1305 ///
1306 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/check-users-saved-shows)
1307 async fn check_users_saved_shows<'a>(
1308 &self,
1309 ids: impl IntoIterator<Item = ShowId<'a>> + Send + 'a,
1310 ) -> ClientResult<Vec<bool>> {
1311 let ids = join_ids(ids);
1312 let params = build_map([("ids", Some(&ids))]);
1313 let result = self.api_get("me/shows/contains", ¶ms).await?;
1314 convert_result(&result)
1315 }
1316
1317 /// Delete one or more shows from current Spotify user's library.
1318 /// Changes to a user's saved shows may not be visible in other Spotify applications immediately.
1319 ///
1320 /// Query Parameters
1321 /// - ids: Required. A comma-separated list of Spotify IDs for the shows to be deleted from the user’s library.
1322 /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token.
1323 ///
1324 /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#/operations/remove-shows-user)
1325 async fn remove_users_saved_shows<'a>(
1326 &self,
1327 show_ids: impl IntoIterator<Item = ShowId<'a>> + Send + 'a,
1328 country: Option<Market>,
1329 ) -> ClientResult<()> {
1330 let url = format!("me/shows?ids={}", join_ids(show_ids));
1331 let params = JsonBuilder::new()
1332 .optional("country", country.map(<&str>::from))
1333 .build();
1334 self.api_delete(&url, ¶ms).await?;
1335
1336 Ok(())
1337 }
1338}