diff --git a/.gitignore b/.gitignore index 50c8301..ac72d99 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ Cargo.lock # These are backup files generated by rustfmt -**/*.rs.bk \ No newline at end of file +**/*.rs.bk + +*.log 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 1bd0e0b..f10cd89 100644 --- a/src/album.rs +++ b/src/album.rs @@ -1,8 +1,10 @@ use std::fmt; use std::sync::Arc; -use rspotify::spotify::model::album::{FullAlbum, SimplifiedAlbum}; +use chrono::{DateTime, Utc}; +use rspotify::spotify::model::album::{FullAlbum, SavedAlbum, SimplifiedAlbum}; +use library::Library; use queue::Queue; use spotify::Spotify; use track::Track; @@ -13,14 +15,16 @@ pub struct Album { pub id: String, pub title: String, pub artists: Vec, + pub artist_ids: Vec, pub year: String, pub cover_url: Option, pub url: String, pub tracks: Option>, + pub added_at: Option>, } impl Album { - fn load_tracks(&mut self, spotify: Arc) { + pub fn load_tracks(&mut self, spotify: Arc) { if self.tracks.is_some() { return; } @@ -43,10 +47,12 @@ impl From<&SimplifiedAlbum> for Album { id: sa.id.clone(), title: sa.name.clone(), artists: sa.artists.iter().map(|sa| sa.name.clone()).collect(), + artist_ids: sa.artists.iter().map(|sa| sa.id.clone()).collect(), year: sa.release_date.split('-').next().unwrap().into(), cover_url: sa.images.get(0).map(|i| i.url.clone()), url: sa.uri.clone(), tracks: None, + added_at: None, } } } @@ -65,14 +71,24 @@ impl From<&FullAlbum> for Album { id: fa.id.clone(), title: fa.name.clone(), artists: fa.artists.iter().map(|sa| sa.name.clone()).collect(), + artist_ids: fa.artists.iter().map(|sa| sa.id.clone()).collect(), year: fa.release_date.split('-').next().unwrap().into(), cover_url: fa.images.get(0).map(|i| i.url.clone()), url: fa.uri.clone(), tracks, + added_at: None, } } } +impl From<&SavedAlbum> for Album { + fn from(sa: &SavedAlbum) -> Self { + let mut album: Self = (&sa.album).into(); + album.added_at = Some(sa.added_at); + album + } +} + impl fmt::Display for Album { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} - {}", self.artists.join(", "), self.title) @@ -112,8 +128,17 @@ impl ListItem for Album { format!("{}", self) } - fn display_right(&self) -> String { - self.year.clone() + fn display_right(&self, library: Arc) -> String { + let saved = if library.is_saved_album(self) { + if library.use_nerdfont { + "\u{f62b} " + } else { + "✓ " + } + } else { + "" + }; + format!("{}{}", saved, self.year) } fn play(&mut self, queue: Arc) { @@ -135,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 f39685b..a738ec1 100644 --- a/src/artist.rs +++ b/src/artist.rs @@ -1,9 +1,10 @@ use std::fmt; use std::sync::Arc; -use rspotify::spotify::model::artist::FullArtist; +use rspotify::spotify::model::artist::{FullArtist, SimplifiedArtist}; use album::Album; +use library::Library; use queue::Queue; use spotify::Spotify; use track::Track; @@ -15,11 +16,16 @@ pub struct Artist { pub name: String, pub url: String, pub albums: Option>, + pub tracks: Option>, + pub is_followed: bool, } impl Artist { fn load_albums(&mut self, spotify: Arc) { - if self.albums.is_some() { + if let Some(albums) = self.albums.as_mut() { + for album in albums { + album.load_tracks(spotify.clone()); + } return; } @@ -41,7 +47,9 @@ impl Artist { } fn tracks(&self) -> Option> { - if let Some(albums) = self.albums.as_ref() { + if let Some(tracks) = self.tracks.as_ref() { + Some(tracks.iter().collect()) + } else if let Some(albums) = self.albums.as_ref() { Some( albums .iter() @@ -55,6 +63,19 @@ impl Artist { } } +impl From<&SimplifiedArtist> for Artist { + fn from(sa: &SimplifiedArtist) -> Self { + Self { + id: sa.id.clone(), + name: sa.name.clone(), + url: sa.uri.clone(), + albums: None, + tracks: None, + is_followed: false, + } + } +} + impl From<&FullArtist> for Artist { fn from(fa: &FullArtist) -> Self { Self { @@ -62,6 +83,8 @@ impl From<&FullArtist> for Artist { name: fa.name.clone(), url: fa.uri.clone(), albums: None, + tracks: None, + is_followed: false, } } } @@ -99,8 +122,24 @@ impl ListItem for Artist { format!("{}", self) } - fn display_right(&self) -> String { - "".into() + fn display_right(&self, library: Arc) -> String { + let followed = if library.is_followed_artist(self) { + if library.use_nerdfont { + "\u{f62b} " + } else { + "✓ " + } + } else { + "" + }; + + let tracks = if let Some(tracks) = self.tracks.as_ref() { + format!("{:>3} saved tracks", tracks.len()) + } else { + "".into() + }; + + format!("{}{}", followed, tracks) } fn play(&mut self, queue: Arc) { @@ -121,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/commands.rs b/src/commands.rs index f1981ac..736b660 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -5,7 +5,7 @@ use cursive::event::{Event, Key}; use cursive::views::ViewRef; use cursive::Cursive; -use playlists::Playlists; +use library::Library; use queue::{Queue, RepeatSetting}; use spotify::Spotify; use traits::ViewExt; @@ -47,7 +47,7 @@ impl CommandManager { &mut self, spotify: Arc, queue: Arc, - playlists: Arc, + library: Arc, ) { self.register_aliases("quit", vec!["q", "x"]); self.register_aliases("playpause", vec!["pause", "toggleplay", "toggleplayback"]); @@ -58,6 +58,7 @@ impl CommandManager { self.register_command("shift", None); self.register_command("play", None); self.register_command("queue", None); + self.register_command("save", None); self.register_command("delete", None); self.register_command( @@ -113,14 +114,13 @@ impl CommandManager { } { - let playlists = playlists.clone(); + let library = library.clone(); self.register_command( "playlists", Some(Box::new(move |_s, args| { if let Some(arg) = args.get(0) { if arg == "update" { - playlists.fetch_playlists(); - playlists.save_cache(); + library.update_playlists(); } } Ok(None) @@ -294,6 +294,8 @@ impl CommandManager { kb.insert("c".into(), "clear".into()); kb.insert(" ".into(), "queue".into()); kb.insert("Enter".into(), "play".into()); + kb.insert("s".into(), "save".into()); + kb.insert("Ctrl+s".into(), "save queue".into()); kb.insert("d".into(), "delete".into()); kb.insert("/".into(), "focus search".into()); kb.insert(".".into(), "seek +500".into()); @@ -303,7 +305,7 @@ impl CommandManager { kb.insert("F1".into(), "focus queue".into()); kb.insert("F2".into(), "focus search".into()); - kb.insert("F3".into(), "focus playlists".into()); + kb.insert("F3".into(), "focus library".into()); kb.insert("Up".into(), "move up".into()); kb.insert("Down".into(), "move down".into()); @@ -362,13 +364,21 @@ impl CommandManager { if split.clone().count() == 2 { let modifier = split.next().unwrap(); let key = split.next().unwrap(); - if let Event::Key(parsed) = Self::parse_key(key) { + let parsed = Self::parse_key(key); + if let Event::Key(parsed) = parsed { match modifier { "Shift" => Some(Event::Shift(parsed)), "Alt" => Some(Event::Alt(parsed)), "Ctrl" => Some(Event::Ctrl(parsed)), _ => None, } + } else if let Event::Char(parsed) = parsed { + match modifier { + "Shift" => Some(Event::Char(parsed.to_uppercase().next().unwrap())), + "Alt" => Some(Event::AltChar(parsed)), + "Ctrl" => Some(Event::CtrlChar(parsed)), + _ => None, + } } else { None } diff --git a/src/library.rs b/src/library.rs new file mode 100644 index 0000000..13ccccb --- /dev/null +++ b/src/library.rs @@ -0,0 +1,765 @@ +use std::collections::HashMap; +use std::iter::Iterator; +use std::ops::Deref; +use std::path::PathBuf; +use std::sync::{Arc, RwLock, RwLockReadGuard}; +use std::thread; + +use rspotify::spotify::model::playlist::{FullPlaylist, SimplifiedPlaylist}; +use serde::de::DeserializeOwned; +use serde::Serialize; + +use album::Album; +use artist::Artist; +use config; +use events::EventManager; +use playlist::Playlist; +use spotify::Spotify; +use track::Track; + +const CACHE_TRACKS: &str = "tracks.db"; +const CACHE_ALBUMS: &str = "albums.db"; +const CACHE_ARTISTS: &str = "artists.db"; +const CACHE_PLAYLISTS: &str = "playlists.db"; + +#[derive(Clone)] +pub struct Library { + pub tracks: Arc>>, + pub albums: Arc>>, + pub artists: Arc>>, + pub playlists: Arc>>, + is_done: Arc>, + ev: EventManager, + spotify: Arc, + pub use_nerdfont: bool, +} + +impl Library { + pub fn new(ev: &EventManager, spotify: Arc, use_nerdfont: bool) -> Self { + let library = Self { + tracks: Arc::new(RwLock::new(Vec::new())), + albums: Arc::new(RwLock::new(Vec::new())), + artists: Arc::new(RwLock::new(Vec::new())), + playlists: Arc::new(RwLock::new(Vec::new())), + is_done: Arc::new(RwLock::new(false)), + ev: ev.clone(), + spotify, + use_nerdfont, + }; + + { + let library = library.clone(); + thread::spawn(move || { + let t_tracks = { + let library = library.clone(); + thread::spawn(move || { + library + .load_cache(config::cache_path(CACHE_TRACKS), library.tracks.clone()); + library.fetch_tracks(); + library + .save_cache(config::cache_path(CACHE_TRACKS), library.tracks.clone()); + }) + }; + + let t_albums = { + let library = library.clone(); + thread::spawn(move || { + library + .load_cache(config::cache_path(CACHE_ALBUMS), library.albums.clone()); + library.fetch_albums(); + library + .save_cache(config::cache_path(CACHE_ALBUMS), library.albums.clone()); + }) + }; + + let t_artists = { + let library = library.clone(); + thread::spawn(move || { + library + .load_cache(config::cache_path(CACHE_ARTISTS), library.artists.clone()); + library.fetch_artists(); + }) + }; + + let t_playlists = { + let library = library.clone(); + thread::spawn(move || { + library.load_cache( + config::cache_path(CACHE_PLAYLISTS), + library.playlists.clone(), + ); + library.fetch_playlists(); + library.save_cache( + config::cache_path(CACHE_PLAYLISTS), + library.playlists.clone(), + ); + }) + }; + + t_tracks.join().unwrap(); + t_artists.join().unwrap(); + + library.populate_artists(); + library.save_cache(config::cache_path(CACHE_ARTISTS), library.artists.clone()); + + t_albums.join().unwrap(); + t_playlists.join().unwrap(); + + let mut is_done = library.is_done.write().unwrap(); + *is_done = true; + }); + } + + library + } + + pub fn items(&self) -> RwLockReadGuard> { + self.playlists + .read() + .expect("could not readlock listview content") + } + + fn load_cache(&self, cache_path: PathBuf, store: Arc>>) { + if let Ok(contents) = std::fs::read_to_string(&cache_path) { + debug!("loading cache from {}", cache_path.display()); + let parsed: Result, _> = serde_json::from_str(&contents); + match parsed { + Ok(cache) => { + debug!( + "cache from {} loaded ({} lists)", + cache_path.display(), + cache.len() + ); + let mut store = store.write().expect("can't writelock store"); + store.clear(); + store.extend(cache); + + // force refresh of UI (if visible) + self.ev.trigger(); + } + Err(e) => { + error!("can't parse cache: {}", e); + } + } + } + } + + fn save_cache(&self, cache_path: PathBuf, store: Arc>>) { + match serde_json::to_string(&store.deref()) { + Ok(contents) => std::fs::write(cache_path, contents).unwrap(), + Err(e) => error!("could not write cache: {:?}", e), + } + } + + pub fn process_simplified_playlist(list: &SimplifiedPlaylist, spotify: &Spotify) -> Playlist { + Self::_process_playlist( + list.id.clone(), + list.name.clone(), + list.owner.id.clone(), + list.snapshot_id.clone(), + spotify, + ) + } + + pub fn process_full_playlist(list: &FullPlaylist, spotify: &Spotify) -> Playlist { + Self::_process_playlist( + list.id.clone(), + list.name.clone(), + list.owner.id.clone(), + list.snapshot_id.clone(), + spotify, + ) + } + + fn _process_playlist( + id: String, + name: String, + owner_id: String, + snapshot_id: String, + spotify: &Spotify, + ) -> Playlist { + let mut collected_tracks = Vec::new(); + + let mut tracks_result = spotify.user_playlist_tracks(&id, 100, 0); + while let Some(ref tracks) = tracks_result.clone() { + for listtrack in &tracks.items { + collected_tracks.push((&listtrack.track).into()); + } + debug!("got {} tracks", tracks.items.len()); + + // load next batch if necessary + tracks_result = match tracks.next { + Some(_) => { + debug!("requesting tracks again.."); + spotify.user_playlist_tracks( + &id, + 100, + tracks.offset + tracks.items.len() as u32, + ) + } + None => None, + } + } + + Playlist { + id, + name, + owner_id, + snapshot_id, + tracks: collected_tracks, + } + } + + fn needs_download(&self, remote: &SimplifiedPlaylist) -> bool { + for local in self + .playlists + .read() + .expect("can't readlock playlists") + .iter() + { + if local.id == remote.id { + return local.snapshot_id != remote.snapshot_id; + } + } + true + } + + fn append_or_update(&self, updated: &Playlist) -> usize { + let mut store = self.playlists.write().expect("can't writelock playlists"); + for (index, mut local) in store.iter_mut().enumerate() { + if local.id == updated.id { + *local = updated.clone(); + return index; + } + } + store.push(updated.clone()); + store.len() - 1 + } + + pub fn delete_playlist(&self, id: &str) { + if !*self.is_done.read().unwrap() { + return; + } + + let pos = { + let store = self.playlists.read().expect("can't readlock playlists"); + store.iter().position(|ref i| i.id == id) + }; + + if let Some(position) = pos { + if self.spotify.delete_playlist(id) { + { + let mut store = self.playlists.write().expect("can't writelock playlists"); + store.remove(position); + } + self.save_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone()); + } + } + } + + pub fn overwrite_playlist(&self, id: &str, tracks: &[Track]) { + debug!("saving {} tracks to {}", tracks.len(), id); + self.spotify.overwrite_playlist(id, &tracks); + + self.update_playlists(); + } + + pub fn save_playlist(&self, name: &str, tracks: &[Track]) { + debug!("saving {} tracks to new list {}", tracks.len(), name); + match self.spotify.create_playlist(name, None, None) { + Some(id) => self.overwrite_playlist(&id, &tracks), + None => error!("could not create new playlist.."), + } + } + + pub fn update_playlists(&self) { + self.fetch_playlists(); + self.save_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone()); + } + + fn fetch_playlists(&self) { + debug!("loading playlists"); + let mut stale_lists = self.playlists.read().unwrap().clone(); + + let mut lists_result = self.spotify.current_user_playlist(50, 0); + while let Some(ref lists) = lists_result.clone() { + for remote in &lists.items { + // remove from stale playlists so we won't prune it later on + if let Some(index) = stale_lists.iter().position(|x| x.id == remote.id) { + stale_lists.remove(index); + } + + if self.needs_download(remote) { + info!("updating playlist {}", remote.name); + let playlist = Self::process_simplified_playlist(remote, &self.spotify); + self.append_or_update(&playlist); + // trigger redraw + self.ev.trigger(); + } + } + + // load next batch if necessary + lists_result = match lists.next { + Some(_) => { + debug!("requesting playlists again.."); + self.spotify + .current_user_playlist(50, lists.offset + lists.items.len() as u32) + } + None => None, + } + } + + // remove stale playlists + for stale in stale_lists { + let index = self + .playlists + .read() + .unwrap() + .iter() + .position(|x| x.id == stale.id); + if let Some(index) = index { + debug!("removing stale list: {:?}", stale.name); + self.playlists.write().unwrap().remove(index); + } + } + // trigger redraw + self.ev.trigger(); + } + + fn fetch_artists(&self) { + let mut artists: Vec = Vec::new(); + let mut last: Option = None; + + let mut i: u32 = 0; + + loop { + let page = self.spotify.current_user_followed_artists(last); + debug!("artists page: {}", i); + i += 1; + if page.is_none() { + error!("Failed to fetch artists."); + return; + } + let page = page.unwrap(); + + artists.extend(page.items.iter().map(|fa| fa.into())); + + if page.next.is_some() { + last = Some(artists.last().unwrap().id.clone()); + } else { + break; + } + } + + let mut store = self.artists.write().unwrap(); + + for artist in artists.iter_mut() { + let pos = store.iter().position(|a| &a.id == &artist.id); + if let Some(i) = pos { + store[i].is_followed = true; + continue; + } + + artist.is_followed = true; + + store.push(artist.clone()); + } + } + + fn insert_artist(&self, id: &String, name: &String) { + let mut artists = self.artists.write().unwrap(); + + if !artists.iter().any(|a| &a.id == id) { + artists.push(Artist { + id: id.clone(), + name: name.clone(), + url: "".into(), + albums: None, + tracks: Some(Vec::new()), + is_followed: false, + }); + } + } + + fn fetch_albums(&self) { + let mut albums: Vec = Vec::new(); + + let mut i: u32 = 0; + + loop { + let page = self.spotify.current_user_saved_albums(albums.len() as u32); + debug!("albums page: {}", i); + i += 1; + if page.is_none() { + error!("Failed to fetch albums."); + return; + } + let page = page.unwrap(); + + if page.offset == 0 { + // If first page matches the first items in store and total is + // identical, assume list is unchanged. + + let store = self.albums.read().unwrap(); + + if page.total as usize == store.len() + && !page + .items + .iter() + .enumerate() + .any(|(i, a)| &a.album.id != &store[i].id) + { + return; + } + } + + albums.extend(page.items.iter().map(|a| a.into())); + + if page.next.is_none() { + break; + } + } + + *(self.albums.write().unwrap()) = albums; + } + + fn fetch_tracks(&self) { + let mut tracks: Vec = Vec::new(); + + let mut i: u32 = 0; + + loop { + let page = self.spotify.current_user_saved_tracks(tracks.len() as u32); + + debug!("tracks page: {}", i); + i += 1; + + if page.is_none() { + error!("Failed to fetch tracks."); + return; + } + let page = page.unwrap(); + + if page.offset == 0 { + // If first page matches the first items in store and total is + // identical, assume list is unchanged. + + let store = self.tracks.read().unwrap(); + + if page.total as usize == store.len() + && !page + .items + .iter() + .enumerate() + .any(|(i, t)| &t.track.id != &store[i].id) + { + return; + } + } + + tracks.extend(page.items.iter().map(|t| t.into())); + + if page.next.is_none() { + break; + } + } + + *(self.tracks.write().unwrap()) = tracks; + } + + 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 + { + let tracks = self.tracks.read().unwrap(); + let mut track_artists: Vec<(&String, &String)> = tracks + .iter() + .flat_map(|t| t.artist_ids.iter().zip(t.artists.iter())) + .collect(); + track_artists.dedup_by(|a, b| a.0 == b.0); + + for (id, name) in track_artists.iter() { + self.insert_artist(id, name); + } + } + + let mut artists = self.artists.write().unwrap(); + let mut lookup: HashMap> = HashMap::new(); + + // Make sure only saved tracks are played when playing artists + for artist in artists.iter_mut() { + artist.tracks = Some(Vec::new()); + } + + artists.sort_unstable_by(|a, b| a.name.partial_cmp(&b.name).unwrap()); + + // Add saved tracks to artists + { + let tracks = self.tracks.read().unwrap(); + for track in tracks.iter() { + for artist_id in &track.artist_ids { + let index = if let Some(i) = lookup.get(artist_id).cloned() { + i + } else { + let i = artists.iter().position(|a| &a.id == artist_id); + lookup.insert(artist_id.clone(), i); + i + }; + + if let Some(i) = index { + let mut artist = artists.get_mut(i).unwrap(); + if artist.tracks.is_none() { + artist.tracks = Some(Vec::new()); + } + + if let Some(tracks) = artist.tracks.as_mut() { + if tracks.iter().any(|t| t.id == track.id) { + continue; + } + + tracks.push(track.clone()); + } + } + } + } + } + } + + pub fn is_saved_track(&self, track: &Track) -> bool { + if !*self.is_done.read().unwrap() { + return false; + } + + let tracks = self.tracks.read().unwrap(); + tracks.iter().any(|t| t.id == track.id) + } + + pub fn save_tracks(&self, tracks: Vec<&Track>, api: bool) { + if !*self.is_done.read().unwrap() { + return; + } + + if api { + if self + .spotify + .current_user_saved_tracks_add(tracks.iter().map(|t| t.id.clone()).collect()) + .is_none() + { + return; + } + } + + { + 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 !*self.is_done.read().unwrap() { + return; + } + + if api { + if self + .spotify + .current_user_saved_tracks_delete(tracks.iter().map(|t| t.id.clone()).collect()) + .is_none() + { + return; + } + } + + { + 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 { + if !*self.is_done.read().unwrap() { + return false; + } + + let albums = self.albums.read().unwrap(); + albums.iter().any(|a| a.id == album.id) + } + + pub fn save_album(&self, album: &mut Album) { + if !*self.is_done.read().unwrap() { + return; + } + + if self + .spotify + .current_user_saved_albums_add(vec![album.id.clone()]) + .is_none() + { + return; + } + + 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) { + if !*self.is_done.read().unwrap() { + return; + } + + if self + .spotify + .current_user_saved_albums_delete(vec![album.id.clone()]) + .is_none() + { + return; + } + + 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 { + if !*self.is_done.read().unwrap() { + return false; + } + + let artists = self.artists.read().unwrap(); + artists.iter().any(|a| a.id == artist.id && a.is_followed) + } + + pub fn follow_artist(&self, artist: &Artist) { + if !*self.is_done.read().unwrap() { + return; + } + + if self + .spotify + .user_follow_artists(vec![artist.id.clone()]) + .is_none() + { + return; + } + + { + 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) { + if !*self.is_done.read().unwrap() { + return; + } + + if self + .spotify + .user_unfollow_artists(vec![artist.id.clone()]) + .is_none() + { + return; + } + + { + 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 { + if !*self.is_done.read().unwrap() { + return false; + } + + let playlists = self.playlists.read().unwrap(); + playlists.iter().any(|p| p.id == playlist.id) + } + + pub fn follow_playlist(&self, playlist: &Playlist) { + if !*self.is_done.read().unwrap() { + return; + } + + if self + .spotify + .user_playlist_follow_playlist(playlist.owner_id.clone(), playlist.id.clone()) + .is_none() + { + return; + } + + { + let mut store = self.playlists.write().unwrap(); + if !store.iter().any(|p| p.id == playlist.id) { + store.insert(0, playlist.clone()); + } + } + + self.save_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone()); + } +} diff --git a/src/main.rs b/src/main.rs index 1145f3d..8819401 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,7 +31,6 @@ extern crate rand; use std::fs; use std::process; use std::sync::Arc; -use std::thread; use clap::{App, Arg}; use cursive::traits::Identifiable; @@ -45,7 +44,8 @@ mod authentication; mod commands; mod config; mod events; -mod playlists; +mod library; +mod playlist; mod queue; mod spotify; mod theme; @@ -58,7 +58,7 @@ mod mpris; use commands::CommandManager; use events::{Event, EventManager}; -use playlists::Playlists; +use library::Library; use spotify::PlayerEvent; fn setup_logging(filename: &str) -> Result<(), fern::InitError> { @@ -156,25 +156,14 @@ fn main() { #[cfg(feature = "mpris")] let mpris_manager = Arc::new(mpris::MprisManager::new(spotify.clone(), queue.clone())); - let playlists = Arc::new(Playlists::new(&event_manager, &spotify)); - - { - // download playlists via web api in a background thread - let playlists = playlists.clone(); - thread::spawn(move || { - // load cache (if existing) - playlists.load_cache(); - - // fetch or update cached playlists - playlists.fetch_playlists(); - - // re-cache for next startup - playlists.save_cache(); - }); - } + let library = Arc::new(Library::new( + &event_manager, + spotify.clone(), + cfg.use_nerdfont.unwrap_or(false), + )); let mut cmd_manager = CommandManager::new(); - cmd_manager.register_all(spotify.clone(), queue.clone(), playlists.clone()); + cmd_manager.register_all(spotify.clone(), queue.clone(), library.clone()); let cmd_manager = Arc::new(cmd_manager); CommandManager::register_keybindings( @@ -183,11 +172,16 @@ fn main() { cfg.keybindings.clone(), ); - let search = ui::search::SearchView::new(event_manager.clone(), spotify.clone(), queue.clone()); + let search = ui::search::SearchView::new( + event_manager.clone(), + spotify.clone(), + queue.clone(), + library.clone(), + ); - let playlistsview = ui::playlists::PlaylistView::new(&playlists, queue.clone()); + let libraryview = ui::library::LibraryView::new(queue.clone(), library.clone()); - let queueview = ui::queue::QueueView::new(queue.clone(), playlists.clone()); + let queueview = ui::queue::QueueView::new(queue.clone(), library.clone()); let status = ui::statusbar::StatusBar::new( queue.clone(), @@ -197,7 +191,7 @@ fn main() { let mut layout = ui::layout::Layout::new(status, &event_manager, theme) .view("search", search.with_id("search"), "Search") - .view("playlists", playlistsview.with_id("playlists"), "Playlists") + .view("library", libraryview.with_id("library"), "Library") .view("queue", queueview, "Queue"); // initial view is queue diff --git a/src/playlist.rs b/src/playlist.rs new file mode 100644 index 0000000..ed217f6 --- /dev/null +++ b/src/playlist.rs @@ -0,0 +1,66 @@ +use std::iter::Iterator; +use std::sync::Arc; + +use library::Library; +use queue::Queue; +use track::Track; +use traits::ListItem; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Playlist { + pub id: String, + pub name: String, + pub owner_id: String, + pub snapshot_id: String, + pub tracks: Vec, +} + +impl ListItem for Playlist { + fn is_playing(&self, queue: Arc) -> bool { + let playing: Vec = queue + .queue + .read() + .unwrap() + .iter() + .map(|t| t.id.clone()) + .collect(); + let ids: Vec = self.tracks.iter().map(|t| t.id.clone()).collect(); + !ids.is_empty() && playing == ids + } + + fn display_left(&self) -> String { + self.name.clone() + } + + fn display_right(&self, library: Arc) -> String { + let saved = if library.is_saved_playlist(self) { + if library.use_nerdfont { + "\u{f62b} " + } else { + "✓ " + } + } else { + "" + }; + format!("{}{:>3} tracks", saved, self.tracks.len()) + } + + fn play(&mut self, queue: Arc) { + let index = queue.append_next(self.tracks.iter().collect()); + queue.play(index, true); + } + + fn queue(&mut self, queue: Arc) { + for track in self.tracks.iter() { + queue.append(track); + } + } + + fn toggle_saved(&mut self, library: Arc) { + if library.is_saved_playlist(self) { + library.delete_playlist(&self.id); + } else { + library.follow_playlist(self); + } + } +} diff --git a/src/playlists.rs b/src/playlists.rs deleted file mode 100644 index 05e9c49..0000000 --- a/src/playlists.rs +++ /dev/null @@ -1,262 +0,0 @@ -use std::iter::Iterator; -use std::ops::Deref; -use std::path::PathBuf; -use std::sync::{Arc, RwLock, RwLockReadGuard}; - -use rspotify::spotify::model::playlist::{FullPlaylist, SimplifiedPlaylist}; - -use config; -use events::EventManager; -use queue::Queue; -use spotify::Spotify; -use track::Track; -use traits::ListItem; - -const CACHE_FILE: &str = "playlists.db"; - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct Playlist { - pub id: String, - pub name: String, - pub snapshot_id: String, - pub tracks: Vec, -} - -#[derive(Clone)] -pub struct Playlists { - pub store: Arc>>, - ev: EventManager, - spotify: Arc, - cache_path: PathBuf, -} - -impl ListItem for Playlist { - fn is_playing(&self, queue: Arc) -> bool { - let playing: Vec = queue - .queue - .read() - .unwrap() - .iter() - .map(|t| t.id.clone()) - .collect(); - let ids: Vec = self.tracks.iter().map(|t| t.id.clone()).collect(); - !ids.is_empty() && playing == ids - } - - fn display_left(&self) -> String { - self.name.clone() - } - - fn display_right(&self) -> String { - format!("{} tracks", self.tracks.len()) - } - - fn play(&mut self, queue: Arc) { - let index = queue.append_next(self.tracks.iter().collect()); - queue.play(index, true); - } - - fn queue(&mut self, queue: Arc) { - for track in self.tracks.iter() { - queue.append(track); - } - } -} - -impl Playlists { - pub fn new(ev: &EventManager, spotify: &Arc) -> Playlists { - Playlists { - store: Arc::new(RwLock::new(Vec::new())), - ev: ev.clone(), - spotify: spotify.clone(), - cache_path: config::cache_path(CACHE_FILE), - } - } - - pub fn items(&self) -> RwLockReadGuard> { - self.store - .read() - .expect("could not readlock listview content") - } - - pub fn load_cache(&self) { - if let Ok(contents) = std::fs::read_to_string(&self.cache_path) { - debug!( - "loading playlist cache from {}", - self.cache_path.to_str().unwrap() - ); - let parsed: Result, _> = serde_json::from_str(&contents); - match parsed { - Ok(cache) => { - debug!("playlist cache loaded ({} lists)", cache.len()); - let mut store = self.store.write().expect("can't writelock playlist store"); - store.clear(); - store.extend(cache); - - // force refresh of UI (if visible) - self.ev.trigger(); - } - Err(e) => { - error!("can't parse playlist cache: {}", e); - } - } - } - } - - pub fn save_cache(&self) { - match serde_json::to_string(&self.store.deref()) { - Ok(contents) => std::fs::write(&self.cache_path, contents).unwrap(), - Err(e) => error!("could not write playlist cache: {:?}", e), - } - } - - pub fn process_simplified_playlist(list: &SimplifiedPlaylist, spotify: &Spotify) -> Playlist { - Playlists::_process_playlist( - list.id.clone(), - list.name.clone(), - list.snapshot_id.clone(), - spotify, - ) - } - - pub fn process_full_playlist(list: &FullPlaylist, spotify: &Spotify) -> Playlist { - Playlists::_process_playlist( - list.id.clone(), - list.name.clone(), - list.snapshot_id.clone(), - spotify, - ) - } - - pub fn _process_playlist( - id: String, - name: String, - snapshot_id: String, - spotify: &Spotify, - ) -> Playlist { - let mut collected_tracks = Vec::new(); - - let mut tracks_result = spotify.user_playlist_tracks(&id, 100, 0); - while let Some(ref tracks) = tracks_result.clone() { - for listtrack in &tracks.items { - collected_tracks.push((&listtrack.track).into()); - } - debug!("got {} tracks", tracks.items.len()); - - // load next batch if necessary - tracks_result = match tracks.next { - Some(_) => { - debug!("requesting tracks again.."); - spotify.user_playlist_tracks( - &id, - 100, - tracks.offset + tracks.items.len() as u32, - ) - } - None => None, - } - } - Playlist { - id: id.clone(), - name: name.clone(), - snapshot_id: snapshot_id.clone(), - tracks: collected_tracks, - } - } - - fn needs_download(&self, remote: &SimplifiedPlaylist) -> bool { - for local in self.store.read().expect("can't readlock playlists").iter() { - if local.id == remote.id { - return local.snapshot_id != remote.snapshot_id; - } - } - true - } - - fn append_or_update(&self, updated: &Playlist) -> usize { - let mut store = self.store.write().expect("can't writelock playlists"); - for (index, mut local) in store.iter_mut().enumerate() { - if local.id == updated.id { - *local = updated.clone(); - return index; - } - } - store.push(updated.clone()); - store.len() - 1 - } - - pub fn delete_playlist(&self, id: &str) { - let mut store = self.store.write().expect("can't writelock playlists"); - if let Some(position) = store.iter().position(|ref i| i.id == id) { - if self.spotify.delete_playlist(id) { - store.remove(position); - self.save_cache(); - } - } - } - - pub fn overwrite_playlist(&self, id: &str, tracks: &[Track]) { - debug!("saving {} tracks to {}", tracks.len(), id); - self.spotify.overwrite_playlist(id, &tracks); - - self.fetch_playlists(); - self.save_cache(); - } - - pub fn save_playlist(&self, name: &str, tracks: &[Track]) { - debug!("saving {} tracks to new list {}", tracks.len(), name); - match self.spotify.create_playlist(name, None, None) { - Some(id) => self.overwrite_playlist(&id, &tracks), - None => error!("could not create new playlist.."), - } - } - - pub fn fetch_playlists(&self) { - debug!("loading playlists"); - let mut stale_lists = self.store.read().unwrap().clone(); - - let mut lists_result = self.spotify.current_user_playlist(50, 0); - while let Some(ref lists) = lists_result.clone() { - for remote in &lists.items { - // remove from stale playlists so we won't prune it later on - if let Some(index) = stale_lists.iter().position(|x| x.id == remote.id) { - stale_lists.remove(index); - } - - if self.needs_download(remote) { - info!("updating playlist {}", remote.name); - let playlist = Self::process_simplified_playlist(remote, &self.spotify); - self.append_or_update(&playlist); - // trigger redraw - self.ev.trigger(); - } - } - - // load next batch if necessary - lists_result = match lists.next { - Some(_) => { - debug!("requesting playlists again.."); - self.spotify - .current_user_playlist(50, lists.offset + lists.items.len() as u32) - } - None => None, - } - } - - // remove stale playlists - for stale in stale_lists { - let index = self - .store - .read() - .unwrap() - .iter() - .position(|x| x.id == stale.id); - if let Some(index) = index { - debug!("removing stale list: {:?}", stale.name); - self.store.write().unwrap().remove(index); - } - } - // trigger redraw - self.ev.trigger(); - } -} diff --git a/src/spotify.rs b/src/spotify.rs index d08efe4..ff593b8 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -13,14 +13,14 @@ use librespot::playback::player::Player; use rspotify::spotify::client::ApiError; use rspotify::spotify::client::Spotify as SpotifyAPI; -use rspotify::spotify::model::album::{FullAlbum, SimplifiedAlbum}; +use rspotify::spotify::model::album::{FullAlbum, SavedAlbum, SimplifiedAlbum}; use rspotify::spotify::model::artist::FullArtist; -use rspotify::spotify::model::page::Page; +use rspotify::spotify::model::page::{CursorBasedPage, Page}; use rspotify::spotify::model::playlist::{FullPlaylist, PlaylistTrack, SimplifiedPlaylist}; use rspotify::spotify::model::search::{ SearchAlbums, SearchArtists, SearchPlaylists, SearchTracks, }; -use rspotify::spotify::model::track::FullTrack; +use rspotify::spotify::model::track::{FullTrack, SavedTrack}; use failure::Error; @@ -527,6 +527,50 @@ impl Spotify { }) } + 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.clone())) + } + + 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 load(&self, track: &Track) { info!("loading track: {:?}", track); self.channel diff --git a/src/track.rs b/src/track.rs index 1a04ba1..2ceef0a 100644 --- a/src/track.rs +++ b/src/track.rs @@ -1,9 +1,11 @@ use std::fmt; use std::sync::Arc; +use chrono::{DateTime, Utc}; use rspotify::spotify::model::album::FullAlbum; -use rspotify::spotify::model::track::{FullTrack, SimplifiedTrack}; +use rspotify::spotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack}; +use library::Library; use queue::Queue; use traits::ListItem; @@ -15,10 +17,12 @@ pub struct Track { pub disc_number: i32, pub duration: u32, pub artists: Vec, + pub artist_ids: Vec, pub album: String, pub album_artists: Vec, pub cover_url: String, pub url: String, + pub added_at: Option>, } impl Track { @@ -28,6 +32,11 @@ impl Track { .iter() .map(|ref artist| artist.name.clone()) .collect::>(); + let artist_ids = track + .artists + .iter() + .map(|ref artist| artist.id.clone()) + .collect::>(); let album_artists = album .artists .iter() @@ -46,10 +55,12 @@ impl Track { disc_number: track.disc_number, duration: track.duration_ms, artists, + artist_ids, album: album.name.clone(), album_artists, cover_url, url: track.uri.clone(), + added_at: None, } } @@ -67,6 +78,11 @@ impl From<&FullTrack> for Track { .iter() .map(|ref artist| artist.name.clone()) .collect::>(); + let artist_ids = track + .artists + .iter() + .map(|ref artist| artist.id.clone()) + .collect::>(); let album_artists = track .album .artists @@ -86,14 +102,24 @@ impl From<&FullTrack> for Track { disc_number: track.disc_number, duration: track.duration_ms, artists, + artist_ids, album: track.album.name.clone(), album_artists, cover_url, url: track.uri.clone(), + added_at: None, } } } +impl From<&SavedTrack> for Track { + fn from(st: &SavedTrack) -> Self { + let mut track: Self = (&st.track).into(); + track.added_at = Some(st.added_at); + track + } +} + impl fmt::Display for Track { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} - {}", self.artists.join(", "), self.title) @@ -122,8 +148,17 @@ impl ListItem for Track { format!("{}", self) } - fn display_right(&self) -> String { - self.duration_str() + fn display_right(&self, library: Arc) -> String { + let saved = if library.is_saved_track(self) { + if library.use_nerdfont { + "\u{f62b} " + } else { + "✓ " + } + } else { + "" + }; + format!("{}{}", saved, self.duration_str()) } fn play(&mut self, queue: Arc) { @@ -134,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 c7791b1..3ec8b09 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -5,14 +5,16 @@ use cursive::views::IdView; use cursive::Cursive; use commands::CommandResult; +use library::Library; use queue::Queue; pub trait ListItem: Sync + Send + 'static { fn is_playing(&self, queue: Arc) -> bool; fn display_left(&self) -> String; - fn display_right(&self) -> String; + 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/library.rs b/src/ui/library.rs new file mode 100644 index 0000000..c3ab05c --- /dev/null +++ b/src/ui/library.rs @@ -0,0 +1,59 @@ +use std::sync::Arc; + +use cursive::view::ViewWrapper; +use cursive::Cursive; + +use commands::CommandResult; +use library::Library; +use queue::Queue; +use traits::ViewExt; +use ui::listview::ListView; +use ui::playlists::PlaylistsView; +use ui::tabview::TabView; + +pub struct LibraryView { + tabs: TabView, +} + +impl LibraryView { + pub fn new(queue: Arc, library: Arc) -> Self { + let tabs = TabView::new() + .tab( + "tracks", + "Tracks", + ListView::new(library.tracks.clone(), queue.clone(), library.clone()), + ) + .tab( + "albums", + "Albums", + ListView::new(library.albums.clone(), queue.clone(), library.clone()), + ) + .tab( + "artists", + "Artists", + ListView::new(library.artists.clone(), queue.clone(), library.clone()), + ) + .tab( + "playlists", + "Playlists", + PlaylistsView::new(queue.clone(), library.clone()), + ); + + Self { tabs } + } +} + +impl ViewWrapper for LibraryView { + wrap_impl!(self.tabs: TabView); +} + +impl ViewExt for LibraryView { + fn on_command( + &mut self, + s: &mut Cursive, + cmd: &str, + args: &[String], + ) -> Result { + self.tabs.on_command(s, cmd, args) + } +} diff --git a/src/ui/listview.rs b/src/ui/listview.rs index d2cc682..da6ed80 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -10,7 +10,9 @@ use cursive::{Cursive, Printer, Rect, Vec2}; use unicode_width::UnicodeWidthStr; use commands::CommandResult; +use library::Library; use queue::Queue; +use track::Track; use traits::{ListItem, ViewExt}; pub type Paginator = Box>>) + Send + Sync>; @@ -83,11 +85,12 @@ pub struct ListView { last_size: Vec2, scrollbar: ScrollBase, queue: Arc, + library: Arc, pagination: Pagination, } impl ListView { - pub fn new(content: Arc>>, queue: Arc) -> Self { + pub fn new(content: Arc>>, queue: Arc, library: Arc) -> Self { Self { content, last_content_len: 0, @@ -95,6 +98,7 @@ impl ListView { last_size: Vec2::new(0, 0), scrollbar: ScrollBase::new(), queue, + library, pagination: Pagination::default(), } } @@ -131,6 +135,19 @@ impl ListView { let new = self.selected as i32 + delta; self.move_focus_to(max(new, 0) as usize); } + + fn attempt_play_all_tracks(&self) -> bool { + let content = self.content.read().unwrap(); + let any = &(*content) as &dyn std::any::Any; + if let Some(tracks) = any.downcast_ref::>() { + let tracks: Vec<&Track> = tracks.iter().collect(); + let index = self.queue.append_next(tracks); + self.queue.play(index + self.selected, true); + true + } else { + false + } + } } impl View for ListView { @@ -160,7 +177,7 @@ impl View for ListView { }; let left = item.display_left(); - let right = item.display_right(); + let right = item.display_right(self.library.clone()); // draw left string printer.with_color(style, |printer| { @@ -256,7 +273,7 @@ impl View for ListView { } } -impl ViewExt for ListView { +impl ViewExt for ListView { fn on_command( &mut self, _s: &mut Cursive, @@ -264,10 +281,13 @@ impl ViewExt for ListView { args: &[String], ) -> Result { if cmd == "play" { - let mut content = self.content.write().unwrap(); - if let Some(item) = content.get_mut(self.selected) { - item.play(self.queue.clone()); + if !self.attempt_play_all_tracks() { + let mut content = self.content.write().unwrap(); + if let Some(item) = content.get_mut(self.selected) { + item.play(self.queue.clone()); + } } + return Ok(CommandResult::Consumed(None)); } @@ -279,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 diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 61c0b77..b6088d3 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,4 +1,5 @@ pub mod layout; +pub mod library; pub mod listview; pub mod modal; pub mod playlists; diff --git a/src/ui/playlists.rs b/src/ui/playlists.rs index a53055c..2912b01 100644 --- a/src/ui/playlists.rs +++ b/src/ui/playlists.rs @@ -1,47 +1,43 @@ use std::sync::Arc; -use cursive::traits::Identifiable; use cursive::view::ViewWrapper; -use cursive::views::{Dialog, IdView}; +use cursive::views::Dialog; use cursive::Cursive; use commands::CommandResult; -use playlists::{Playlist, Playlists}; +use library::Library; +use playlist::Playlist; use queue::Queue; use traits::ViewExt; use ui::listview::ListView; use ui::modal::Modal; -pub struct PlaylistView { - list: IdView>, - playlists: Playlists, +pub struct PlaylistsView { + list: ListView, + library: Arc, } -pub const LIST_ID: &str = "playlist_list"; -impl PlaylistView { - pub fn new(playlists: &Playlists, queue: Arc) -> PlaylistView { - let list = ListView::new(playlists.store.clone(), queue).with_id(LIST_ID); - - PlaylistView { - list, - playlists: playlists.clone(), +impl PlaylistsView { + pub fn new(queue: Arc, library: Arc) -> Self { + Self { + list: ListView::new(library.playlists.clone(), queue.clone(), library.clone()), + library, } } pub fn delete_dialog(&mut self) -> Option> { - let list = self.list.get_mut(); - let store = self.playlists.items(); - let current = store.get(list.get_selected_index()); + let store = self.library.items(); + let current = store.get(self.list.get_selected_index()); if let Some(playlist) = current { - let playlists = self.playlists.clone(); + let library = self.library.clone(); let id = playlist.id.clone(); let dialog = Dialog::text("Are you sure you want to delete this playlist?") .padding((1, 1, 1, 0)) .title("Delete playlist") .dismiss_button("No") .button("Yes", move |s: &mut Cursive| { - playlists.delete_playlist(&id); + library.delete_playlist(&id); s.pop_layer(); }); Some(Modal::new(dialog)) @@ -51,11 +47,11 @@ impl PlaylistView { } } -impl ViewWrapper for PlaylistView { - wrap_impl!(self.list: IdView>); +impl ViewWrapper for PlaylistsView { + wrap_impl!(self.list: ListView); } -impl ViewExt for PlaylistView { +impl ViewExt for PlaylistsView { fn on_command( &mut self, s: &mut Cursive, diff --git a/src/ui/queue.rs b/src/ui/queue.rs index ab753ff..416b02e 100644 --- a/src/ui/queue.rs +++ b/src/ui/queue.rs @@ -1,5 +1,4 @@ -use cursive::event::{Callback, Event, EventResult}; -use cursive::traits::{Boxable, Identifiable, View}; +use cursive::traits::{Boxable, Identifiable}; use cursive::view::ViewWrapper; use cursive::views::{Dialog, EditView, ScrollView, SelectView}; use cursive::Cursive; @@ -8,7 +7,7 @@ use std::cmp::min; use std::sync::Arc; use commands::CommandResult; -use playlists::Playlists; +use library::Library; use queue::Queue; use track::Track; use traits::ViewExt; @@ -17,17 +16,17 @@ use ui::modal::Modal; pub struct QueueView { list: ListView, - playlists: Arc, + library: Arc, queue: Arc, } impl QueueView { - pub fn new(queue: Arc, playlists: Arc) -> QueueView { - let list = ListView::new(queue.queue.clone(), queue.clone()); + pub fn new(queue: Arc, library: Arc) -> QueueView { + let list = ListView::new(queue.queue.clone(), queue.clone(), library.clone()); QueueView { list, - playlists, + library, queue, } } @@ -35,20 +34,20 @@ impl QueueView { fn save_dialog_cb( s: &mut Cursive, queue: Arc, - playlists: Arc, + library: Arc, id: Option, ) { let tracks = queue.queue.read().unwrap().clone(); match id { Some(id) => { - playlists.overwrite_playlist(&id, &tracks); + library.overwrite_playlist(&id, &tracks); s.pop_layer(); } None => { s.pop_layer(); let edit = EditView::new() .on_submit(move |s: &mut Cursive, name| { - playlists.save_playlist(name, &tracks); + library.save_playlist(name, &tracks); s.pop_layer(); }) .with_id("name") @@ -63,16 +62,16 @@ impl QueueView { } } - fn save_dialog(queue: Arc, playlists: Arc) -> Modal { + fn save_dialog(queue: Arc, library: Arc) -> Modal { let mut list_select: SelectView> = SelectView::new().autojump(); list_select.add_item("[Create new]", None); - for list in playlists.items().iter() { + for list in library.items().iter() { list_select.add_item(list.name.clone(), Some(list.id.clone())); } list_select.set_on_submit(move |s, selected| { - Self::save_dialog_cb(s, queue.clone(), playlists.clone(), selected.clone()) + Self::save_dialog_cb(s, queue.clone(), library.clone(), selected.clone()) }); let dialog = Dialog::new() @@ -86,22 +85,6 @@ impl QueueView { impl ViewWrapper for QueueView { wrap_impl!(self.list: ListView); - - fn wrap_on_event(&mut self, ch: Event) -> EventResult { - match ch { - Event::Char('s') => { - debug!("save list"); - let queue = self.queue.clone(); - let playlists = self.playlists.clone(); - let cb = move |s: &mut Cursive| { - let dialog = Self::save_dialog(queue.clone(), playlists.clone()); - s.add_layer(dialog) - }; - EventResult::Consumed(Some(Callback::from_fn(cb))) - } - _ => self.list.on_event(ch), - } - } } impl ViewExt for QueueView { @@ -147,6 +130,12 @@ impl ViewExt for QueueView { } } + if cmd == "save" && args.get(0).unwrap_or(&"".to_string()) == "queue" { + let dialog = Self::save_dialog(self.queue.clone(), self.library.clone()); + s.add_layer(dialog); + return Ok(CommandResult::Consumed(None)); + } + self.with_view_mut(move |v| v.on_command(s, cmd, args)) .unwrap() } diff --git a/src/ui/search.rs b/src/ui/search.rs index 0748083..f0818b3 100644 --- a/src/ui/search.rs +++ b/src/ui/search.rs @@ -13,7 +13,8 @@ use album::Album; use artist::Artist; use commands::CommandResult; use events::EventManager; -use playlists::{Playlist, Playlists}; +use library::Library; +use playlist::Playlist; use queue::Queue; use spotify::{Spotify, URIType}; use track::Track; @@ -43,7 +44,12 @@ type SearchHandler = pub const LIST_ID: &str = "search_list"; pub const EDIT_ID: &str = "search_edit"; impl SearchView { - pub fn new(events: EventManager, spotify: Arc, queue: Arc) -> SearchView { + pub fn new( + events: EventManager, + spotify: Arc, + queue: Arc, + library: Arc, + ) -> SearchView { let results_tracks = Arc::new(RwLock::new(Vec::new())); let results_albums = Arc::new(RwLock::new(Vec::new())); let results_artists = Arc::new(RwLock::new(Vec::new())); @@ -60,13 +66,14 @@ impl SearchView { }) .with_id(EDIT_ID); - let list_tracks = ListView::new(results_tracks.clone(), queue.clone()); + let list_tracks = ListView::new(results_tracks.clone(), queue.clone(), library.clone()); let pagination_tracks = list_tracks.get_pagination().clone(); - let list_albums = ListView::new(results_albums.clone(), queue.clone()); + let list_albums = ListView::new(results_albums.clone(), queue.clone(), library.clone()); let pagination_albums = list_albums.get_pagination().clone(); - let list_artists = ListView::new(results_artists.clone(), queue.clone()); + let list_artists = ListView::new(results_artists.clone(), queue.clone(), library.clone()); let pagination_artists = list_artists.get_pagination().clone(); - let list_playlists = ListView::new(results_playlists.clone(), queue.clone()); + let list_playlists = + ListView::new(results_playlists.clone(), queue.clone(), library.clone()); let pagination_playlists = list_playlists.get_pagination().clone(); let tabs = TabView::new() @@ -218,7 +225,7 @@ impl SearchView { _append: bool, ) -> u32 { if let Some(results) = spotify.playlist(&query) { - let pls = vec![Playlists::process_full_playlist(&results, &&spotify)]; + let pls = vec![Library::process_full_playlist(&results, &&spotify)]; let mut r = playlists.write().unwrap(); *r = pls; return 1; @@ -238,7 +245,7 @@ impl SearchView { .playlists .items .iter() - .map(|sp| Playlists::process_simplified_playlist(sp, &&spotify)) + .map(|sp| Library::process_simplified_playlist(sp, &&spotify)) .collect(); let mut r = playlists.write().unwrap();