From dd69a8c6f280cda99ddf7f16bddfed88394d0ca1 Mon Sep 17 00:00:00 2001 From: KoffeinFlummi Date: Tue, 16 Apr 2019 19:45:06 +0200 Subject: [PATCH 01/13] Fix keybinding parsing for Mod+Char --- src/commands.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/commands.rs b/src/commands.rs index f1981ac..3b6ca18 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -362,13 +362,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 } From 210c7d9f4e678d4905ed8bb14a7e3bbd42ced02d Mon Sep 17 00:00:00 2001 From: KoffeinFlummi Date: Tue, 16 Apr 2019 19:52:22 +0200 Subject: [PATCH 02/13] Implement saved tracks, albums, and artists --- src/album.rs | 19 +- src/artist.rs | 32 +++- src/commands.rs | 11 +- src/library.rs | 445 ++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 31 +-- src/playlist.rs | 47 +++++ src/playlists.rs | 262 -------------------------- src/spotify.rs | 19 +- src/track.rs | 27 ++- src/ui/library.rs | 79 ++++++++ src/ui/mod.rs | 2 +- src/ui/playlists.rs | 74 -------- src/ui/queue.rs | 24 +-- src/ui/search.rs | 7 +- 14 files changed, 688 insertions(+), 391 deletions(-) create mode 100644 src/library.rs create mode 100644 src/playlist.rs delete mode 100644 src/playlists.rs create mode 100644 src/ui/library.rs delete mode 100644 src/ui/playlists.rs diff --git a/src/album.rs b/src/album.rs index 1bd0e0b..54d7124 100644 --- a/src/album.rs +++ b/src/album.rs @@ -1,7 +1,8 @@ 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 queue::Queue; use spotify::Spotify; @@ -13,14 +14,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 +46,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 +70,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) diff --git a/src/artist.rs b/src/artist.rs index f39685b..4fe16b0 100644 --- a/src/artist.rs +++ b/src/artist.rs @@ -1,7 +1,7 @@ use std::fmt; use std::sync::Arc; -use rspotify::spotify::model::artist::FullArtist; +use rspotify::spotify::model::artist::{FullArtist, SimplifiedArtist}; use album::Album; use queue::Queue; @@ -15,11 +15,15 @@ pub struct Artist { pub name: String, pub url: String, pub albums: Option>, + pub tracks: Option>, } 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 +45,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 +61,18 @@ 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, + } + } +} + impl From<&FullArtist> for Artist { fn from(fa: &FullArtist) -> Self { Self { @@ -62,6 +80,7 @@ impl From<&FullArtist> for Artist { name: fa.name.clone(), url: fa.uri.clone(), albums: None, + tracks: None, } } } @@ -100,7 +119,12 @@ impl ListItem for Artist { } fn display_right(&self) -> String { - "".into() + // TODO: indicate following status + if let Some(tracks) = self.tracks.as_ref() { + format!("{} saved tracks", tracks.len()) + } else { + "".into() + } } fn play(&mut self, queue: Arc) { diff --git a/src/commands.rs b/src/commands.rs index 3b6ca18..3b02055 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"]); @@ -113,14 +113,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) @@ -303,7 +302,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()); diff --git a/src/library.rs b/src/library.rs new file mode 100644 index 0000000..03320cf --- /dev/null +++ b/src/library.rs @@ -0,0 +1,445 @@ +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::artist::SimplifiedArtist; +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>>, + ev: EventManager, + spotify: Arc, +} + +impl Library { + pub fn new(ev: &EventManager, spotify: Arc) -> 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())), + ev: ev.clone(), + spotify, + }; + + { + // download playlists via web api in a background thread + let library = library.clone(); + thread::spawn(move || { + // load cache (if existing) + library.load_caches(); + + library.fetch_artists(); + library.fetch_tracks(); + library.fetch_albums(); + library.fetch_playlists(); + + library.populate_artists(); + + // re-cache for next startup + library.save_caches(); + }); + } + + 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 load_caches(&self) { + self.load_cache(config::cache_path(CACHE_TRACKS), self.tracks.clone()); + self.load_cache(config::cache_path(CACHE_ALBUMS), self.albums.clone()); + self.load_cache(config::cache_path(CACHE_ARTISTS), self.artists.clone()); + self.load_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone()); + } + + 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), + } + } + + fn save_caches(&self) { + self.save_cache(config::cache_path(CACHE_TRACKS), self.tracks.clone()); + self.save_cache(config::cache_path(CACHE_ALBUMS), self.albums.clone()); + self.save_cache(config::cache_path(CACHE_ARTISTS), self.artists.clone()); + self.save_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone()); + } + + pub fn process_simplified_playlist(list: &SimplifiedPlaylist, spotify: &Spotify) -> Playlist { + Self::_process_playlist( + list.id.clone(), + list.name.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.snapshot_id.clone(), + spotify, + ) + } + + 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.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) { + let mut store = self.playlists.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(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; + } + } + + for artist in artists.iter_mut() { + // Only play saved tracks + artist.albums = Some(Vec::new()); + artist.tracks = Some(Vec::new()); + } + + *(self.artists.write().unwrap()) = artists; + } + + fn insert_artist(&self, artist: &SimplifiedArtist) { + let mut artists = self.artists.write().unwrap(); + if artists.iter().any(|a| a.id == artist.id) { + return; + } + + artists.push(artist.into()); + } + + 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(); + + 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; + } + } + + for track in page.items.iter() { + for artist in track.track.artists.iter() { + self.insert_artist(artist); + } + tracks.push(track.into()); + } + + if page.next.is_none() { + break; + } + } + + *(self.tracks.write().unwrap()) = tracks; + } + + fn populate_artists(&self) { + let mut artists = self.artists.write().unwrap(); + let mut lookup: HashMap> = HashMap::new(); + + artists.sort_unstable_by(|a, b| a.name.partial_cmp(&b.name).unwrap()); + + { + let albums = self.albums.read().unwrap(); + for album in albums.iter() { + for artist_id in &album.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.albums.is_none() { + artist.albums = Some(Vec::new()); + } + + if let Some(albums) = artist.albums.as_mut() { + albums.push(album.clone()); + } + } + } + } + } + + { + 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() { + tracks.push(track.clone()); + } + } + } + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 1145f3d..41a7322 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,10 @@ 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())); 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( @@ -185,9 +170,9 @@ fn main() { let search = ui::search::SearchView::new(event_manager.clone(), spotify.clone(), queue.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 +182,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..e17b762 --- /dev/null +++ b/src/playlist.rs @@ -0,0 +1,47 @@ +use std::iter::Iterator; +use std::sync::Arc; + +use queue::Queue; +use track::Track; +use traits::ListItem; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Playlist { + pub id: String, + pub name: 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) -> 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); + } + } +} 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..a72aa3c 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,19 @@ 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 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_tracks(&self, offset: u32) -> Option> { + self.api_with_retry(|api| api.current_user_saved_tracks(50, offset)) + } + pub fn load(&self, track: &Track) { info!("loading track: {:?}", track); self.channel diff --git a/src/track.rs b/src/track.rs index 1a04ba1..85c202f 100644 --- a/src/track.rs +++ b/src/track.rs @@ -1,8 +1,9 @@ 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 queue::Queue; use traits::ListItem; @@ -15,10 +16,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 +31,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 +54,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 +77,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 +101,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) diff --git a/src/ui/library.rs b/src/ui/library.rs new file mode 100644 index 0000000..dd68b75 --- /dev/null +++ b/src/ui/library.rs @@ -0,0 +1,79 @@ +use std::sync::Arc; + +use cursive::view::ViewWrapper; +use cursive::views::Dialog; +use cursive::Cursive; + +use commands::CommandResult; +use library::Library; +use queue::Queue; +use traits::ViewExt; +use ui::listview::ListView; +use ui::modal::Modal; +use ui::tabview::TabView; + +pub struct LibraryView { + list: TabView, + library: Arc, +} + +impl LibraryView { + pub fn new(queue: Arc, library: Arc) -> Self { + let tabs = TabView::new() + .tab("tracks", "Tracks", ListView::new(library.tracks.clone(), queue.clone())) + .tab("albums", "Albums", ListView::new(library.albums.clone(), queue.clone())) + .tab("artists", "Artists", ListView::new(library.artists.clone(), queue.clone())) + .tab("playlists", "Playlists", ListView::new(library.playlists.clone(), queue.clone())); + + Self { + list: tabs, + library, + } + } + + pub fn delete_dialog(&mut self) -> Option> { + return None; + + // TODO + //let store = self.library.items(); + //let current = store.get(self.list.get_selected_index()); + + //if let Some(playlist) = current { + // 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| { + // library.delete_playlist(&id); + // s.pop_layer(); + // }); + // Some(Modal::new(dialog)) + //} else { + // None + //} + } +} + +impl ViewWrapper for LibraryView { + wrap_impl!(self.list: TabView); +} + +impl ViewExt for LibraryView { + fn on_command( + &mut self, + s: &mut Cursive, + cmd: &str, + args: &[String], + ) -> Result { + if cmd == "delete" { + if let Some(dialog) = self.delete_dialog() { + s.add_layer(dialog); + } + return Ok(CommandResult::Consumed(None)); + } + + self.list.on_command(s, cmd, args) + } +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 61c0b77..e0f3563 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,7 +1,7 @@ pub mod layout; +pub mod library; pub mod listview; pub mod modal; -pub mod playlists; pub mod queue; pub mod search; pub mod statusbar; diff --git a/src/ui/playlists.rs b/src/ui/playlists.rs deleted file mode 100644 index a53055c..0000000 --- a/src/ui/playlists.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::sync::Arc; - -use cursive::traits::Identifiable; -use cursive::view::ViewWrapper; -use cursive::views::{Dialog, IdView}; -use cursive::Cursive; - -use commands::CommandResult; -use playlists::{Playlist, Playlists}; -use queue::Queue; -use traits::ViewExt; -use ui::listview::ListView; -use ui::modal::Modal; - -pub struct PlaylistView { - list: IdView>, - playlists: Playlists, -} - -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(), - } - } - - 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()); - - if let Some(playlist) = current { - let playlists = self.playlists.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); - s.pop_layer(); - }); - Some(Modal::new(dialog)) - } else { - None - } - } -} - -impl ViewWrapper for PlaylistView { - wrap_impl!(self.list: IdView>); -} - -impl ViewExt for PlaylistView { - fn on_command( - &mut self, - s: &mut Cursive, - cmd: &str, - args: &[String], - ) -> Result { - if cmd == "delete" { - if let Some(dialog) = self.delete_dialog() { - s.add_layer(dialog); - } - return Ok(CommandResult::Consumed(None)); - } - - self.list.on_command(s, cmd, args) - } -} diff --git a/src/ui/queue.rs b/src/ui/queue.rs index ab753ff..352949b 100644 --- a/src/ui/queue.rs +++ b/src/ui/queue.rs @@ -8,7 +8,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 +17,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 { + pub fn new(queue: Arc, library: Arc) -> QueueView { let list = ListView::new(queue.queue.clone(), queue.clone()); QueueView { list, - playlists, + library, queue, } } @@ -35,20 +35,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 +63,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() @@ -92,9 +92,9 @@ impl ViewWrapper for QueueView { Event::Char('s') => { debug!("save list"); let queue = self.queue.clone(); - let playlists = self.playlists.clone(); + let library = self.library.clone(); let cb = move |s: &mut Cursive| { - let dialog = Self::save_dialog(queue.clone(), playlists.clone()); + let dialog = Self::save_dialog(queue.clone(), library.clone()); s.add_layer(dialog) }; EventResult::Consumed(Some(Callback::from_fn(cb))) diff --git a/src/ui/search.rs b/src/ui/search.rs index 0748083..0a76013 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; @@ -218,7 +219,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 +239,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(); From cb32f0ca072ce6e0f592fb3e66a4d6408f9d12ee Mon Sep 17 00:00:00 2001 From: KoffeinFlummi Date: Tue, 16 Apr 2019 19:52:44 +0200 Subject: [PATCH 03/13] Add all tracks to queue when playing --- src/ui/listview.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/ui/listview.rs b/src/ui/listview.rs index d2cc682..11a25aa 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -11,6 +11,7 @@ use unicode_width::UnicodeWidthStr; use commands::CommandResult; use queue::Queue; +use track::Track; use traits::{ListItem, ViewExt}; pub type Paginator = Box>>) + Send + Sync>; @@ -131,6 +132,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 { @@ -264,10 +278,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)); } From d8f3365867231eea44855e17d522d070ec48d5a7 Mon Sep 17 00:00:00 2001 From: KoffeinFlummi Date: Tue, 16 Apr 2019 20:30:04 +0200 Subject: [PATCH 04/13] Don't remove artists added via tracks --- src/library.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/library.rs b/src/library.rs index 03320cf..034733b 100644 --- a/src/library.rs +++ b/src/library.rs @@ -293,13 +293,19 @@ impl Library { } } + let mut store = self.artists.write().unwrap(); + for artist in artists.iter_mut() { + if store.iter().any(|a| &a.id == &artist.id) { + continue; + } + // Only play saved tracks artist.albums = Some(Vec::new()); artist.tracks = Some(Vec::new()); - } - *(self.artists.write().unwrap()) = artists; + store.push(artist.clone()); + } } fn insert_artist(&self, artist: &SimplifiedArtist) { @@ -409,6 +415,10 @@ impl Library { } if let Some(albums) = artist.albums.as_mut() { + if albums.iter().any(|a| a.id == album.id) { + continue; + } + albums.push(album.clone()); } } @@ -435,6 +445,10 @@ impl Library { } if let Some(tracks) = artist.tracks.as_mut() { + if tracks.iter().any(|t| t.id == track.id) { + continue; + } + tracks.push(track.clone()); } } From ab0d3eb9e7dbc1778552be6c479c100cc42fcb12 Mon Sep 17 00:00:00 2001 From: KoffeinFlummi Date: Thu, 18 Apr 2019 13:30:44 +0200 Subject: [PATCH 05/13] Add .log to .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 From d93302a63f8a4fb448594c8eaa8844397da9dd56 Mon Sep 17 00:00:00 2001 From: KoffeinFlummi Date: Thu, 18 Apr 2019 13:31:27 +0200 Subject: [PATCH 06/13] Restore playlist deletion --- src/library.rs | 13 +++++++-- src/ui/library.rs | 46 ++++------------------------- src/ui/mod.rs | 1 + src/ui/playlists.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 43 deletions(-) create mode 100644 src/ui/playlists.rs diff --git a/src/library.rs b/src/library.rs index 034733b..dfc1ade 100644 --- a/src/library.rs +++ b/src/library.rs @@ -190,10 +190,17 @@ impl Library { } pub fn delete_playlist(&self, id: &str) { - let mut store = self.playlists.write().expect("can't writelock playlists"); - if let Some(position) = store.iter().position(|ref i| i.id == id) { + 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) { - store.remove(position); + { + 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()); } } diff --git a/src/ui/library.rs b/src/ui/library.rs index dd68b75..c2605c7 100644 --- a/src/ui/library.rs +++ b/src/ui/library.rs @@ -1,7 +1,6 @@ use std::sync::Arc; use cursive::view::ViewWrapper; -use cursive::views::Dialog; use cursive::Cursive; use commands::CommandResult; @@ -9,12 +8,11 @@ use library::Library; use queue::Queue; use traits::ViewExt; use ui::listview::ListView; -use ui::modal::Modal; +use ui::playlists::PlaylistsView; use ui::tabview::TabView; pub struct LibraryView { - list: TabView, - library: Arc, + tabs: TabView, } impl LibraryView { @@ -23,41 +21,16 @@ impl LibraryView { .tab("tracks", "Tracks", ListView::new(library.tracks.clone(), queue.clone())) .tab("albums", "Albums", ListView::new(library.albums.clone(), queue.clone())) .tab("artists", "Artists", ListView::new(library.artists.clone(), queue.clone())) - .tab("playlists", "Playlists", ListView::new(library.playlists.clone(), queue.clone())); + .tab("playlists", "Playlists", PlaylistsView::new(queue.clone(), library.clone())); Self { - list: tabs, - library, + tabs } } - - pub fn delete_dialog(&mut self) -> Option> { - return None; - - // TODO - //let store = self.library.items(); - //let current = store.get(self.list.get_selected_index()); - - //if let Some(playlist) = current { - // 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| { - // library.delete_playlist(&id); - // s.pop_layer(); - // }); - // Some(Modal::new(dialog)) - //} else { - // None - //} - } } impl ViewWrapper for LibraryView { - wrap_impl!(self.list: TabView); + wrap_impl!(self.tabs: TabView); } impl ViewExt for LibraryView { @@ -67,13 +40,6 @@ impl ViewExt for LibraryView { cmd: &str, args: &[String], ) -> Result { - if cmd == "delete" { - if let Some(dialog) = self.delete_dialog() { - s.add_layer(dialog); - } - return Ok(CommandResult::Consumed(None)); - } - - self.list.on_command(s, cmd, args) + self.tabs.on_command(s, cmd, args) } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index e0f3563..b6088d3 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -2,6 +2,7 @@ pub mod layout; pub mod library; pub mod listview; pub mod modal; +pub mod playlists; pub mod queue; pub mod search; pub mod statusbar; diff --git a/src/ui/playlists.rs b/src/ui/playlists.rs new file mode 100644 index 0000000..afbb27e --- /dev/null +++ b/src/ui/playlists.rs @@ -0,0 +1,70 @@ +use std::sync::Arc; + +use cursive::view::ViewWrapper; +use cursive::views::Dialog; +use cursive::Cursive; + +use commands::CommandResult; +use library::Library; +use playlist::Playlist; +use queue::Queue; +use traits::ViewExt; +use ui::listview::ListView; +use ui::modal::Modal; + +pub struct PlaylistsView { + list: ListView, + library: Arc, +} + +impl PlaylistsView { + pub fn new(queue: Arc, library: Arc) -> Self { + Self { + list: ListView::new(library.playlists.clone(), queue.clone()), + library, + } + } + + pub fn delete_dialog(&mut self) -> Option> { + let store = self.library.items(); + let current = store.get(self.list.get_selected_index()); + + if let Some(playlist) = current { + 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| { + library.delete_playlist(&id); + s.pop_layer(); + }); + Some(Modal::new(dialog)) + } else { + None + } + } +} + +impl ViewWrapper for PlaylistsView { + wrap_impl!(self.list: ListView); +} + +impl ViewExt for PlaylistsView { + fn on_command( + &mut self, + s: &mut Cursive, + cmd: &str, + args: &[String], + ) -> Result { + if cmd == "delete" { + if let Some(dialog) = self.delete_dialog() { + s.add_layer(dialog); + } + return Ok(CommandResult::Consumed(None)); + } + + self.list.on_command(s, cmd, args) + } +} From adba8093271478611d82a87072b920cecfd77041 Mon Sep 17 00:00:00 2001 From: KoffeinFlummi Date: Thu, 18 Apr 2019 14:04:40 +0200 Subject: [PATCH 07/13] Display saved/followed checkmark --- src/album.rs | 14 ++++++++++++-- src/artist.rs | 25 ++++++++++++++++++++----- src/library.rs | 29 +++++++++++++++++++++++++++-- src/main.rs | 9 +++++++-- src/playlist.rs | 14 ++++++++++++-- src/track.rs | 14 ++++++++++++-- src/traits.rs | 3 ++- src/ui/library.rs | 6 +++--- src/ui/listview.rs | 7 +++++-- src/ui/playlists.rs | 2 +- src/ui/queue.rs | 2 +- src/ui/search.rs | 15 ++++++++++----- 12 files changed, 112 insertions(+), 28 deletions(-) diff --git a/src/album.rs b/src/album.rs index 54d7124..eae1110 100644 --- a/src/album.rs +++ b/src/album.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use chrono::{DateTime, Utc}; use rspotify::spotify::model::album::{FullAlbum, SavedAlbum, SimplifiedAlbum}; +use library::Library; use queue::Queue; use spotify::Spotify; use track::Track; @@ -127,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) { diff --git a/src/artist.rs b/src/artist.rs index 4fe16b0..511c2c7 100644 --- a/src/artist.rs +++ b/src/artist.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use rspotify::spotify::model::artist::{FullArtist, SimplifiedArtist}; use album::Album; +use library::Library; use queue::Queue; use spotify::Spotify; use track::Track; @@ -16,6 +17,7 @@ pub struct Artist { pub url: String, pub albums: Option>, pub tracks: Option>, + pub is_followed: bool, } impl Artist { @@ -69,6 +71,7 @@ impl From<&SimplifiedArtist> for Artist { url: sa.uri.clone(), albums: None, tracks: None, + is_followed: false, } } } @@ -81,6 +84,7 @@ impl From<&FullArtist> for Artist { url: fa.uri.clone(), albums: None, tracks: None, + is_followed: false, } } } @@ -118,13 +122,24 @@ impl ListItem for Artist { format!("{}", self) } - fn display_right(&self) -> String { - // TODO: indicate following status - if let Some(tracks) = self.tracks.as_ref() { - format!("{} saved tracks", tracks.len()) + 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) { diff --git a/src/library.rs b/src/library.rs index dfc1ade..73e68fe 100644 --- a/src/library.rs +++ b/src/library.rs @@ -31,10 +31,11 @@ pub struct Library { pub playlists: Arc>>, ev: EventManager, spotify: Arc, + pub use_nerdfont: bool, } impl Library { - pub fn new(ev: &EventManager, spotify: Arc) -> Self { + 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())), @@ -42,6 +43,7 @@ impl Library { playlists: Arc::new(RwLock::new(Vec::new())), ev: ev.clone(), spotify, + use_nerdfont, }; { @@ -303,13 +305,16 @@ impl Library { let mut store = self.artists.write().unwrap(); for artist in artists.iter_mut() { - if store.iter().any(|a| &a.id == &artist.id) { + let pos = store.iter().position(|a| &a.id == &artist.id); + if let Some(i) = pos { + store[i].is_followed = true; continue; } // Only play saved tracks artist.albums = Some(Vec::new()); artist.tracks = Some(Vec::new()); + artist.is_followed = true; store.push(artist.clone()); } @@ -463,4 +468,24 @@ impl Library { } } } + + pub fn is_saved_track(&self, track: &Track) -> bool { + let tracks = self.tracks.read().unwrap(); + tracks.iter().any(|t| t.id == track.id) + } + + 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 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 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/main.rs b/src/main.rs index 41a7322..958c1cb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,7 +156,7 @@ fn main() { #[cfg(feature = "mpris")] let mpris_manager = Arc::new(mpris::MprisManager::new(spotify.clone(), queue.clone())); - let library = Arc::new(Library::new(&event_manager, spotify.clone())); + 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(), library.clone()); @@ -168,7 +168,12 @@ 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 libraryview = ui::library::LibraryView::new(queue.clone(), library.clone()); diff --git a/src/playlist.rs b/src/playlist.rs index e17b762..4c90aa2 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -2,6 +2,7 @@ use std::iter::Iterator; use std::sync::Arc; use queue::Queue; +use library::Library; use track::Track; use traits::ListItem; @@ -30,8 +31,17 @@ impl ListItem for Playlist { self.name.clone() } - fn display_right(&self) -> String { - format!("{} tracks", self.tracks.len()) + 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) { diff --git a/src/track.rs b/src/track.rs index 85c202f..9d188ba 100644 --- a/src/track.rs +++ b/src/track.rs @@ -5,6 +5,7 @@ use chrono::{DateTime, Utc}; use rspotify::spotify::model::album::FullAlbum; use rspotify::spotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack}; +use library::Library; use queue::Queue; use traits::ListItem; @@ -147,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) { diff --git a/src/traits.rs b/src/traits.rs index c7791b1..d851a32 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -5,12 +5,13 @@ 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); } diff --git a/src/ui/library.rs b/src/ui/library.rs index c2605c7..4c98838 100644 --- a/src/ui/library.rs +++ b/src/ui/library.rs @@ -18,9 +18,9 @@ pub struct LibraryView { impl LibraryView { pub fn new(queue: Arc, library: Arc) -> Self { let tabs = TabView::new() - .tab("tracks", "Tracks", ListView::new(library.tracks.clone(), queue.clone())) - .tab("albums", "Albums", ListView::new(library.albums.clone(), queue.clone())) - .tab("artists", "Artists", ListView::new(library.artists.clone(), queue.clone())) + .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 { diff --git a/src/ui/listview.rs b/src/ui/listview.rs index 11a25aa..afd76a4 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -10,6 +10,7 @@ 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}; @@ -84,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, @@ -96,6 +98,7 @@ impl ListView { last_size: Vec2::new(0, 0), scrollbar: ScrollBase::new(), queue, + library, pagination: Pagination::default(), } } @@ -174,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| { diff --git a/src/ui/playlists.rs b/src/ui/playlists.rs index afbb27e..2912b01 100644 --- a/src/ui/playlists.rs +++ b/src/ui/playlists.rs @@ -20,7 +20,7 @@ pub struct PlaylistsView { impl PlaylistsView { pub fn new(queue: Arc, library: Arc) -> Self { Self { - list: ListView::new(library.playlists.clone(), queue.clone()), + list: ListView::new(library.playlists.clone(), queue.clone(), library.clone()), library, } } diff --git a/src/ui/queue.rs b/src/ui/queue.rs index 352949b..0ec38b7 100644 --- a/src/ui/queue.rs +++ b/src/ui/queue.rs @@ -23,7 +23,7 @@ pub struct QueueView { impl QueueView { pub fn new(queue: Arc, library: Arc) -> QueueView { - let list = ListView::new(queue.queue.clone(), queue.clone()); + let list = ListView::new(queue.queue.clone(), queue.clone(), library.clone()); QueueView { list, diff --git a/src/ui/search.rs b/src/ui/search.rs index 0a76013..1ff7fd9 100644 --- a/src/ui/search.rs +++ b/src/ui/search.rs @@ -44,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())); @@ -61,13 +66,13 @@ 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() From 4c974a83f7fc3045d17ff7137017846718172675 Mon Sep 17 00:00:00 2001 From: KoffeinFlummi Date: Thu, 18 Apr 2019 15:43:04 +0200 Subject: [PATCH 08/13] Use command system for queue saving --- src/commands.rs | 3 +++ src/ui/queue.rs | 25 +++++++------------------ 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 3b02055..736b660 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -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( @@ -293,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()); diff --git a/src/ui/queue.rs b/src/ui/queue.rs index 0ec38b7..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; @@ -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 library = self.library.clone(); - let cb = move |s: &mut Cursive| { - let dialog = Self::save_dialog(queue.clone(), library.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() } From 1e58ca9345bffcefbb6010b986016eb64c71f20b Mon Sep 17 00:00:00 2001 From: KoffeinFlummi Date: Thu, 18 Apr 2019 15:43:51 +0200 Subject: [PATCH 09/13] Implement (un)saving tracks, albums; (un)following artists --- Cargo.toml | 6 +- src/album.rs | 8 ++ src/artist.rs | 8 ++ src/library.rs | 186 +++++++++++++++++++++++++++++++++++++++++---- src/playlist.rs | 4 + src/spotify.rs | 24 ++++++ src/track.rs | 8 ++ src/traits.rs | 1 + src/ui/listview.rs | 13 +++- 9 files changed, 241 insertions(+), 17 deletions(-) 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 From 30447153629356b5f2b7e5f7b5dd1c010be9736b Mon Sep 17 00:00:00 2001 From: KoffeinFlummi Date: Fri, 19 Apr 2019 22:49:30 +0200 Subject: [PATCH 10/13] Lock library until initialization is done --- src/library.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/library.rs b/src/library.rs index 21fb171..c648f49 100644 --- a/src/library.rs +++ b/src/library.rs @@ -28,6 +28,7 @@ pub struct Library { pub albums: Arc>>, pub artists: Arc>>, pub playlists: Arc>>, + is_done: Arc>, ev: EventManager, spotify: Arc, pub use_nerdfont: bool, @@ -40,6 +41,7 @@ impl Library { 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, @@ -61,6 +63,8 @@ impl Library { // re-cache for next startup library.save_caches(); + let mut is_done = library.is_done.write().unwrap(); + *is_done = true; }); } @@ -498,11 +502,19 @@ impl Library { } 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 { self.spotify.current_user_saved_tracks_add( tracks @@ -532,6 +544,10 @@ impl Library { } pub fn unsave_tracks(&self, tracks: Vec<&Track>, api: bool) { + if !*self.is_done.read().unwrap() { + return; + } + if api { self.spotify.current_user_saved_tracks_delete( tracks @@ -557,11 +573,19 @@ impl Library { } 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; + } + self.spotify.current_user_saved_albums_add(vec![album.id.clone()]); album.load_tracks(self.spotify.clone()); @@ -581,6 +605,10 @@ impl Library { } pub fn unsave_album(&self, album: &mut Album) { + if !*self.is_done.read().unwrap() { + return; + } + self.spotify.current_user_saved_albums_delete(vec![album.id.clone()]); album.load_tracks(self.spotify.clone()); @@ -602,11 +630,19 @@ impl Library { } 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; + } + self.spotify.user_follow_artists(vec![artist.id.clone()]); { @@ -626,6 +662,10 @@ impl Library { } pub fn unfollow_artist(&self, artist: &Artist) { + if !*self.is_done.read().unwrap() { + return; + } + self.spotify.user_unfollow_artists(vec![artist.id.clone()]); { @@ -641,6 +681,10 @@ impl Library { } 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) } From e68ba601794fc5b75f60e362cbbde613d42e186c Mon Sep 17 00:00:00 2001 From: KoffeinFlummi Date: Fri, 19 Apr 2019 22:50:12 +0200 Subject: [PATCH 11/13] Optimize/parallelize library initialization --- src/library.rs | 140 +++++++++++++++++++++++++------------------------ 1 file changed, 71 insertions(+), 69 deletions(-) diff --git a/src/library.rs b/src/library.rs index c648f49..d8013d9 100644 --- a/src/library.rs +++ b/src/library.rs @@ -48,21 +48,52 @@ impl Library { }; { - // download playlists via web api in a background thread let library = library.clone(); thread::spawn(move || { - // load cache (if existing) - library.load_caches(); + 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()); + }) + }; - library.fetch_artists(); - library.fetch_tracks(); - library.fetch_albums(); - library.fetch_playlists(); + 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(); - // re-cache for next startup - library.save_caches(); let mut is_done = library.is_done.write().unwrap(); *is_done = true; }); @@ -98,13 +129,6 @@ impl Library { } } - fn load_caches(&self) { - self.load_cache(config::cache_path(CACHE_TRACKS), self.tracks.clone()); - self.load_cache(config::cache_path(CACHE_ALBUMS), self.albums.clone()); - self.load_cache(config::cache_path(CACHE_ARTISTS), self.artists.clone()); - self.load_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone()); - } - 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(), @@ -112,13 +136,6 @@ impl Library { } } - fn save_caches(&self) { - self.save_cache(config::cache_path(CACHE_TRACKS), self.tracks.clone()); - self.save_cache(config::cache_path(CACHE_ALBUMS), self.albums.clone()); - self.save_cache(config::cache_path(CACHE_ARTISTS), self.artists.clone()); - self.save_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone()); - } - pub fn process_simplified_playlist(list: &SimplifiedPlaylist, spotify: &Spotify) -> Playlist { Self::_process_playlist( list.id.clone(), @@ -320,24 +337,18 @@ impl Library { } } - fn insert_artist(&self, track: &Track) { + fn insert_artist(&self, id: &String, name: &String) { let mut artists = self.artists.write().unwrap(); - for (id, name) in track.artist_ids.iter().zip(track.artists.iter()) { - let artist = Artist { + if !artists.iter().any(|a| &a.id == id) { + artists.push(Artist { id: id.clone(), name: name.clone(), url: "".into(), - albums: Some(Vec::new()), + albums: None, tracks: Some(Vec::new()), is_followed: false, - }; - - if artists.iter().any(|a| a.id == artist.id) { - continue; - } - - artists.push(artist.into()); + }); } } @@ -356,6 +367,22 @@ impl Library { } 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() { @@ -420,55 +447,30 @@ impl Library { .collect(); } - // Add artists that aren't followed but have saved tracks/albums + // Add artists that aren't followed but have saved tracks { let tracks = self.tracks.read().unwrap(); - for track in tracks.iter() { - self.insert_artist(track); + 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.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() { - for artist_id in &album.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.albums.is_none() { - artist.albums = Some(Vec::new()); - } - - if let Some(albums) = artist.albums.as_mut() { - if albums.iter().any(|a| a.id == album.id) { - continue; - } - - albums.push(album.clone()); - } - } - } - } - } - // Add saved tracks to artists { let tracks = self.tracks.read().unwrap(); From f320b953d6c2a8d6e9fbb91135ceab3bdf743d2d Mon Sep 17 00:00:00 2001 From: KoffeinFlummi Date: Fri, 19 Apr 2019 23:50:36 +0200 Subject: [PATCH 12/13] Implement following playlists --- src/library.rs | 62 ++++++++++++++++++++++++++++++++++++++++--------- src/playlist.rs | 9 +++++-- src/spotify.rs | 4 ++++ 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/src/library.rs b/src/library.rs index d8013d9..7ea5a08 100644 --- a/src/library.rs +++ b/src/library.rs @@ -140,6 +140,7 @@ impl Library { Self::_process_playlist( list.id.clone(), list.name.clone(), + list.owner.id.clone(), list.snapshot_id.clone(), spotify, ) @@ -149,6 +150,7 @@ impl Library { Self::_process_playlist( list.id.clone(), list.name.clone(), + list.owner.id.clone(), list.snapshot_id.clone(), spotify, ) @@ -157,6 +159,7 @@ impl Library { fn _process_playlist( id: String, name: String, + owner_id: String, snapshot_id: String, spotify: &Spotify, ) -> Playlist { @@ -182,10 +185,12 @@ impl Library { None => None, } } + Playlist { - id: id.clone(), - name: name.clone(), - snapshot_id: snapshot_id.clone(), + id, + name, + owner_id, + snapshot_id, tracks: collected_tracks, } } @@ -212,6 +217,10 @@ impl Library { } 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) @@ -518,12 +527,14 @@ impl Library { } if api { - self.spotify.current_user_saved_tracks_add( + if self.spotify.current_user_saved_tracks_add( tracks .iter() .map(|t| t.id.clone()) .collect() - ); + ).is_none() { + return; + } } { @@ -551,12 +562,14 @@ impl Library { } if api { - self.spotify.current_user_saved_tracks_delete( + if self.spotify.current_user_saved_tracks_delete( tracks .iter() .map(|t| t.id.clone()) .collect() - ); + ).is_none() { + return; + } } { @@ -588,7 +601,9 @@ impl Library { return; } - self.spotify.current_user_saved_albums_add(vec![album.id.clone()]); + if self.spotify.current_user_saved_albums_add(vec![album.id.clone()]).is_none() { + return; + } album.load_tracks(self.spotify.clone()); @@ -611,7 +626,9 @@ impl Library { return; } - self.spotify.current_user_saved_albums_delete(vec![album.id.clone()]); + if self.spotify.current_user_saved_albums_delete(vec![album.id.clone()]).is_none() { + return; + } album.load_tracks(self.spotify.clone()); @@ -645,7 +662,9 @@ impl Library { return; } - self.spotify.user_follow_artists(vec![artist.id.clone()]); + if self.spotify.user_follow_artists(vec![artist.id.clone()]).is_none() { + return; + } { let mut store = self.artists.write().unwrap(); @@ -668,7 +687,9 @@ impl Library { return; } - self.spotify.user_unfollow_artists(vec![artist.id.clone()]); + if self.spotify.user_unfollow_artists(vec![artist.id.clone()]).is_none() { + return; + } { let mut store = self.artists.write().unwrap(); @@ -690,4 +711,23 @@ impl Library { 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/playlist.rs b/src/playlist.rs index c7821a9..2902fab 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -10,6 +10,7 @@ use traits::ListItem; pub struct Playlist { pub id: String, pub name: String, + pub owner_id: String, pub snapshot_id: String, pub tracks: Vec, } @@ -55,7 +56,11 @@ impl ListItem for Playlist { } } - fn toggle_saved(&mut self, _library: Arc) { - // TODO + 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/spotify.rs b/src/spotify.rs index a8cdf08..16948ec 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -564,6 +564,10 @@ impl Spotify { 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 From 1a178609573db38659e4a2a8514b1c5fc6ccf2fd Mon Sep 17 00:00:00 2001 From: KoffeinFlummi Date: Sat, 20 Apr 2019 00:11:43 +0200 Subject: [PATCH 13/13] cargo fmt --- src/album.rs | 2 +- src/library.rs | 112 +++++++++++++++++++++++++++++----------------- src/main.rs | 8 +++- src/playlist.rs | 2 +- src/spotify.rs | 5 ++- src/track.rs | 2 +- src/ui/library.rs | 28 +++++++++--- src/ui/search.rs | 5 ++- 8 files changed, 109 insertions(+), 55 deletions(-) diff --git a/src/album.rs b/src/album.rs index f99868b..f10cd89 100644 --- a/src/album.rs +++ b/src/album.rs @@ -20,7 +20,7 @@ pub struct Album { pub cover_url: Option, pub url: String, pub tracks: Option>, - pub added_at: Option> + pub added_at: Option>, } impl Album { diff --git a/src/library.rs b/src/library.rs index 7ea5a08..13ccccb 100644 --- a/src/library.rs +++ b/src/library.rs @@ -53,25 +53,30 @@ impl Library { let t_tracks = { let library = library.clone(); thread::spawn(move || { - library.load_cache(config::cache_path(CACHE_TRACKS), library.tracks.clone()); + 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()); + 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 + .load_cache(config::cache_path(CACHE_ALBUMS), library.albums.clone()); library.fetch_albums(); - library.save_cache(config::cache_path(CACHE_ALBUMS), library.albums.clone()); + 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 + .load_cache(config::cache_path(CACHE_ARTISTS), library.artists.clone()); library.fetch_artists(); }) }; @@ -79,9 +84,15 @@ impl Library { let t_playlists = { let library = library.clone(); thread::spawn(move || { - library.load_cache(config::cache_path(CACHE_PLAYLISTS), library.playlists.clone()); + 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()); + library.save_cache( + config::cache_path(CACHE_PLAYLISTS), + library.playlists.clone(), + ); }) }; @@ -114,7 +125,11 @@ impl Library { let parsed: Result, _> = serde_json::from_str(&contents); match parsed { Ok(cache) => { - debug!("cache from {} loaded ({} lists)", cache_path.display(), cache.len()); + 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); @@ -196,7 +211,12 @@ impl Library { } fn needs_download(&self, remote: &SimplifiedPlaylist) -> bool { - for local in self.playlists.read().expect("can't readlock playlists").iter() { + for local in self + .playlists + .read() + .expect("can't readlock playlists") + .iter() + { if local.id == remote.id { return local.snapshot_id != remote.snapshot_id; } @@ -382,8 +402,9 @@ impl Library { let store = self.albums.read().unwrap(); - if page.total as usize == store.len() && - !page.items + if page.total as usize == store.len() + && !page + .items .iter() .enumerate() .any(|(i, a)| &a.album.id != &store[i].id) @@ -425,8 +446,9 @@ impl Library { let store = self.tracks.read().unwrap(); - if page.total as usize == store.len() && - !page.items + if page.total as usize == store.len() + && !page + .items .iter() .enumerate() .any(|(i, t)| &t.track.id != &store[i].id) @@ -449,11 +471,7 @@ impl Library { // Remove old unfollowed artists { let mut artists = self.artists.write().unwrap(); - *artists = artists - .iter() - .filter(|a| a.is_followed) - .cloned() - .collect(); + *artists = artists.iter().filter(|a| a.is_followed).cloned().collect(); } // Add artists that aren't followed but have saved tracks @@ -527,12 +545,11 @@ impl Library { } if api { - if self.spotify.current_user_saved_tracks_add( - tracks - .iter() - .map(|t| t.id.clone()) - .collect() - ).is_none() { + if self + .spotify + .current_user_saved_tracks_add(tracks.iter().map(|t| t.id.clone()).collect()) + .is_none() + { return; } } @@ -562,12 +579,11 @@ impl Library { } if api { - if self.spotify.current_user_saved_tracks_delete( - tracks - .iter() - .map(|t| t.id.clone()) - .collect() - ).is_none() { + if self + .spotify + .current_user_saved_tracks_delete(tracks.iter().map(|t| t.id.clone()).collect()) + .is_none() + { return; } } @@ -601,7 +617,11 @@ impl Library { return; } - if self.spotify.current_user_saved_albums_add(vec![album.id.clone()]).is_none() { + if self + .spotify + .current_user_saved_albums_add(vec![album.id.clone()]) + .is_none() + { return; } @@ -626,7 +646,11 @@ impl Library { return; } - if self.spotify.current_user_saved_albums_delete(vec![album.id.clone()]).is_none() { + if self + .spotify + .current_user_saved_albums_delete(vec![album.id.clone()]) + .is_none() + { return; } @@ -634,11 +658,7 @@ impl Library { { let mut store = self.albums.write().unwrap(); - *store = store - .iter() - .filter(|a| a.id != album.id) - .cloned() - .collect(); + *store = store.iter().filter(|a| a.id != album.id).cloned().collect(); } if let Some(tracks) = album.tracks.as_ref() { @@ -662,7 +682,11 @@ impl Library { return; } - if self.spotify.user_follow_artists(vec![artist.id.clone()]).is_none() { + if self + .spotify + .user_follow_artists(vec![artist.id.clone()]) + .is_none() + { return; } @@ -687,7 +711,11 @@ impl Library { return; } - if self.spotify.user_unfollow_artists(vec![artist.id.clone()]).is_none() { + if self + .spotify + .user_unfollow_artists(vec![artist.id.clone()]) + .is_none() + { return; } @@ -717,7 +745,11 @@ impl Library { return; } - if self.spotify.user_playlist_follow_playlist(playlist.owner_id.clone(), playlist.id.clone()).is_none() { + if self + .spotify + .user_playlist_follow_playlist(playlist.owner_id.clone(), playlist.id.clone()) + .is_none() + { return; } diff --git a/src/main.rs b/src/main.rs index 958c1cb..8819401 100644 --- a/src/main.rs +++ b/src/main.rs @@ -156,7 +156,11 @@ fn main() { #[cfg(feature = "mpris")] let mpris_manager = Arc::new(mpris::MprisManager::new(spotify.clone(), queue.clone())); - let library = Arc::new(Library::new(&event_manager, spotify.clone(), cfg.use_nerdfont.unwrap_or(false))); + 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(), library.clone()); @@ -172,7 +176,7 @@ fn main() { event_manager.clone(), spotify.clone(), queue.clone(), - library.clone() + library.clone(), ); let libraryview = ui::library::LibraryView::new(queue.clone(), library.clone()); diff --git a/src/playlist.rs b/src/playlist.rs index 2902fab..ed217f6 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -1,8 +1,8 @@ use std::iter::Iterator; use std::sync::Arc; -use queue::Queue; use library::Library; +use queue::Queue; use track::Track; use traits::ListItem; diff --git a/src/spotify.rs b/src/spotify.rs index 16948ec..ff593b8 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -527,7 +527,10 @@ impl Spotify { }) } - pub fn current_user_followed_artists(&self, last: Option) -> Option> { + 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) } diff --git a/src/track.rs b/src/track.rs index bf745f2..2ceef0a 100644 --- a/src/track.rs +++ b/src/track.rs @@ -22,7 +22,7 @@ pub struct Track { pub album_artists: Vec, pub cover_url: String, pub url: String, - pub added_at: Option> + pub added_at: Option>, } impl Track { diff --git a/src/ui/library.rs b/src/ui/library.rs index 4c98838..c3ab05c 100644 --- a/src/ui/library.rs +++ b/src/ui/library.rs @@ -18,14 +18,28 @@ pub struct LibraryView { 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())); + .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 - } + Self { tabs } } } diff --git a/src/ui/search.rs b/src/ui/search.rs index 1ff7fd9..f0818b3 100644 --- a/src/ui/search.rs +++ b/src/ui/search.rs @@ -48,7 +48,7 @@ impl SearchView { events: EventManager, spotify: Arc, queue: Arc, - library: Arc + library: Arc, ) -> SearchView { let results_tracks = Arc::new(RwLock::new(Vec::new())); let results_albums = Arc::new(RwLock::new(Vec::new())); @@ -72,7 +72,8 @@ impl SearchView { let pagination_albums = list_albums.get_pagination().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(), library.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()