spotify_client/client/
mod.rs

1use std::ops::Deref;
2use std::sync::Arc;
3
4use crate::auth::AuthConfig;
5use crate::constant::*;
6
7use anyhow::Result;
8use librespot_core::session::Session;
9use rspotify::{
10    http::Query,
11    model::{
12        FullPlaylist, FullAlbum, FullArtist, FullTrack,
13        Market, Page, SimplifiedPlaylist
14    },
15    prelude::*,
16};
17use rspotify_model::SavedTrack;
18use serde::Deserialize;
19
20mod spotify;
21
22
23/// The application's Spotify client
24pub struct Client {
25    http: reqwest::Client,
26    spotify: Arc<spotify::Spotify>,
27    auth_config: AuthConfig,
28}
29
30impl Deref for Client {
31    type Target = spotify::Spotify;
32    fn deref(&self) -> &Self::Target {
33        self.spotify.as_ref()
34    }
35}
36
37fn market_query() -> Query<'static> {
38    Query::from([("market", "from_token")])
39}
40
41impl Client {
42    /// Construct a new client
43    pub fn new(session: Session, auth_config: AuthConfig, client_id: String) -> Self {
44        Self {
45            spotify: Arc::new(spotify::Spotify::new(session, client_id)),
46            http: reqwest::Client::new(),
47            auth_config
48        }
49    }
50
51    /// Create a new client session
52    // unused variables:
53    // - `state` when the `streaming` feature is not enabled
54    #[allow(unused_variables)]
55    async fn new_session(&self) -> Result<()> {
56        let session = crate::auth::new_session(&self.auth_config, false).await?;
57        *self.session.lock().await = Some(session);
58
59        tracing::info!("Used a new session for Spotify client.");
60
61        Ok(())
62    }
63
64    /// Get the UserName of Spotify
65    pub fn username(&self) -> UserId {
66        let name = self.auth_config.login_info.0.to_owned();
67        UserId::from_id(name).unwrap()
68    }
69
70    /// Get Spotify's available browse categories
71    pub async fn browse_categories(&self) -> Result<Vec<Category>> {
72        let first_page = self
73            .categories_manual(Some("EN"), None, Some(50), None)
74            .await?;
75
76        Ok(first_page.items.into_iter().map(Category::from).collect())
77    }
78
79    /// Get Spotify's available browse playlists of a given category
80    pub async fn browse_category_playlists(&self, category_id: &str) -> Result<Vec<Playlist>> {
81        let first_page = self
82            .category_playlists_manual(category_id, None, Some(50), None)
83            .await?;
84
85        Ok(first_page.items.into_iter().map(Playlist::from).collect())
86    }
87
88    /// Get the saved (liked) tracks of the current user
89    pub async fn current_user_saved_tracks(&self) -> Result<Vec<SavedTrack>> {
90        let first_page = self
91            .current_user_saved_tracks_manual(Some(Market::FromToken), Some(50), None)
92            .await?;
93        self.all_paging_items(first_page, &market_query()).await
94    }
95
96    /// Get the recently played tracks of the current user
97    pub async fn current_user_recently_played_tracks(&self) -> Result<Vec<FullTrack>> {
98        let first_page = self.current_user_recently_played(Some(50), None).await?;
99
100        let play_histories = self.all_cursor_based_paging_items(first_page).await?;
101
102        // de-duplicate the tracks returned from the recently-played API
103        let mut tracks = Vec::<FullTrack>::new();
104        for history in play_histories {
105            if !tracks.iter().any(|t| t.name == history.track.name) {
106                tracks.push(history.track);
107            }
108        }
109        Ok(tracks)
110    }
111
112    /// Get the top tracks of the current user
113    pub async fn current_user_top_tracks(&self) -> Result<Vec<FullTrack>> {
114        let first_page = self
115            .current_user_top_tracks_manual(None, Some(50), None)
116            .await?;
117
118        self.all_paging_items(first_page, &Query::new()).await
119    }
120
121    /// Get all playlists of the current user
122    pub async fn current_user_playlists(&self) -> Result<Vec<SimplifiedPlaylist>> {
123        // TODO: this should use `rspotify::current_user_playlists_manual` API instead of `internal_call`
124        // See: https://github.com/ramsayleung/rspotify/issues/459
125        let first_page = self
126            .http_get::<Page<SimplifiedPlaylist>>(
127                &format!("{SPOTIFY_API_ENDPOINT}/me/playlists"),
128                &Query::from([("limit", "50")]),
129            )
130            .await?;
131        // let first_page = self
132        //     .current_user_playlists_manual(Some(50), None)
133        //     .await?;
134
135        self.all_paging_items(first_page, &Query::new()).await
136    }
137
138    /// Get all followed artists of the current user
139    pub async fn current_user_followed_artists(&self) -> Result<Vec<FullArtist>> {
140        let first_page = self
141            .spotify
142            .current_user_followed_artists(None, None)
143            .await?;
144
145        // followed artists pagination is handled different from
146        // other paginations. The endpoint uses cursor-based pagination.
147        let mut artists = first_page.items;
148        let mut maybe_next = first_page.next;
149        while let Some(url) = maybe_next {
150            let mut next_page = self
151                .http_get::<rspotify_model::CursorPageFullArtists>(&url, &Query::new())
152                .await?
153                .artists;
154            artists.append(&mut next_page.items);
155            maybe_next = next_page.next;
156        }
157
158        // converts `rspotify_model::FullArtist` into `state::Artist`
159        Ok(artists)
160    }
161
162    /// Get all saved albums of the current user
163    pub async fn current_user_saved_albums(&self) -> Result<Vec<Album>> {
164        let first_page = self
165            .current_user_saved_albums_manual(Some(Market::FromToken), Some(50), None)
166            .await?;
167
168        let albums = self.all_paging_items(first_page, &Query::new()).await?;
169
170        // converts `rspotify_model::SavedAlbum` into `state::Album`
171        Ok(albums.into_iter().map(|a| a.album.into()).collect())
172    }
173
174    /// Get all albums of an artist
175    pub async fn artist_albums(&self, artist_id: ArtistId<'_>) -> Result<Vec<Album>> {
176        let payload = market_query();
177
178        let mut singles = {
179            let first_page = self
180                .artist_albums_manual(
181                    artist_id.as_ref(),
182                    Some(rspotify_model::AlbumType::Single),
183                    Some(Market::FromToken),
184                    Some(50),
185                    None,
186                )
187                .await?;
188            self.all_paging_items(first_page, &payload).await
189        }?;
190        let mut albums = {
191            let first_page = self
192                .artist_albums_manual(
193                    artist_id.as_ref(),
194                    Some(rspotify_model::AlbumType::Album),
195                    Some(Market::FromToken),
196                    Some(50),
197                    None,
198                )
199                .await?;
200            self.all_paging_items(first_page, &payload).await
201        }?;
202        albums.append(&mut singles);
203
204        // converts `rspotify_model::SimplifiedAlbum` into `state::Album`
205        let albums = albums
206            .into_iter()
207            .filter_map(Album::try_from_simplified_album)
208            .collect();
209        Ok(self.process_artist_albums(albums))
210    }
211
212    /// Get recommendation (radio) tracks based on a seed
213    pub async fn radio_tracks(&self, seed_uri: String) -> Result<Vec<Track>> {
214        let session = self.session().await;
215
216        // Get an autoplay URI from the seed URI.
217        // The return URI is a Spotify station's URI
218        let autoplay_query_url = format!("hm://autoplay-enabled/query?uri={seed_uri}");
219        let response = session
220            .mercury()
221            .get(autoplay_query_url)
222            .await
223            .map_err(|_| anyhow::anyhow!("Failed to get autoplay URI: got a Mercury error"))?;
224        if response.status_code != 200 {
225            anyhow::bail!(
226                "Failed to get autoplay URI: got non-OK status code: {}",
227                response.status_code
228            );
229        }
230        let autoplay_uri = String::from_utf8(response.payload[0].to_vec())?;
231
232        // Retrieve radio's data based on the autoplay URI
233        let radio_query_url = format!("hm://radio-apollo/v3/stations/{autoplay_uri}");
234        let response = session.mercury().get(radio_query_url).await.map_err(|_| {
235            anyhow::anyhow!("Failed to get radio data of {autoplay_uri}: got a Mercury error")
236        })?;
237        if response.status_code != 200 {
238            anyhow::bail!(
239                "Failed to get radio data of {autoplay_uri}: got non-OK status code: {}",
240                response.status_code
241            );
242        }
243
244        #[derive(Debug, Deserialize)]
245        struct TrackData {
246            original_gid: String,
247        }
248        #[derive(Debug, Deserialize)]
249        struct RadioStationResponse {
250            tracks: Vec<TrackData>,
251        }
252        // Parse a list consisting of IDs of tracks inside the radio station
253        let track_ids = serde_json::from_slice::<RadioStationResponse>(&response.payload[0])?
254            .tracks
255            .into_iter()
256            .filter_map(|t| TrackId::from_id(t.original_gid).ok());
257
258        // Retrieve tracks based on IDs
259        let tracks = self.tracks(track_ids, Some(Market::FromToken)).await?;
260        let tracks = tracks
261            .into_iter()
262            .filter_map(Track::try_from_full_track)
263            .collect();
264
265        Ok(tracks)
266    }
267
268    /// Search for items (tracks, artists, albums, playlists) matching a given query
269    #[cfg(feature = "test_mod")]
270    pub async fn search(&self, query: &str) -> Result<SearchResults> {
271        let (track_result, artist_result, album_result, playlist_result) = tokio::try_join!(
272            self.search_specific_type(query, rspotify_model::SearchType::Track),
273            self.search_specific_type(query, rspotify_model::SearchType::Artist),
274            self.search_specific_type(query, rspotify_model::SearchType::Album),
275            self.search_specific_type(query, rspotify_model::SearchType::Playlist)
276        )?;
277
278        let (tracks, artists, albums, playlists) = (
279            match track_result {
280                rspotify_model::SearchResult::Tracks(p) => p
281                    .items
282                    .into_iter()
283                    .filter_map(Track::try_from_full_track)
284                    .collect(),
285                _ => anyhow::bail!("expect a track search result"),
286            },
287            match artist_result {
288                rspotify_model::SearchResult::Artists(p) => {
289                    p.items.into_iter().map(|a| a.into()).collect()
290                }
291                _ => anyhow::bail!("expect an artist search result"),
292            },
293            match album_result {
294                rspotify_model::SearchResult::Albums(p) => p
295                    .items
296                    .into_iter()
297                    .filter_map(Album::try_from_simplified_album)
298                    .collect(),
299                _ => anyhow::bail!("expect an album search result"),
300            },
301            match playlist_result {
302                rspotify_model::SearchResult::Playlists(p) => {
303                    p.items.into_iter().map(|i| i.into()).collect()
304                }
305                _ => anyhow::bail!("expect a playlist search result"),
306            },
307        );
308
309        Ok(SearchResults {
310            tracks,
311            artists,
312            albums,
313            playlists,
314        })
315    }
316
317    /// Search for items of a specific type matching a given query
318    pub async fn search_specific_type(
319        &self,
320        query: &str,
321        _type: rspotify_model::SearchType,
322    ) -> Result<rspotify_model::SearchResult> {
323        Ok(self
324            .spotify
325            .search(query, _type, None, None, None, None)
326            .await?)
327    }
328
329    /// Add a track to a playlist
330    pub async fn add_track_to_playlist(
331        &self,
332        playlist_id: PlaylistId<'_>,
333        track_id: TrackId<'_>,
334    ) -> Result<()> {
335        // remove all the occurrences of the track to ensure no duplication in the playlist
336        self.playlist_remove_all_occurrences_of_items(
337            playlist_id.as_ref(),
338            [PlayableId::Track(track_id.as_ref())],
339            None,
340        )
341        .await?;
342
343        self.playlist_add_items(
344            playlist_id.as_ref(),
345            [PlayableId::Track(track_id.as_ref())],
346            None,
347        )
348        .await?;
349
350        Ok(())
351    }
352
353    // pub async fn add_tracks_to_playlist(
354    //     &self
355    // ) -> Result<()> {
356
357    //     Ok(())
358    // }
359
360    /// Remove a track from a playlist
361    pub async fn delete_track_from_playlist(
362        &self,
363        playlist_id: PlaylistId<'_>,
364        track_id: TrackId<'_>,
365    ) -> Result<()> {
366        // remove all the occurrences of the track to ensure no duplication in the playlist
367        self.playlist_remove_all_occurrences_of_items(
368            playlist_id.as_ref(),
369            [PlayableId::Track(track_id.as_ref())],
370            None,
371        )
372            .await?;
373
374        Ok(())
375    }
376
377    /// Reorder items in a playlist
378    async fn reorder_playlist_items(
379        &self,
380        playlist_id: PlaylistId<'_>,
381        insert_index: usize,
382        range_start: usize,
383        range_length: Option<usize>,
384        snapshot_id: Option<&str>,
385    ) -> Result<()> {
386        let insert_before = match insert_index > range_start {
387            true => insert_index + 1,
388            false => insert_index,
389        };
390
391        self.playlist_reorder_items(
392            playlist_id.clone(),
393            Some(range_start as i32),
394            Some(insert_before as i32),
395            range_length.map(|range_length| range_length as u32),
396            snapshot_id,
397        )
398            .await?;
399
400        Ok(())
401    }
402
403    /// Add a Spotify item to current user's library.
404    async fn add_to_library(&self, item: Item) -> Result<()> {
405        // Before adding new item, checks if that item already exists in the library to avoid adding a duplicated item.
406        match item {
407            Item::Track(track) => {
408                let contains = self
409                    .current_user_saved_tracks_contains([track.id.as_ref()])
410                    .await?;
411                if !contains[0] {
412                    self.current_user_saved_tracks_add([track.id.as_ref()])
413                        .await?;
414                }
415            }
416            Item::Album(album) => {
417                let contains = self
418                    .current_user_saved_albums_contains([album.id.as_ref()])
419                    .await?;
420                if !contains[0] {
421                    self.current_user_saved_albums_add([album.id.as_ref()])
422                        .await?;
423                }
424            }
425            Item::Artist(artist) => {
426                let follows = self.user_artist_check_follow([artist.id.as_ref()]).await?;
427                if !follows[0] {
428                    self.user_follow_artists([artist.id.as_ref()]).await?;
429                }
430            }
431            Item::Playlist(_playlist) => {
432                // let user_id = self.state
433                //     .data
434                //     .read()
435                //     .user_data
436                //     .user
437                //     .as_ref()
438                //     .map(|u| u.id.clone());
439
440                // if let Some(user_id) = user_id {
441                //     let follows = self
442                //         .playlist_check_follow(playlist.id.as_ref(), &[user_id])
443                //         .await?;
444                //     if !follows[0] {
445                //         self.playlist_follow(playlist.id.as_ref(), None).await?;
446                //     }
447                // }
448            }
449        }
450        Ok(())
451    }
452
453    // Delete a Spotify item from user's library
454    async fn delete_from_library(&self, id: ItemId) -> Result<()> {
455        match id {
456            ItemId::Track(id) => self.current_user_saved_tracks_delete([id]).await?,
457            ItemId::Album(id) => self.current_user_saved_albums_delete([id]).await?,
458            ItemId::Artist(id) => self.user_unfollow_artists([id]).await?,
459            ItemId::Playlist(id) => self.playlist_unfollow(id).await?
460        }
461        Ok(())
462    }
463
464    /// Get a track data
465    // pub async fn track(&self, track_id: TrackId<'_>) -> Result<Track> {
466    //     Track::try_from_full_track(
467    //         self.spotify
468    //             .track(track_id, Some(Market::FromToken))
469    //             .await?,
470    //     )
471    //     .context("convert FullTrack into Track")
472    // }
473
474    /// Get a playlist context data
475    pub async fn playlist_context(&self, playlist_id: PlaylistId<'_>) -> Result<Context> {
476        let playlist_uri = playlist_id.uri();
477        tracing::info!("Get playlist context: {}", playlist_uri);
478
479        // TODO: this should use `rspotify::playlist` API instead of `internal_call`
480        // See: https://github.com/ramsayleung/rspotify/issues/459
481        // let playlist = self
482        //     .playlist(playlist_id, None, Some(Market::FromToken))
483        //     .await?;
484        let playlist = self
485            .http_get::<FullPlaylist>(
486                &format!("{SPOTIFY_API_ENDPOINT}/playlists/{}", playlist_id.id()),
487                &market_query(),
488            )
489            .await?;
490
491        // get the playlist's tracks
492        let first_page = playlist.tracks.clone();
493        let tracks = self
494            .all_paging_items(first_page, &market_query())
495            .await?
496            .into_iter()
497            .filter_map(|item| match item.track {
498                Some(rspotify_model::PlayableItem::Track(track)) => {
499                    Track::try_from_full_track(track)
500                }
501                _ => None,
502            })
503            .collect::<Vec<_>>();
504
505        Ok(Context::Playlist {
506            playlist: playlist.into(),
507            tracks,
508        })
509    }
510
511    /// Get an album context data
512    pub async fn album_context(&self, album_id: AlbumId<'_>) -> Result<Context> {
513        let album_uri = album_id.uri();
514        tracing::info!("Get album context: {}", album_uri);
515
516        let album = self.album(album_id, Some(Market::FromToken)).await?;
517        let first_page = album.tracks.clone();
518
519        // converts `rspotify_model::FullAlbum` into `state::Album`
520        let album: Album = album.into();
521
522        // get the album's tracks
523        let tracks = self
524            .all_paging_items(first_page, &Query::new())
525            .await?
526            .into_iter()
527            .filter_map(|t| {
528                // simplified track doesn't have album so
529                // we need to manually include one during
530                // converting into `state::Track`
531                Track::try_from_simplified_track(t).map(|mut t| {
532                    t.album = Some(album.clone());
533                    t
534                })
535            })
536            .collect::<Vec<_>>();
537
538        Ok(Context::Album { album, tracks })
539    }
540
541    /// Get an artist context data
542    pub async fn artist_context(&self, artist_id: ArtistId<'_>) -> Result<Context> {
543        let artist_uri = artist_id.uri();
544        tracing::info!("Get artist context: {}", artist_uri);
545
546        // get the artist's information, including top tracks, related artists, and albums
547
548        let artist = self.artist(artist_id.as_ref()).await?.into();
549
550        let top_tracks = self
551            .artist_top_tracks(artist_id.as_ref(), Some(Market::FromToken))
552            .await?;
553        let top_tracks = top_tracks
554            .into_iter()
555            .filter_map(Track::try_from_full_track)
556            .collect::<Vec<_>>();
557
558        let related_artists = self.artist_related_artists(artist_id.as_ref()).await?;
559        let related_artists = related_artists
560            .into_iter()
561            .map(|a| a.into())
562            .collect::<Vec<_>>();
563
564        let albums = self.artist_albums(artist_id.as_ref()).await?;
565
566        Ok(Context::Artist {
567            artist,
568            top_tracks,
569            albums,
570            related_artists,
571        })
572    }
573
574    /// Make a GET HTTP request to the Spotify server
575    async fn http_get<T>(&self, url: &str, payload: &Query<'_>) -> Result<T>
576    where
577        T: serde::de::DeserializeOwned,
578    {
579        /// a helper function to process an API response from Spotify server
580        ///
581        /// This function is mainly used to patch upstream API bugs , resulting in
582        /// a type error when a third-party library like `rspotify` parses the response
583        fn process_spotify_api_response(text: String) -> String {
584            // See: https://github.com/ramsayleung/rspotify/issues/459
585            text.replace("\"images\":null", "\"images\":[]")
586        }
587
588        let access_token = self.access_token().await?;
589
590        tracing::debug!("{access_token} {url}");
591
592        let response = self
593            .http
594            .get(url)
595            .query(payload)
596            .header(
597                reqwest::header::AUTHORIZATION,
598                format!("Bearer {access_token}"),
599            )
600            .send()
601            .await?;
602
603        let text = process_spotify_api_response(response.text().await?);
604        tracing::debug!("{text}");
605
606        Ok(serde_json::from_str(&text)?)
607    }
608
609    /// Get all paging items starting from a pagination object of the first page
610    async fn all_paging_items<T>(
611        &self,
612        first_page: rspotify_model::Page<T>,
613        payload: &Query<'_>,
614    ) -> Result<Vec<T>>
615    where
616        T: serde::de::DeserializeOwned,
617    {
618        let mut items = first_page.items;
619        let mut maybe_next = first_page.next;
620
621        while let Some(url) = maybe_next {
622            let mut next_page = self
623                .http_get::<rspotify_model::Page<T>>(&url, payload)
624                .await?;
625            items.append(&mut next_page.items);
626            maybe_next = next_page.next;
627        }
628        Ok(items)
629    }
630
631    /// Get all cursor-based paging items starting from a pagination object of the first page
632    async fn all_cursor_based_paging_items<T>(
633        &self,
634        first_page: rspotify_model::CursorBasedPage<T>,
635    ) -> Result<Vec<T>>
636    where
637        T: serde::de::DeserializeOwned,
638    {
639        let mut items = first_page.items;
640        let mut maybe_next = first_page.next;
641        while let Some(url) = maybe_next {
642            let mut next_page = self
643                .http_get::<rspotify_model::CursorBasedPage<T>>(&url, &Query::new())
644                .await?;
645            items.append(&mut next_page.items);
646            maybe_next = next_page.next;
647        }
648        Ok(items)
649    }
650
651    /// Create a new playlist
652    async fn create_new_playlist(
653        &self,
654        user_id: UserId<'static>,
655        playlist_name: &str,
656        public: bool,
657        collab: bool,
658        desc: &str,
659    ) -> Result<()> {
660        let playlist: Playlist = self
661            .user_playlist_create(
662                user_id,
663                playlist_name,
664                Some(public),
665                Some(collab),
666                Some(desc),
667            )
668            .await?
669            .into();
670        tracing::info!(
671            "new playlist (name={},id={}) was successfully created",
672            playlist.name,
673            playlist.id
674        );
675        Ok(())
676    }
677
678
679    /// Process a list of albums, which includes
680    /// - sort albums by the release date
681    /// - remove albums with duplicated names
682    fn process_artist_albums(&self, albums: Vec<Album>) -> Vec<Album> {
683        let mut albums = albums.into_iter().collect::<Vec<_>>();
684
685        albums.sort_by(|x, y| x.release_date.partial_cmp(&y.release_date).unwrap());
686
687        // use a HashSet to keep track albums with the same name
688        let mut seen_names = std::collections::HashSet::new();
689
690        albums.into_iter().rfold(vec![], |mut acc, a| {
691            if !seen_names.contains(&a.name) {
692                seen_names.insert(a.name.clone());
693                acc.push(a);
694            }
695            acc
696        })
697    }
698}