diff --git a/Cargo.toml b/Cargo.toml index d7914e7..0f76768 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ failure = "0.1.3" fern = "0.5" futures = "0.1" log = "0.4.0" -rspotify = "0.4.0" +#rspotify = "0.4.0" serde = "1.0" serde_json = "1.0" toml = "0.4" @@ -34,6 +34,10 @@ dbus = { version = "0.6.4", optional = true } rand = "0.6.5" webbrowser = "0.5" +[dependencies.rspotify] +git = "https://github.com/KoffeinFlummi/rspotify" +rev = "1a30afc" + [dependencies.librespot] git = "https://github.com/librespot-org/librespot.git" rev = "14721f4" diff --git a/src/album.rs b/src/album.rs index eae1110..f99868b 100644 --- a/src/album.rs +++ b/src/album.rs @@ -160,4 +160,12 @@ impl ListItem for Album { } } } + + fn toggle_saved(&mut self, library: Arc) { + if library.is_saved_album(self) { + library.unsave_album(self); + } else { + library.save_album(self); + } + } } diff --git a/src/artist.rs b/src/artist.rs index 511c2c7..a738ec1 100644 --- a/src/artist.rs +++ b/src/artist.rs @@ -160,4 +160,12 @@ impl ListItem for Artist { } } } + + fn toggle_saved(&mut self, library: Arc) { + if library.is_followed_artist(self) { + library.unfollow_artist(self); + } else { + library.follow_artist(self); + } + } } diff --git a/src/library.rs b/src/library.rs index 73e68fe..21fb171 100644 --- a/src/library.rs +++ b/src/library.rs @@ -5,7 +5,6 @@ use std::path::PathBuf; use std::sync::{Arc, RwLock, RwLockReadGuard}; use std::thread; -use rspotify::spotify::model::artist::SimplifiedArtist; use rspotify::spotify::model::playlist::{FullPlaylist, SimplifiedPlaylist}; use serde::de::DeserializeOwned; use serde::Serialize; @@ -311,22 +310,31 @@ impl Library { continue; } - // Only play saved tracks - artist.albums = Some(Vec::new()); - artist.tracks = Some(Vec::new()); artist.is_followed = true; store.push(artist.clone()); } } - fn insert_artist(&self, artist: &SimplifiedArtist) { + fn insert_artist(&self, track: &Track) { let mut artists = self.artists.write().unwrap(); - if artists.iter().any(|a| a.id == artist.id) { - return; - } - artists.push(artist.into()); + for (id, name) in track.artist_ids.iter().zip(track.artists.iter()) { + let artist = Artist { + id: id.clone(), + name: name.clone(), + url: "".into(), + albums: Some(Vec::new()), + tracks: Some(Vec::new()), + is_followed: false, + }; + + if artists.iter().any(|a| a.id == artist.id) { + continue; + } + + artists.push(artist.into()); + } } fn fetch_albums(&self) { @@ -387,12 +395,7 @@ impl Library { } } - for track in page.items.iter() { - for artist in track.track.artists.iter() { - self.insert_artist(artist); - } - tracks.push(track.into()); - } + tracks.extend(page.items.iter().map(|t| t.into())); if page.next.is_none() { break; @@ -403,11 +406,35 @@ impl Library { } fn populate_artists(&self) { + // Remove old unfollowed artists + { + let mut artists = self.artists.write().unwrap(); + *artists = artists + .iter() + .filter(|a| a.is_followed) + .cloned() + .collect(); + } + + // Add artists that aren't followed but have saved tracks/albums + { + let tracks = self.tracks.read().unwrap(); + for track in tracks.iter() { + self.insert_artist(track); + } + } + let mut artists = self.artists.write().unwrap(); let mut lookup: HashMap> = HashMap::new(); + for artist in artists.iter_mut() { + artist.albums = Some(Vec::new()); + artist.tracks = Some(Vec::new()); + } + artists.sort_unstable_by(|a, b| a.name.partial_cmp(&b.name).unwrap()); + // Add saved albums to artists { let albums = self.albums.read().unwrap(); for album in albums.iter() { @@ -438,6 +465,7 @@ impl Library { } } + // Add saved tracks to artists { let tracks = self.tracks.read().unwrap(); for track in tracks.iter() { @@ -474,16 +502,144 @@ impl Library { tracks.iter().any(|t| t.id == track.id) } + pub fn save_tracks(&self, tracks: Vec<&Track>, api: bool) { + if api { + self.spotify.current_user_saved_tracks_add( + tracks + .iter() + .map(|t| t.id.clone()) + .collect() + ); + } + + { + let mut store = self.tracks.write().unwrap(); + let mut i = 0; + for track in tracks { + if store.iter().any(|t| t.id == track.id) { + continue; + } + + store.insert(i, track.clone()); + i += 1; + } + } + + self.populate_artists(); + + self.save_cache(config::cache_path(CACHE_TRACKS), self.tracks.clone()); + self.save_cache(config::cache_path(CACHE_ARTISTS), self.artists.clone()); + } + + pub fn unsave_tracks(&self, tracks: Vec<&Track>, api: bool) { + if api { + self.spotify.current_user_saved_tracks_delete( + tracks + .iter() + .map(|t| t.id.clone()) + .collect() + ); + } + + { + let mut store = self.tracks.write().unwrap(); + *store = store + .iter() + .filter(|t| !tracks.iter().any(|tt| t.id == tt.id)) + .cloned() + .collect(); + } + + self.populate_artists(); + + self.save_cache(config::cache_path(CACHE_TRACKS), self.tracks.clone()); + self.save_cache(config::cache_path(CACHE_ARTISTS), self.artists.clone()); + } + pub fn is_saved_album(&self, album: &Album) -> bool { let albums = self.albums.read().unwrap(); albums.iter().any(|a| a.id == album.id) } + pub fn save_album(&self, album: &mut Album) { + self.spotify.current_user_saved_albums_add(vec![album.id.clone()]); + + album.load_tracks(self.spotify.clone()); + + { + let mut store = self.albums.write().unwrap(); + if !store.iter().any(|a| a.id == album.id) { + store.insert(0, album.clone()); + } + } + + if let Some(tracks) = album.tracks.as_ref() { + self.save_tracks(tracks.iter().collect(), false); + } + + self.save_cache(config::cache_path(CACHE_ALBUMS), self.albums.clone()); + } + + pub fn unsave_album(&self, album: &mut Album) { + self.spotify.current_user_saved_albums_delete(vec![album.id.clone()]); + + album.load_tracks(self.spotify.clone()); + + { + let mut store = self.albums.write().unwrap(); + *store = store + .iter() + .filter(|a| a.id != album.id) + .cloned() + .collect(); + } + + if let Some(tracks) = album.tracks.as_ref() { + self.unsave_tracks(tracks.iter().collect(), false); + } + + self.save_cache(config::cache_path(CACHE_ALBUMS), self.albums.clone()); + } + pub fn is_followed_artist(&self, artist: &Artist) -> bool { let artists = self.artists.read().unwrap(); artists.iter().any(|a| a.id == artist.id && a.is_followed) } + pub fn follow_artist(&self, artist: &Artist) { + self.spotify.user_follow_artists(vec![artist.id.clone()]); + + { + let mut store = self.artists.write().unwrap(); + if let Some(i) = store.iter().position(|a| a.id == artist.id) { + store[i].is_followed = true; + } else { + let mut artist = artist.clone(); + artist.is_followed = true; + store.push(artist); + } + } + + self.populate_artists(); + + self.save_cache(config::cache_path(CACHE_ARTISTS), self.artists.clone()); + } + + pub fn unfollow_artist(&self, artist: &Artist) { + self.spotify.user_unfollow_artists(vec![artist.id.clone()]); + + { + let mut store = self.artists.write().unwrap(); + if let Some(i) = store.iter().position(|a| a.id == artist.id) { + store[i].is_followed = false; + } + } + + self.populate_artists(); + + self.save_cache(config::cache_path(CACHE_ARTISTS), self.artists.clone()); + } + pub fn is_saved_playlist(&self, playlist: &Playlist) -> bool { let playlists = self.playlists.read().unwrap(); playlists.iter().any(|p| p.id == playlist.id) diff --git a/src/playlist.rs b/src/playlist.rs index 4c90aa2..c7821a9 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -54,4 +54,8 @@ impl ListItem for Playlist { queue.append(track); } } + + fn toggle_saved(&mut self, _library: Arc) { + // TODO + } } diff --git a/src/spotify.rs b/src/spotify.rs index a72aa3c..a8cdf08 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -532,14 +532,38 @@ impl Spotify { .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.clone())) + } + pub fn load(&self, track: &Track) { info!("loading track: {:?}", track); self.channel diff --git a/src/track.rs b/src/track.rs index 9d188ba..bf745f2 100644 --- a/src/track.rs +++ b/src/track.rs @@ -169,4 +169,12 @@ impl ListItem for Track { fn queue(&mut self, queue: Arc) { queue.append(self); } + + fn toggle_saved(&mut self, library: Arc) { + if library.is_saved_track(self) { + library.unsave_tracks(vec![self], true); + } else { + library.save_tracks(vec![self], true); + } + } } diff --git a/src/traits.rs b/src/traits.rs index d851a32..3ec8b09 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -14,6 +14,7 @@ pub trait ListItem: Sync + Send + 'static { fn display_right(&self, library: Arc) -> String; fn play(&mut self, queue: Arc); fn queue(&mut self, queue: Arc); + fn toggle_saved(&mut self, library: Arc); } pub trait ViewExt: View { diff --git a/src/ui/listview.rs b/src/ui/listview.rs index afd76a4..da6ed80 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -273,7 +273,7 @@ impl View for ListView { } } -impl ViewExt for ListView { +impl ViewExt for ListView { fn on_command( &mut self, _s: &mut Cursive, @@ -299,6 +299,17 @@ impl ViewExt for ListView { return Ok(CommandResult::Consumed(None)); } + if cmd == "save" { + let mut item = { + let content = self.content.read().unwrap(); + content.get(self.selected).cloned() + }; + + if let Some(item) = item.as_mut() { + item.toggle_saved(self.library.clone()); + } + } + if cmd == "move" { if let Some(dir) = args.get(0) { let amount: usize = args