Skip to main content

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", &params).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, &params).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, &params).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, &params).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, &params).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, &params).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, &params).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, &params).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, &params).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", &params).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", &params).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", &params).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", &params).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", &params).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", &params).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", &params).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", &params).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", &params).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", &params).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, &params).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, &params).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, &params).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", &params).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", &params).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, &params).await?;
1335
1336        Ok(())
1337    }
1338}