diff --git a/src/album.rs b/src/album.rs index 4bea787..a07c449 100644 --- a/src/album.rs +++ b/src/album.rs @@ -35,7 +35,7 @@ impl Album { if let Some(ref album_id) = self.id { let mut collected_tracks = Vec::new(); - if let Some(full_album) = spotify.full_album(album_id) { + if let Some(full_album) = spotify.api.full_album(album_id) { let mut tracks_result = Some(full_album.tracks.clone()); while let Some(ref tracks) = tracks_result { for t in &tracks.items { @@ -48,7 +48,7 @@ impl Album { tracks_result = match tracks.next { Some(_) => { debug!("requesting tracks again.."); - spotify.album_tracks( + spotify.api.album_tracks( album_id, 50, tracks.offset + tracks.items.len() as u32, diff --git a/src/artist.rs b/src/artist.rs index 42260ed..466e0b0 100644 --- a/src/artist.rs +++ b/src/artist.rs @@ -34,7 +34,7 @@ impl Artist { fn load_top_tracks(&mut self, spotify: Spotify) { if let Some(artist_id) = &self.id { if self.tracks.is_none() { - self.tracks = spotify.artist_top_tracks(artist_id); + self.tracks = spotify.api.artist_top_tracks(artist_id); } } } diff --git a/src/commands.rs b/src/commands.rs index 4dd6e8c..0a38302 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -213,7 +213,7 @@ impl CommandManager { Ok(None) } Command::NewPlaylist(name) => { - match self.spotify.create_playlist(name, None, None) { + match self.spotify.api.create_playlist(name, None, None) { Some(_) => self.library.update_library(), None => error!("could not create playlist {}", name), } diff --git a/src/library.rs b/src/library.rs index d45f7fa..692e1bc 100644 --- a/src/library.rs +++ b/src/library.rs @@ -42,7 +42,7 @@ pub struct Library { impl Library { pub fn new(ev: &EventManager, spotify: Spotify, cfg: Arc) -> Self { - let current_user = spotify.current_user(); + let current_user = spotify.api.current_user(); let user_id = current_user.as_ref().map(|u| u.id.clone()); let display_name = current_user.as_ref().and_then(|u| u.display_name.clone()); @@ -131,7 +131,7 @@ impl Library { }; if let Some(position) = pos { - if self.spotify.delete_playlist(id) { + if self.spotify.api.delete_playlist(id) { { let mut store = self.playlists.write().expect("can't writelock playlists"); store.remove(position); @@ -143,7 +143,7 @@ impl Library { pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) { debug!("saving {} tracks to list {}", tracks.len(), id); - self.spotify.overwrite_playlist(id, tracks); + self.spotify.api.overwrite_playlist(id, tracks); self.fetch_playlists(); self.save_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone()); @@ -151,7 +151,7 @@ impl Library { pub fn save_playlist(&self, name: &str, tracks: &[Playable]) { debug!("saving {} tracks to new list {}", tracks.len(), name); - match self.spotify.create_playlist(name, None, None) { + match self.spotify.api.create_playlist(name, None, None) { Some(id) => self.overwrite_playlist(&id, tracks), None => error!("could not create new playlist.."), } @@ -231,7 +231,7 @@ impl Library { debug!("loading shows"); let mut saved_shows: Vec = Vec::new(); - let mut shows_result = self.spotify.get_saved_shows(0); + let mut shows_result = self.spotify.api.get_saved_shows(0); while let Some(shows) = shows_result.as_ref() { saved_shows.extend(shows.items.iter().map(|show| (&show.show).into())); @@ -241,6 +241,7 @@ impl Library { Some(_) => { debug!("requesting shows again.."); self.spotify + .api .get_saved_shows(shows.offset + shows.items.len() as u32) } None => None, @@ -255,7 +256,7 @@ impl Library { let mut stale_lists = self.playlists.read().unwrap().clone(); let mut list_order = Vec::new(); - let lists_page = self.spotify.current_user_playlist(); + let lists_page = self.spotify.api.current_user_playlist(); let mut lists_batch = Some(lists_page.items.read().unwrap().clone()); while let Some(lists) = &lists_batch { for (index, remote) in lists.iter().enumerate() { @@ -311,7 +312,7 @@ impl Library { let mut i: u32 = 0; loop { - let page = self.spotify.current_user_followed_artists(last); + let page = self.spotify.api.current_user_followed_artists(last); debug!("artists page: {}", i); i += 1; if page.is_none() { @@ -363,7 +364,10 @@ impl Library { let mut i: u32 = 0; loop { - let page = self.spotify.current_user_saved_albums(albums.len() as u32); + let page = self + .spotify + .api + .current_user_saved_albums(albums.len() as u32); debug!("albums page: {}", i); i += 1; if page.is_none() { @@ -409,7 +413,10 @@ impl Library { let mut i: u32 = 0; loop { - let page = self.spotify.current_user_saved_tracks(tracks.len() as u32); + let page = self + .spotify + .api + .current_user_saved_tracks(tracks.len() as u32); debug!("tracks page: {}", i); i += 1; @@ -539,6 +546,7 @@ impl Library { if api && self .spotify + .api .current_user_saved_tracks_add(tracks.iter().filter_map(|t| t.id.clone()).collect()) .is_none() { @@ -572,6 +580,7 @@ impl Library { if api && self .spotify + .api .current_user_saved_tracks_delete( tracks.iter().filter_map(|t| t.id.clone()).collect(), ) @@ -612,6 +621,7 @@ impl Library { if let Some(ref album_id) = album.id { if self .spotify + .api .current_user_saved_albums_add(vec![album_id.clone()]) .is_none() { @@ -637,6 +647,7 @@ impl Library { if let Some(ref album_id) = album.id { if self .spotify + .api .current_user_saved_albums_delete(vec![album_id.clone()]) .is_none() { @@ -669,6 +680,7 @@ impl Library { if let Some(ref artist_id) = artist.id { if self .spotify + .api .user_follow_artists(vec![artist_id.clone()]) .is_none() { @@ -700,6 +712,7 @@ impl Library { if let Some(ref artist_id) = artist.id { if self .spotify + .api .user_unfollow_artists(vec![artist_id.clone()]) .is_none() { @@ -742,6 +755,7 @@ impl Library { if self .spotify + .api .user_playlist_follow_playlist(playlist.owner_id.clone(), playlist.id.clone()) .is_none() { @@ -775,7 +789,7 @@ impl Library { return; } - if self.spotify.save_shows(vec![show.id.clone()]) { + if self.spotify.api.save_shows(vec![show.id.clone()]) { { let mut store = self.shows.write().unwrap(); if !store.iter().any(|s| s.id == show.id) { @@ -790,7 +804,7 @@ impl Library { return; } - if self.spotify.unsave_shows(vec![show.id.clone()]) { + if self.spotify.api.unsave_shows(vec![show.id.clone()]) { { let mut store = self.shows.write().unwrap(); *store = store.iter().filter(|s| s.id != show.id).cloned().collect(); diff --git a/src/main.rs b/src/main.rs index b24478f..8d4e12c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,6 +34,7 @@ mod serialization; mod sharing; mod show; mod spotify; +mod spotify_api; mod spotify_url; mod spotify_worker; mod theme; diff --git a/src/mpris.rs b/src/mpris.rs index 15828c6..9b826d8 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -47,6 +47,7 @@ fn get_metadata(playable: Option, spotify: Spotify) -> Metadata { Some(Playable::Track(track)) } else { spotify + .api .track(&track.id.unwrap_or_default()) .as_ref() .map(|t| Playable::Track(t.into())) @@ -558,7 +559,7 @@ fn run_dbus_server( let uri_type = UriType::from_uri(&uri); match uri_type { Some(UriType::Album) => { - if let Some(a) = spotify.album(id) { + if let Some(a) = spotify.api.album(id) { if let Some(t) = &Album::from(&a).tracks { queue.clear(); let index = queue.append_next( @@ -571,14 +572,14 @@ fn run_dbus_server( } } Some(UriType::Track) => { - if let Some(t) = spotify.track(id) { + if let Some(t) = spotify.api.track(id) { queue.clear(); queue.append(Playable::Track(Track::from(&t))); queue.play(0, false, false) } } Some(UriType::Playlist) => { - if let Some(p) = spotify.playlist(id) { + if let Some(p) = spotify.api.playlist(id) { let mut playlist = Playlist::from(&p); let spotify = spotify.clone(); playlist.load_tracks(spotify); @@ -594,7 +595,7 @@ fn run_dbus_server( } } Some(UriType::Show) => { - if let Some(s) = spotify.get_show(id) { + if let Some(s) = spotify.api.get_show(id) { let mut show: Show = (&s).into(); let spotify = spotify.clone(); show.load_all_episodes(spotify); @@ -612,14 +613,14 @@ fn run_dbus_server( } } Some(UriType::Episode) => { - if let Some(e) = spotify.episode(id) { + if let Some(e) = spotify.api.episode(id) { queue.clear(); queue.append(Playable::Episode(Episode::from(&e))); queue.play(0, false, false) } } Some(UriType::Artist) => { - if let Some(a) = spotify.artist_top_tracks(id) { + if let Some(a) = spotify.api.artist_top_tracks(id) { queue.clear(); queue.append_next(a.iter().map(|track| Playable::Track(track.clone())).collect()); queue.play(0, false, false) diff --git a/src/playlist.rs b/src/playlist.rs index f548cc0..86c5377 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -33,7 +33,7 @@ impl Playlist { } fn get_all_tracks(&self, spotify: Spotify) -> Vec { - let tracks_result = spotify.user_playlist_tracks(&self.id); + let tracks_result = spotify.api.user_playlist_tracks(&self.id); while !tracks_result.at_end() { tracks_result.next(); } @@ -53,7 +53,10 @@ impl Playlist { pub fn delete_track(&mut self, index: usize, spotify: Spotify, library: Arc) -> bool { let track = self.tracks.as_ref().unwrap()[index].clone(); debug!("deleting track: {} {:?}", index, track); - match spotify.delete_tracks(&self.id, &self.snapshot_id, &[(&track, track.list_index)]) { + match spotify + .api + .delete_tracks(&self.id, &self.snapshot_id, &[(&track, track.list_index)]) + { false => false, true => { if let Some(tracks) = &mut self.tracks { @@ -75,7 +78,7 @@ impl Playlist { let mut has_modified = false; - if spotify.append_tracks(&self.id, &track_ids, None) { + if spotify.api.append_tracks(&self.id, &track_ids, None) { if let Some(tracks) = &mut self.tracks { tracks.append(&mut new_tracks.to_vec()); has_modified = true; diff --git a/src/show.rs b/src/show.rs index 3bf434d..a9cf843 100644 --- a/src/show.rs +++ b/src/show.rs @@ -26,7 +26,7 @@ impl Show { return; } - let episodes_result = spotify.show_episodes(&self.id); + let episodes_result = spotify.api.show_episodes(&self.id); while !episodes_result.at_end() { episodes_result.next(); } diff --git a/src/spotify.rs b/src/spotify.rs index d8b43c7..0a6dc00 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -11,20 +11,7 @@ use librespot_playback::audio_backend; use librespot_playback::config::Bitrate; use librespot_playback::player::Player; -use rspotify::blocking::client::Spotify as SpotifyAPI; -use rspotify::model::album::{FullAlbum, SavedAlbum}; -use rspotify::model::artist::FullArtist; -use rspotify::model::page::{CursorBasedPage, Page}; -use rspotify::model::playlist::FullPlaylist; -use rspotify::model::search::SearchResult; -use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack}; -use rspotify::model::user::PrivateUser; -use rspotify::senum::{AlbumType, SearchType}; -use rspotify::{blocking::client::ApiError, senum::Country}; - -use serde_json::{json, Map}; - -use failure::Error; +use rspotify::senum::Country; use futures::channel::oneshot; use tokio::sync::mpsc; @@ -34,22 +21,13 @@ use url::Url; use std::env; use std::str::FromStr; use std::sync::{Arc, RwLock}; -use std::thread; use std::time::{Duration, SystemTime}; -use crate::artist::Artist; use crate::config; use crate::events::{Event, EventManager}; use crate::playable::Playable; +use crate::spotify_api::WebApi; use crate::spotify_worker::{Worker, WorkerCommand}; -use crate::track::Track; - -use crate::album::Album; -use crate::episode::Episode; -use crate::playlist::Playlist; -use crate::ui::pagination::{ApiPage, ApiResult}; -use rspotify::model::recommend::Recommendations; -use rspotify::model::show::{FullEpisode, FullShow, Show}; pub const VOLUME_PERCENT: u16 = ((u16::max_value() as f64) * 1.0 / 100.0) as u16; @@ -67,13 +45,12 @@ pub struct Spotify { credentials: Credentials, cfg: Arc, status: Arc>, - api: Arc>, + pub api: WebApi, elapsed: Arc>>, since: Arc>>, token_issued: Arc>>, channel: Arc>>>, user: Option, - country: Option, } impl Spotify { @@ -87,13 +64,12 @@ impl Spotify { credentials, cfg: cfg.clone(), status: Arc::new(RwLock::new(PlayerEvent::Stopped)), - api: Arc::new(RwLock::new(SpotifyAPI::default())), + api: WebApi::new(), elapsed: Arc::new(RwLock::new(None)), since: Arc::new(RwLock::new(None)), token_issued: Arc::new(RwLock::new(None)), channel: Arc::new(RwLock::new(None)), user: None, - country: None, }; let (user_tx, user_rx) = oneshot::channel(); @@ -102,11 +78,18 @@ impl Spotify { let volume = cfg.state().volume; spotify.set_volume(volume); - spotify.country = spotify + spotify.api.set_worker_channel(spotify.channel.clone()); + spotify.api.update_token(); + + let country: Option = spotify + .api .current_user() .and_then(|u| u.country) .and_then(|c| c.parse().ok()); + spotify.api.set_user(spotify.user.clone()); + spotify.api.set_country(country); + spotify } @@ -136,9 +119,6 @@ impl Spotify { .await }); } - - // acquire token for web api usage - self.refresh_token(); } pub fn session_config() -> SessionConfig { @@ -292,417 +272,6 @@ impl Spotify { *since } - pub fn refresh_token(&self) { - { - let expiry = self.token_issued.read().unwrap(); - if let Some(time) = *expiry { - if time.elapsed().unwrap() < Duration::from_secs(3000) { - return; - } - } - } - - let (token_tx, token_rx) = oneshot::channel(); - self.send_worker(WorkerCommand::RequestToken(token_tx)); - let token = futures::executor::block_on(token_rx).unwrap(); - - // update token used by web api calls - self.api.write().expect("can't writelock api").access_token = Some(token.access_token); - self.token_issued - .write() - .unwrap() - .replace(SystemTime::now()); - } - - /// retries once when rate limits are hit - fn api_with_retry(&self, cb: F) -> Option - where - F: Fn(&SpotifyAPI) -> Result, - { - let result = { - let api = self.api.read().expect("can't read api"); - cb(&api) - }; - match result { - Ok(v) => Some(v), - Err(e) => { - debug!("api error: {:?}", e); - if let Ok(apierror) = e.downcast::() { - match apierror { - ApiError::RateLimited(d) => { - debug!("rate limit hit. waiting {:?} seconds", d); - thread::sleep(Duration::from_secs(d.unwrap_or(0) as u64)); - let api = self.api.read().expect("can't read api"); - cb(&api).ok() - } - ApiError::Unauthorized => { - debug!("token unauthorized. trying refresh.."); - self.refresh_token(); - let api = self.api.read().expect("can't read api"); - cb(&api).ok() - } - e => { - error!("unhandled api error: {}", e); - None - } - } - } else { - None - } - } - } - } - - pub fn append_tracks( - &self, - playlist_id: &str, - tracks: &[String], - position: Option, - ) -> bool { - self.api_with_retry(|api| { - api.user_playlist_add_tracks(self.user.as_ref().unwrap(), playlist_id, tracks, position) - }) - .is_some() - } - - pub fn delete_tracks( - &self, - playlist_id: &str, - snapshot_id: &str, - track_pos_pairs: &[(&Track, usize)], - ) -> bool { - let mut tracks = Vec::new(); - for (track, pos) in track_pos_pairs { - let track_occurrence = json!({ - "uri": format!("spotify:track:{}", track.id.clone().unwrap()), - "positions": [pos] - }); - let track_occurrence_object = track_occurrence.as_object(); - tracks.push(track_occurrence_object.unwrap().clone()); - } - self.api_with_retry(|api| { - api.user_playlist_remove_specific_occurrenes_of_tracks( - self.user.as_ref().unwrap(), - playlist_id, - tracks.clone(), - Some(snapshot_id.to_string()), - ) - }) - .is_some() - } - - pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) { - // extract only track IDs - let mut tracks: Vec = tracks.iter().filter_map(|track| track.id()).collect(); - - // we can only send 100 tracks per request - let mut remainder = if tracks.len() > 100 { - Some(tracks.split_off(100)) - } else { - None - }; - - if let Some(()) = self.api_with_retry(|api| { - api.user_playlist_replace_tracks(self.user.as_ref().unwrap(), id, &tracks) - }) { - debug!("saved {} tracks to playlist {}", tracks.len(), id); - while let Some(ref mut tracks) = remainder.clone() { - // grab the next set of 100 tracks - remainder = if tracks.len() > 100 { - Some(tracks.split_off(100)) - } else { - None - }; - - debug!("adding another {} tracks to playlist", tracks.len()); - if self.append_tracks(id, tracks, None) { - debug!("{} tracks successfully added", tracks.len()); - } else { - error!("error saving tracks to playlists {}", id); - return; - } - } - } else { - error!("error saving tracks to playlist {}", id); - } - } - - pub fn delete_playlist(&self, id: &str) -> bool { - self.api_with_retry(|api| api.user_playlist_unfollow(self.user.as_ref().unwrap(), id)) - .is_some() - } - - pub fn create_playlist( - &self, - name: &str, - public: Option, - description: Option, - ) -> Option { - let result = self.api_with_retry(|api| { - api.user_playlist_create( - self.user.as_ref().unwrap(), - name, - public, - description.clone(), - ) - }); - result.map(|r| r.id) - } - - pub fn album(&self, album_id: &str) -> Option { - self.api_with_retry(|api| api.album(album_id)) - } - - pub fn artist(&self, artist_id: &str) -> Option { - self.api_with_retry(|api| api.artist(artist_id)) - } - - pub fn playlist(&self, playlist_id: &str) -> Option { - self.api_with_retry(|api| api.playlist(playlist_id, None, self.country)) - } - - pub fn track(&self, track_id: &str) -> Option { - self.api_with_retry(|api| api.track(track_id)) - } - - pub fn get_show(&self, show_id: &str) -> Option { - self.api_with_retry(|api| api.get_a_show(show_id.to_string(), self.country)) - } - - pub fn episode(&self, episode_id: &str) -> Option { - self.api_with_retry(|api| api.get_an_episode(episode_id.to_string(), self.country)) - } - - pub fn recommendations( - &self, - seed_artists: Option>, - seed_genres: Option>, - seed_tracks: Option>, - ) -> Option { - self.api_with_retry(|api| { - api.recommendations( - seed_artists.clone(), - seed_genres.clone(), - seed_tracks.clone(), - 100, - self.country, - &Map::new(), - ) - }) - } - - pub fn search( - &self, - searchtype: SearchType, - query: &str, - limit: u32, - offset: u32, - ) -> Option { - self.api_with_retry(|api| api.search(query, searchtype, limit, offset, self.country, None)) - .take() - } - - pub fn current_user_playlist(&self) -> ApiResult { - const MAX_LIMIT: u32 = 50; - let spotify = self.clone(); - let fetch_page = move |offset: u32| { - debug!("fetching user playlists, offset: {}", offset); - spotify.api_with_retry(|api| match api.current_user_playlists(MAX_LIMIT, offset) { - Ok(page) => Ok(ApiPage { - offset: page.offset, - total: page.total, - items: page.items.iter().map(|sp| sp.into()).collect(), - }), - Err(e) => Err(e), - }) - }; - ApiResult::new(MAX_LIMIT, Arc::new(fetch_page)) - } - - pub fn user_playlist_tracks(&self, playlist_id: &str) -> ApiResult { - const MAX_LIMIT: u32 = 100; - let spotify = self.clone(); - let playlist_id = playlist_id.to_string(); - let fetch_page = move |offset: u32| { - debug!( - "fetching playlist {} tracks, offset: {}", - playlist_id, offset - ); - spotify.api_with_retry(|api| { - match api.user_playlist_tracks( - spotify.user.as_ref().unwrap(), - &playlist_id, - None, - MAX_LIMIT, - offset, - spotify.country, - ) { - Ok(page) => Ok(ApiPage { - offset: page.offset, - total: page.total, - items: page - .items - .iter() - .enumerate() - .flat_map(|(index, pt)| { - pt.track.as_ref().map(|t| { - let mut track: Track = t.into(); - track.added_at = Some(pt.added_at); - track.list_index = page.offset as usize + index; - track - }) - }) - .collect(), - }), - Err(e) => Err(e), - } - }) - }; - ApiResult::new(MAX_LIMIT, Arc::new(fetch_page)) - } - - pub fn full_album(&self, album_id: &str) -> Option { - self.api_with_retry(|api| api.album(album_id)) - } - - pub fn album_tracks( - &self, - album_id: &str, - limit: u32, - offset: u32, - ) -> Option> { - self.api_with_retry(|api| api.album_track(album_id, limit, offset)) - } - - pub fn artist_albums( - &self, - artist_id: &str, - album_type: Option, - ) -> ApiResult { - const MAX_SIZE: u32 = 50; - let spotify = self.clone(); - let artist_id = artist_id.to_string(); - let fetch_page = move |offset: u32| { - debug!("fetching artist {} albums, offset: {}", artist_id, offset); - spotify.api_with_retry(|api| { - match api.artist_albums( - &artist_id, - album_type, - spotify.country, - Some(MAX_SIZE), - Some(offset), - ) { - Ok(page) => { - let mut albums: Vec = - page.items.iter().map(|sa| sa.into()).collect(); - albums.sort_by(|a, b| b.year.cmp(&a.year)); - Ok(ApiPage { - offset: page.offset, - total: page.total, - items: albums, - }) - } - Err(e) => Err(e), - } - }) - }; - - ApiResult::new(MAX_SIZE, Arc::new(fetch_page)) - } - - pub fn show_episodes(&self, show_id: &str) -> ApiResult { - const MAX_SIZE: u32 = 50; - let spotify = self.clone(); - let show_id = show_id.to_string(); - let fetch_page = move |offset: u32| { - debug!("fetching show {} episodes, offset: {}", &show_id, offset); - spotify.api_with_retry(|api| { - match api.get_shows_episodes(show_id.clone(), MAX_SIZE, offset, spotify.country) { - Ok(page) => Ok(ApiPage { - offset: page.offset, - total: page.total, - items: page.items.iter().map(|se| se.into()).collect(), - }), - Err(e) => Err(e), - } - }) - }; - - ApiResult::new(MAX_SIZE, Arc::new(fetch_page)) - } - - pub fn get_saved_shows(&self, offset: u32) -> Option> { - self.api_with_retry(|api| api.get_saved_show(50, offset)) - } - - pub fn save_shows(&self, ids: Vec) -> bool { - self.api_with_retry(|api| api.save_shows(ids.clone())) - .is_some() - } - - pub fn unsave_shows(&self, ids: Vec) -> bool { - self.api_with_retry(|api| api.remove_users_saved_shows(ids.clone(), self.country)) - .is_some() - } - - pub fn current_user_followed_artists( - &self, - last: Option, - ) -> Option> { - self.api_with_retry(|api| api.current_user_followed_artists(50, last.clone())) - .map(|cp| cp.artists) - } - - pub fn user_follow_artists(&self, ids: Vec) -> Option<()> { - self.api_with_retry(|api| api.user_follow_artists(&ids)) - } - - pub fn user_unfollow_artists(&self, ids: Vec) -> Option<()> { - self.api_with_retry(|api| api.user_unfollow_artists(&ids)) - } - - pub fn current_user_saved_albums(&self, offset: u32) -> Option> { - self.api_with_retry(|api| api.current_user_saved_albums(50, offset)) - } - - pub fn current_user_saved_albums_add(&self, ids: Vec) -> Option<()> { - self.api_with_retry(|api| api.current_user_saved_albums_add(&ids)) - } - - pub fn current_user_saved_albums_delete(&self, ids: Vec) -> Option<()> { - self.api_with_retry(|api| api.current_user_saved_albums_delete(&ids)) - } - - pub fn current_user_saved_tracks(&self, offset: u32) -> Option> { - self.api_with_retry(|api| api.current_user_saved_tracks(50, offset)) - } - - pub fn current_user_saved_tracks_add(&self, ids: Vec) -> Option<()> { - self.api_with_retry(|api| api.current_user_saved_tracks_add(&ids)) - } - - pub fn current_user_saved_tracks_delete(&self, ids: Vec) -> Option<()> { - self.api_with_retry(|api| api.current_user_saved_tracks_delete(&ids)) - } - - pub fn user_playlist_follow_playlist(&self, owner_id: String, id: String) -> Option<()> { - self.api_with_retry(|api| api.user_playlist_follow_playlist(&owner_id, &id, true)) - } - - pub fn artist_top_tracks(&self, id: &str) -> Option> { - self.api_with_retry(|api| api.artist_top_tracks(id, self.country)) - .map(|ft| ft.tracks.iter().map(|t| t.into()).collect()) - } - - pub fn artist_related_artists(&self, id: String) -> Option> { - self.api_with_retry(|api| api.artist_related_artists(&id)) - .map(|fa| fa.artists.iter().map(|a| a.into()).collect()) - } - - pub fn current_user(&self) -> Option { - self.api_with_retry(|api| api.current_user()) - } - pub fn load(&self, track: &Playable, start_playing: bool, position_ms: u32) { info!("loading track: {:?}", track); self.send_worker(WorkerCommand::Load( diff --git a/src/spotify_api.rs b/src/spotify_api.rs new file mode 100644 index 0000000..7207223 --- /dev/null +++ b/src/spotify_api.rs @@ -0,0 +1,490 @@ +use crate::album::Album; +use crate::artist::Artist; +use crate::episode::Episode; +use crate::playable::Playable; +use crate::playlist::Playlist; +use crate::spotify_worker::WorkerCommand; +use crate::track::Track; +use crate::ui::pagination::{ApiPage, ApiResult}; +use chrono::{DateTime, Duration as ChronoDuration, Utc}; +use failure::Error; +use futures::channel::oneshot; +use log::{debug, error, info}; +use rspotify::blocking::client::ApiError; +use rspotify::blocking::client::Spotify as SpotifyAPI; +use rspotify::model::album::{FullAlbum, SavedAlbum}; +use rspotify::model::artist::FullArtist; +use rspotify::model::page::{CursorBasedPage, Page}; +use rspotify::model::playlist::FullPlaylist; +use rspotify::model::recommend::Recommendations; +use rspotify::model::search::SearchResult; +use rspotify::model::show::{FullEpisode, FullShow, Show}; +use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack}; +use rspotify::model::user::PrivateUser; +use rspotify::senum::{AlbumType, Country, SearchType}; +use serde_json::{json, Map}; +use std::sync::{Arc, RwLock}; +use std::thread; +use std::time::Duration; +use tokio::sync::mpsc; + +#[derive(Clone)] +pub struct WebApi { + api: Arc>, + user: Option, + country: Option, + worker_channel: Arc>>>, + token_expiration: Arc>>, +} + +impl WebApi { + pub fn new() -> WebApi { + WebApi { + api: Arc::new(RwLock::new(SpotifyAPI::default())), + user: None, + country: None, + worker_channel: Arc::new(RwLock::new(None)), + token_expiration: Arc::new(RwLock::new(Utc::now())), + } + } + + pub fn set_user(&mut self, user: Option) { + self.user = user; + } + + pub fn set_country(&mut self, country: Option) { + self.country = country; + } + + pub(crate) fn set_worker_channel( + &mut self, + channel: Arc>>>, + ) { + self.worker_channel = channel; + } + + pub fn update_token(&self) { + { + let token_expiration = self.token_expiration.read().unwrap(); + let now = Utc::now(); + let delta = *token_expiration - now; + + // token is valid for 5 more minutes, renewal is not necessary yet + if delta.num_seconds() > 60 * 5 { + return; + } + + info!("Token will expire in {}, renewing", delta); + } + + let (token_tx, token_rx) = oneshot::channel(); + let cmd = WorkerCommand::RequestToken(token_tx); + if let Some(channel) = self + .worker_channel + .read() + .expect("can't readlock worker channel") + .as_ref() + { + channel.send(cmd).expect("can't send message to worker"); + let token = futures::executor::block_on(token_rx).unwrap(); + self.api.write().expect("can't writelock api").access_token = + Some(token.access_token.to_string()); + *self + .token_expiration + .write() + .expect("could not writelock token") = + Utc::now() + ChronoDuration::seconds(token.expires_in.into()); + } else { + error!("worker channel is not set"); + } + } + + /// retries once when rate limits are hit + fn api_with_retry(&self, cb: F) -> Option + where + F: Fn(&SpotifyAPI) -> Result, + { + let result = { + let api = self.api.read().expect("can't read api"); + cb(&api) + }; + match result { + Ok(v) => Some(v), + Err(e) => { + debug!("api error: {:?}", e); + if let Ok(apierror) = e.downcast::() { + match apierror { + ApiError::RateLimited(d) => { + debug!("rate limit hit. waiting {:?} seconds", d); + thread::sleep(Duration::from_secs(d.unwrap_or(0) as u64)); + let api = self.api.read().expect("can't read api"); + cb(&api).ok() + } + ApiError::Unauthorized => { + debug!("token unauthorized. trying refresh.."); + self.update_token(); + let api = self.api.read().expect("can't read api"); + cb(&api).ok() + } + e => { + error!("unhandled api error: {}", e); + None + } + } + } else { + None + } + } + } + } + + pub fn append_tracks( + &self, + playlist_id: &str, + tracks: &[String], + position: Option, + ) -> bool { + self.api_with_retry(|api| { + api.user_playlist_add_tracks(self.user.as_ref().unwrap(), playlist_id, tracks, position) + }) + .is_some() + } + + pub fn delete_tracks( + &self, + playlist_id: &str, + snapshot_id: &str, + track_pos_pairs: &[(&Track, usize)], + ) -> bool { + let mut tracks = Vec::new(); + for (track, pos) in track_pos_pairs { + let track_occurrence = json!({ + "uri": format!("spotify:track:{}", track.id.clone().unwrap()), + "positions": [pos] + }); + let track_occurrence_object = track_occurrence.as_object(); + tracks.push(track_occurrence_object.unwrap().clone()); + } + self.api_with_retry(|api| { + api.user_playlist_remove_specific_occurrenes_of_tracks( + self.user.as_ref().unwrap(), + playlist_id, + tracks.clone(), + Some(snapshot_id.to_string()), + ) + }) + .is_some() + } + + pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) { + // extract only track IDs + let mut tracks: Vec = tracks.iter().filter_map(|track| track.id()).collect(); + + // we can only send 100 tracks per request + let mut remainder = if tracks.len() > 100 { + Some(tracks.split_off(100)) + } else { + None + }; + + if let Some(()) = self.api_with_retry(|api| { + api.user_playlist_replace_tracks(self.user.as_ref().unwrap(), id, &tracks) + }) { + debug!("saved {} tracks to playlist {}", tracks.len(), id); + while let Some(ref mut tracks) = remainder.clone() { + // grab the next set of 100 tracks + remainder = if tracks.len() > 100 { + Some(tracks.split_off(100)) + } else { + None + }; + + debug!("adding another {} tracks to playlist", tracks.len()); + if self.append_tracks(id, tracks, None) { + debug!("{} tracks successfully added", tracks.len()); + } else { + error!("error saving tracks to playlists {}", id); + return; + } + } + } else { + error!("error saving tracks to playlist {}", id); + } + } + + pub fn delete_playlist(&self, id: &str) -> bool { + self.api_with_retry(|api| api.user_playlist_unfollow(self.user.as_ref().unwrap(), id)) + .is_some() + } + + pub fn create_playlist( + &self, + name: &str, + public: Option, + description: Option, + ) -> Option { + let result = self.api_with_retry(|api| { + api.user_playlist_create( + self.user.as_ref().unwrap(), + name, + public, + description.clone(), + ) + }); + result.map(|r| r.id) + } + + pub fn album(&self, album_id: &str) -> Option { + self.api_with_retry(|api| api.album(album_id)) + } + + pub fn artist(&self, artist_id: &str) -> Option { + self.api_with_retry(|api| api.artist(artist_id)) + } + + pub fn playlist(&self, playlist_id: &str) -> Option { + self.api_with_retry(|api| api.playlist(playlist_id, None, self.country)) + } + + pub fn track(&self, track_id: &str) -> Option { + self.api_with_retry(|api| api.track(track_id)) + } + + pub fn get_show(&self, show_id: &str) -> Option { + self.api_with_retry(|api| api.get_a_show(show_id.to_string(), self.country)) + } + + pub fn episode(&self, episode_id: &str) -> Option { + self.api_with_retry(|api| api.get_an_episode(episode_id.to_string(), self.country)) + } + + pub fn recommendations( + &self, + seed_artists: Option>, + seed_genres: Option>, + seed_tracks: Option>, + ) -> Option { + self.api_with_retry(|api| { + api.recommendations( + seed_artists.clone(), + seed_genres.clone(), + seed_tracks.clone(), + 100, + self.country, + &Map::new(), + ) + }) + } + + pub fn search( + &self, + searchtype: SearchType, + query: &str, + limit: u32, + offset: u32, + ) -> Option { + self.api_with_retry(|api| api.search(query, searchtype, limit, offset, self.country, None)) + .take() + } + + pub fn current_user_playlist(&self) -> ApiResult { + const MAX_LIMIT: u32 = 50; + let spotify = self.clone(); + let fetch_page = move |offset: u32| { + debug!("fetching user playlists, offset: {}", offset); + spotify.api_with_retry(|api| match api.current_user_playlists(MAX_LIMIT, offset) { + Ok(page) => Ok(ApiPage { + offset: page.offset, + total: page.total, + items: page.items.iter().map(|sp| sp.into()).collect(), + }), + Err(e) => Err(e), + }) + }; + ApiResult::new(MAX_LIMIT, Arc::new(fetch_page)) + } + + pub fn user_playlist_tracks(&self, playlist_id: &str) -> ApiResult { + const MAX_LIMIT: u32 = 100; + let spotify = self.clone(); + let playlist_id = playlist_id.to_string(); + let fetch_page = move |offset: u32| { + debug!( + "fetching playlist {} tracks, offset: {}", + playlist_id, offset + ); + spotify.api_with_retry(|api| { + match api.user_playlist_tracks( + spotify.user.as_ref().unwrap(), + &playlist_id, + None, + MAX_LIMIT, + offset, + spotify.country, + ) { + Ok(page) => Ok(ApiPage { + offset: page.offset, + total: page.total, + items: page + .items + .iter() + .enumerate() + .flat_map(|(index, pt)| { + pt.track.as_ref().map(|t| { + let mut track: Track = t.into(); + track.added_at = Some(pt.added_at); + track.list_index = page.offset as usize + index; + track + }) + }) + .collect(), + }), + Err(e) => Err(e), + } + }) + }; + ApiResult::new(MAX_LIMIT, Arc::new(fetch_page)) + } + + pub fn full_album(&self, album_id: &str) -> Option { + self.api_with_retry(|api| api.album(album_id)) + } + + pub fn album_tracks( + &self, + album_id: &str, + limit: u32, + offset: u32, + ) -> Option> { + self.api_with_retry(|api| api.album_track(album_id, limit, offset)) + } + + pub fn artist_albums( + &self, + artist_id: &str, + album_type: Option, + ) -> ApiResult { + const MAX_SIZE: u32 = 50; + let spotify = self.clone(); + let artist_id = artist_id.to_string(); + let fetch_page = move |offset: u32| { + debug!("fetching artist {} albums, offset: {}", artist_id, offset); + spotify.api_with_retry(|api| { + match api.artist_albums( + &artist_id, + album_type, + spotify.country, + Some(MAX_SIZE), + Some(offset), + ) { + Ok(page) => { + let mut albums: Vec = + page.items.iter().map(|sa| sa.into()).collect(); + albums.sort_by(|a, b| b.year.cmp(&a.year)); + Ok(ApiPage { + offset: page.offset, + total: page.total, + items: albums, + }) + } + Err(e) => Err(e), + } + }) + }; + + ApiResult::new(MAX_SIZE, Arc::new(fetch_page)) + } + + pub fn show_episodes(&self, show_id: &str) -> ApiResult { + const MAX_SIZE: u32 = 50; + let spotify = self.clone(); + let show_id = show_id.to_string(); + let fetch_page = move |offset: u32| { + debug!("fetching show {} episodes, offset: {}", &show_id, offset); + spotify.api_with_retry(|api| { + match api.get_shows_episodes(show_id.clone(), MAX_SIZE, offset, spotify.country) { + Ok(page) => Ok(ApiPage { + offset: page.offset, + total: page.total, + items: page.items.iter().map(|se| se.into()).collect(), + }), + Err(e) => Err(e), + } + }) + }; + + ApiResult::new(MAX_SIZE, Arc::new(fetch_page)) + } + + pub fn get_saved_shows(&self, offset: u32) -> Option> { + self.api_with_retry(|api| api.get_saved_show(50, offset)) + } + + pub fn save_shows(&self, ids: Vec) -> bool { + self.api_with_retry(|api| api.save_shows(ids.clone())) + .is_some() + } + + pub fn unsave_shows(&self, ids: Vec) -> bool { + self.api_with_retry(|api| api.remove_users_saved_shows(ids.clone(), self.country)) + .is_some() + } + + pub fn current_user_followed_artists( + &self, + last: Option, + ) -> Option> { + self.api_with_retry(|api| api.current_user_followed_artists(50, last.clone())) + .map(|cp| cp.artists) + } + + pub fn user_follow_artists(&self, ids: Vec) -> Option<()> { + self.api_with_retry(|api| api.user_follow_artists(&ids)) + } + + pub fn user_unfollow_artists(&self, ids: Vec) -> Option<()> { + self.api_with_retry(|api| api.user_unfollow_artists(&ids)) + } + + pub fn current_user_saved_albums(&self, offset: u32) -> Option> { + self.api_with_retry(|api| api.current_user_saved_albums(50, offset)) + } + + pub fn current_user_saved_albums_add(&self, ids: Vec) -> Option<()> { + self.api_with_retry(|api| api.current_user_saved_albums_add(&ids)) + } + + pub fn current_user_saved_albums_delete(&self, ids: Vec) -> Option<()> { + self.api_with_retry(|api| api.current_user_saved_albums_delete(&ids)) + } + + pub fn current_user_saved_tracks(&self, offset: u32) -> Option> { + self.api_with_retry(|api| api.current_user_saved_tracks(50, offset)) + } + + pub fn current_user_saved_tracks_add(&self, ids: Vec) -> Option<()> { + self.api_with_retry(|api| api.current_user_saved_tracks_add(&ids)) + } + + pub fn current_user_saved_tracks_delete(&self, ids: Vec) -> Option<()> { + self.api_with_retry(|api| api.current_user_saved_tracks_delete(&ids)) + } + + pub fn user_playlist_follow_playlist(&self, owner_id: String, id: String) -> Option<()> { + self.api_with_retry(|api| api.user_playlist_follow_playlist(&owner_id, &id, true)) + } + + pub fn artist_top_tracks(&self, id: &str) -> Option> { + self.api_with_retry(|api| api.artist_top_tracks(id, self.country)) + .map(|ft| ft.tracks.iter().map(|t| t.into()).collect()) + } + + pub fn artist_related_artists(&self, id: String) -> Option> { + self.api_with_retry(|api| api.artist_related_artists(&id)) + .map(|fa| fa.artists.iter().map(|a| a.into()).collect()) + } + + pub fn current_user(&self) -> Option { + self.api_with_retry(|api| api.current_user()) + } +} diff --git a/src/track.rs b/src/track.rs index 871a555..bc9c9fd 100644 --- a/src/track.rs +++ b/src/track.rs @@ -251,6 +251,7 @@ impl ListItem for Track { let recommendations: Option> = if let Some(id) = &self.id { spotify + .api .recommendations(None, None, Some(vec![id.clone()])) .map(|r| r.tracks) .map(|tracks| tracks.iter().map(Track::from).collect()) @@ -283,7 +284,7 @@ impl ListItem for Track { let spotify = queue.get_spotify(); match self.album_id { - Some(ref album_id) => spotify.album(album_id).map(|ref fa| fa.into()), + Some(ref album_id) => spotify.api.album(album_id).map(|ref fa| fa.into()), None => None, } } diff --git a/src/ui/artist.rs b/src/ui/artist.rs index 83121f9..1ca260a 100644 --- a/src/ui/artist.rs +++ b/src/ui/artist.rs @@ -38,7 +38,7 @@ impl ArtistView { let library = library.clone(); thread::spawn(move || { if let Some(id) = id { - if let Some(tracks) = spotify.artist_top_tracks(&id) { + if let Some(tracks) = spotify.api.artist_top_tracks(&id) { top_tracks.write().unwrap().extend(tracks); library.trigger_redraw(); } @@ -53,7 +53,7 @@ impl ArtistView { let library = library.clone(); thread::spawn(move || { if let Some(id) = id { - if let Some(artists) = spotify.artist_related_artists(id) { + if let Some(artists) = spotify.api.artist_related_artists(id) { related.write().unwrap().extend(artists); library.trigger_redraw(); } @@ -106,7 +106,7 @@ impl ArtistView { ) -> ListView { if let Some(artist_id) = &artist.id { let spotify = queue.get_spotify(); - let albums_page = spotify.artist_albums(artist_id, Some(album_type)); + let albums_page = spotify.api.artist_albums(artist_id, Some(album_type)); let view = ListView::new(albums_page.items.clone(), queue, library); albums_page.apply_pagination(view.get_pagination()); diff --git a/src/ui/listview.rs b/src/ui/listview.rs index a436452..bcf130a 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -534,21 +534,27 @@ impl ViewExt for ListView { if let Some(url) = url { let target: Option> = match url.uri_type { UriType::Track => spotify + .api .track(&url.id) .map(|track| Track::from(&track).as_listitem()), UriType::Album => spotify + .api .album(&url.id) .map(|album| Album::from(&album).as_listitem()), UriType::Playlist => spotify + .api .playlist(&url.id) .map(|playlist| Playlist::from(&playlist).as_listitem()), UriType::Artist => spotify + .api .artist(&url.id) .map(|artist| Artist::from(&artist).as_listitem()), UriType::Episode => spotify + .api .episode(&url.id) .map(|episode| Episode::from(&episode).as_listitem()), UriType::Show => spotify + .api .get_show(&url.id) .map(|show| Show::from(&show).as_listitem()), }; diff --git a/src/ui/search_results.rs b/src/ui/search_results.rs index 7fb7e58..9382011 100644 --- a/src/ui/search_results.rs +++ b/src/ui/search_results.rs @@ -109,7 +109,7 @@ impl SearchResultsView { _offset: usize, _append: bool, ) -> u32 { - if let Some(results) = spotify.track(query) { + if let Some(results) = spotify.api.track(query) { let t = vec![(&results).into()]; let mut r = tracks.write().unwrap(); *r = t; @@ -126,7 +126,9 @@ impl SearchResultsView { append: bool, ) -> u32 { if let Some(SearchResult::Tracks(results)) = - spotify.search(SearchType::Track, query, 50, offset as u32) + spotify + .api + .search(SearchType::Track, query, 50, offset as u32) { let mut t = results.items.iter().map(|ft| ft.into()).collect(); let mut r = tracks.write().unwrap(); @@ -148,7 +150,7 @@ impl SearchResultsView { _offset: usize, _append: bool, ) -> u32 { - if let Some(results) = spotify.album(query) { + if let Some(results) = spotify.api.album(query) { let a = vec![(&results).into()]; let mut r = albums.write().unwrap(); *r = a; @@ -165,7 +167,9 @@ impl SearchResultsView { append: bool, ) -> u32 { if let Some(SearchResult::Albums(results)) = - spotify.search(SearchType::Album, query, 50, offset as u32) + spotify + .api + .search(SearchType::Album, query, 50, offset as u32) { let mut a = results.items.iter().map(|sa| sa.into()).collect(); let mut r = albums.write().unwrap(); @@ -187,7 +191,7 @@ impl SearchResultsView { _offset: usize, _append: bool, ) -> u32 { - if let Some(results) = spotify.artist(query) { + if let Some(results) = spotify.api.artist(query) { let a = vec![(&results).into()]; let mut r = artists.write().unwrap(); *r = a; @@ -204,7 +208,9 @@ impl SearchResultsView { append: bool, ) -> u32 { if let Some(SearchResult::Artists(results)) = - spotify.search(SearchType::Artist, query, 50, offset as u32) + spotify + .api + .search(SearchType::Artist, query, 50, offset as u32) { let mut a = results.items.iter().map(|fa| fa.into()).collect(); let mut r = artists.write().unwrap(); @@ -226,7 +232,7 @@ impl SearchResultsView { _offset: usize, _append: bool, ) -> u32 { - if let Some(result) = spotify.playlist(query).as_ref() { + if let Some(result) = spotify.api.playlist(query).as_ref() { let pls = vec![result.into()]; let mut r = playlists.write().unwrap(); *r = pls; @@ -243,7 +249,9 @@ impl SearchResultsView { append: bool, ) -> u32 { if let Some(SearchResult::Playlists(results)) = - spotify.search(SearchType::Playlist, query, 50, offset as u32) + spotify + .api + .search(SearchType::Playlist, query, 50, offset as u32) { let mut pls = results.items.iter().map(|sp| sp.into()).collect(); let mut r = playlists.write().unwrap(); @@ -265,7 +273,7 @@ impl SearchResultsView { _offset: usize, _append: bool, ) -> u32 { - if let Some(result) = spotify.get_show(query).as_ref() { + if let Some(result) = spotify.api.get_show(query).as_ref() { let pls = vec![result.into()]; let mut r = shows.write().unwrap(); *r = pls; @@ -282,7 +290,9 @@ impl SearchResultsView { append: bool, ) -> u32 { if let Some(SearchResult::Shows(results)) = - spotify.search(SearchType::Show, query, 50, offset as u32) + spotify + .api + .search(SearchType::Show, query, 50, offset as u32) { let mut pls = results.items.iter().map(|sp| sp.into()).collect(); let mut r = shows.write().unwrap(); @@ -304,7 +314,7 @@ impl SearchResultsView { _offset: usize, _append: bool, ) -> u32 { - if let Some(result) = spotify.episode(query).as_ref() { + if let Some(result) = spotify.api.episode(query).as_ref() { let e = vec![result.into()]; let mut r = episodes.write().unwrap(); *r = e; @@ -321,7 +331,9 @@ impl SearchResultsView { append: bool, ) -> u32 { if let Some(SearchResult::Episodes(results)) = - spotify.search(SearchType::Episode, query, 50, offset as u32) + spotify + .api + .search(SearchType::Episode, query, 50, offset as u32) { let mut e = results.items.iter().map(|se| se.into()).collect(); let mut r = episodes.write().unwrap(); @@ -378,7 +390,7 @@ impl SearchResultsView { // check if API token refresh is necessary before commencing multiple // requests to avoid deadlock, as the parallel requests might // simultaneously try to refresh the token - self.spotify.refresh_token(); + self.spotify.api.update_token(); // is the query a Spotify URI? if let Some(uritype) = UriType::from_uri(&query) { diff --git a/src/ui/show.rs b/src/ui/show.rs index 2225e91..48e6d93 100644 --- a/src/ui/show.rs +++ b/src/ui/show.rs @@ -23,7 +23,7 @@ impl ShowView { let show = show.clone(); let list = { - let results = spotify.show_episodes(&show.id); + let results = spotify.api.show_episodes(&show.id); let view = ListView::new(results.items.clone(), queue, library); results.apply_pagination(view.get_pagination());