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
23pub 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 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 #[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 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 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 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 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 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 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 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 pub async fn current_user_playlists(&self) -> Result<Vec<SimplifiedPlaylist>> {
123 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 self.all_paging_items(first_page, &Query::new()).await
136 }
137
138 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 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 Ok(artists)
160 }
161
162 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 Ok(albums.into_iter().map(|a| a.album.into()).collect())
172 }
173
174 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 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 pub async fn radio_tracks(&self, seed_uri: String) -> Result<Vec<Track>> {
214 let session = self.session().await;
215
216 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 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 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 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 #[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 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 pub async fn add_track_to_playlist(
331 &self,
332 playlist_id: PlaylistId<'_>,
333 track_id: TrackId<'_>,
334 ) -> Result<()> {
335 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 delete_track_from_playlist(
362 &self,
363 playlist_id: PlaylistId<'_>,
364 track_id: TrackId<'_>,
365 ) -> Result<()> {
366 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 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 async fn add_to_library(&self, item: Item) -> Result<()> {
405 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 }
449 }
450 Ok(())
451 }
452
453 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 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 let playlist = self
485 .http_get::<FullPlaylist>(
486 &format!("{SPOTIFY_API_ENDPOINT}/playlists/{}", playlist_id.id()),
487 &market_query(),
488 )
489 .await?;
490
491 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 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 let album: Album = album.into();
521
522 let tracks = self
524 .all_paging_items(first_page, &Query::new())
525 .await?
526 .into_iter()
527 .filter_map(|t| {
528 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 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 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 async fn http_get<T>(&self, url: &str, payload: &Query<'_>) -> Result<T>
576 where
577 T: serde::de::DeserializeOwned,
578 {
579 fn process_spotify_api_response(text: String) -> String {
584 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 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 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 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 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 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}