diff --git a/Cargo.toml b/Cargo.toml index bd10df6..6750d87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ webbrowser = "0.5" [dependencies.rspotify] git = "https://github.com/samrayleung/rspotify" -rev = "9d9cf7c" +rev = "8f8dc17" [dependencies.librespot] git = "https://github.com/librespot-org/librespot.git" diff --git a/src/commands.rs b/src/commands.rs index 0f94dd2..819fc4f 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::sync::Arc; +use std::time::Duration; use cursive::event::{Event, Key}; use cursive::views::ViewRef; @@ -85,10 +86,15 @@ impl CommandManager { { let queue = queue.clone(); + let spotify = spotify.clone(); self.register_command( "previous", Some(Box::new(move |_s, _args| { - queue.previous(); + if spotify.get_current_progress() < Duration::from_secs(5) { + queue.previous(); + } else { + spotify.seek(0); + } Ok(None) })), ); diff --git a/src/library.rs b/src/library.rs index 8809dfe..8d405fd 100644 --- a/src/library.rs +++ b/src/library.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; use std::sync::{Arc, RwLock, RwLockReadGuard}; use std::thread; -use rspotify::spotify::model::playlist::{FullPlaylist, SimplifiedPlaylist}; +use rspotify::spotify::model::playlist::SimplifiedPlaylist; use serde::de::DeserializeOwned; use serde::Serialize; @@ -29,6 +29,7 @@ pub struct Library { pub artists: Arc>>, pub playlists: Arc>>, is_done: Arc>, + user_id: Option, ev: EventManager, spotify: Arc, pub use_nerdfont: bool, @@ -36,12 +37,15 @@ pub struct Library { impl Library { pub fn new(ev: &EventManager, spotify: Arc, use_nerdfont: bool) -> Self { + let user_id = spotify.current_user().map(|u| u.id); + let library = Self { tracks: Arc::new(RwLock::new(Vec::new())), albums: Arc::new(RwLock::new(Vec::new())), artists: Arc::new(RwLock::new(Vec::new())), playlists: Arc::new(RwLock::new(Vec::new())), is_done: Arc::new(RwLock::new(false)), + user_id, ev: ev.clone(), spotify, use_nerdfont, @@ -107,6 +111,8 @@ impl Library { let mut is_done = library.is_done.write().unwrap(); *is_done = true; + + library.ev.trigger(); }); } @@ -151,65 +157,6 @@ impl Library { } } - pub fn process_simplified_playlist(list: &SimplifiedPlaylist, spotify: &Spotify) -> Playlist { - Self::_process_playlist( - list.id.clone(), - list.name.clone(), - list.owner.id.clone(), - list.snapshot_id.clone(), - spotify, - ) - } - - pub fn process_full_playlist(list: &FullPlaylist, spotify: &Spotify) -> Playlist { - Self::_process_playlist( - list.id.clone(), - list.name.clone(), - list.owner.id.clone(), - list.snapshot_id.clone(), - spotify, - ) - } - - fn _process_playlist( - id: String, - name: String, - owner_id: String, - snapshot_id: String, - spotify: &Spotify, - ) -> Playlist { - let mut collected_tracks = Vec::new(); - - let mut tracks_result = spotify.user_playlist_tracks(&id, 100, 0); - while let Some(ref tracks) = tracks_result.clone() { - for listtrack in &tracks.items { - collected_tracks.push((&listtrack.track).into()); - } - debug!("got {} tracks", tracks.items.len()); - - // load next batch if necessary - tracks_result = match tracks.next { - Some(_) => { - debug!("requesting tracks again.."); - spotify.user_playlist_tracks( - &id, - 100, - tracks.offset + tracks.items.len() as u32, - ) - } - None => None, - } - } - - Playlist { - id, - name, - owner_id, - snapshot_id, - tracks: collected_tracks, - } - } - fn needs_download(&self, remote: &SimplifiedPlaylist) -> bool { for local in self .playlists @@ -291,7 +238,8 @@ impl Library { if self.needs_download(remote) { info!("updating playlist {}", remote.name); - let playlist = Self::process_simplified_playlist(remote, &self.spotify); + let mut playlist: Playlist = remote.into(); + playlist.load_tracks(self.spotify.clone()); self.append_or_update(&playlist); // trigger redraw self.ev.trigger(); @@ -758,6 +706,13 @@ impl Library { playlists.iter().any(|p| p.id == playlist.id) } + pub fn is_followed_playlist(&self, playlist: &Playlist) -> bool { + self.user_id + .as_ref() + .map(|id| id != &playlist.owner_id) + .unwrap_or(false) + } + pub fn follow_playlist(&self, playlist: &Playlist) { if !*self.is_done.read().unwrap() { return; @@ -771,6 +726,9 @@ impl Library { return; } + let mut playlist = playlist.clone(); + playlist.load_tracks(self.spotify.clone()); + { let mut store = self.playlists.write().unwrap(); if !store.iter().any(|p| p.id == playlist.id) { diff --git a/src/main.rs b/src/main.rs index 8819401..78c35f4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -185,7 +185,7 @@ fn main() { let status = ui::statusbar::StatusBar::new( queue.clone(), - spotify.clone(), + library.clone(), cfg.use_nerdfont.unwrap_or(false), ); @@ -194,8 +194,8 @@ fn main() { .view("library", libraryview.with_id("library"), "Library") .view("queue", queueview, "Queue"); - // initial view is queue - layout.set_view("queue"); + // initial view is library + layout.set_view("library"); cursive.add_global_callback(':', move |s| { s.call_on_id("main", |v: &mut ui::layout::Layout| { diff --git a/src/playlist.rs b/src/playlist.rs index 02dddc0..bd32498 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -1,8 +1,11 @@ use std::iter::Iterator; use std::sync::Arc; +use rspotify::spotify::model::playlist::{FullPlaylist, SimplifiedPlaylist}; + use library::Library; use queue::Queue; +use spotify::Spotify; use track::Track; use traits::{IntoBoxedViewExt, ListItem, ViewExt}; use ui::playlist::PlaylistView; @@ -13,26 +16,95 @@ pub struct Playlist { pub name: String, pub owner_id: String, pub snapshot_id: String, - pub tracks: Vec, + pub num_tracks: usize, + pub tracks: Option>, +} + +impl Playlist { + pub fn load_tracks(&mut self, spotify: Arc) { + if self.tracks.is_some() { + return; + } + + let mut collected_tracks = Vec::new(); + + let mut tracks_result = spotify.user_playlist_tracks(&self.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( + &self.id, + 100, + tracks.offset + tracks.items.len() as u32, + ) + } + None => None, + } + } + + self.tracks = Some(collected_tracks); + } +} + +impl From<&SimplifiedPlaylist> for Playlist { + fn from(list: &SimplifiedPlaylist) -> Self { + let num_tracks = if let Some(number) = list.tracks.get("total") { + number.as_u64().unwrap() as usize + } else { + 0 + }; + + Playlist { + id: list.id.clone(), + name: list.name.clone(), + owner_id: list.owner.id.clone(), + snapshot_id: list.snapshot_id.clone(), + num_tracks, + tracks: None, + } + } +} + +impl From<&FullPlaylist> for Playlist { + fn from(list: &FullPlaylist) -> Self { + Playlist { + id: list.id.clone(), + name: list.name.clone(), + owner_id: list.owner.id.clone(), + snapshot_id: list.snapshot_id.clone(), + num_tracks: list.tracks.total as usize, + tracks: None, + } + } } impl ListItem for Playlist { fn is_playing(&self, queue: Arc) -> bool { - let playing: Vec = queue - .queue - .read() - .unwrap() - .iter() - .filter(|t| t.id.is_some()) - .map(|t| t.id.clone().unwrap()) - .collect(); - let ids: Vec = self - .tracks - .iter() - .filter(|t| t.id.is_some()) - .map(|t| t.id.clone().unwrap()) - .collect(); - !ids.is_empty() && playing == ids + if let Some(tracks) = self.tracks.as_ref() { + let playing: Vec = queue + .queue + .read() + .unwrap() + .iter() + .filter(|t| t.id.is_some()) + .map(|t| t.id.clone().unwrap()) + .collect(); + let ids: Vec = tracks + .iter() + .filter(|t| t.id.is_some()) + .map(|t| t.id.clone().unwrap()) + .collect(); + !ids.is_empty() && playing == ids + } else { + false + } } fn display_left(&self) -> String { @@ -40,7 +112,7 @@ impl ListItem for Playlist { } fn display_right(&self, library: Arc) -> String { - let saved = if library.is_saved_playlist(self) { + let followed = if library.is_followed_playlist(self) { if library.use_nerdfont { "\u{f62b} " } else { @@ -49,21 +121,41 @@ impl ListItem for Playlist { } else { "" }; - format!("{}{:>3} tracks", saved, self.tracks.len()) + + let num_tracks = self + .tracks + .as_ref() + .map(|t| t.len()) + .unwrap_or(self.num_tracks); + + format!("{}{:>4} tracks", followed, num_tracks) } fn play(&mut self, queue: Arc) { - let index = queue.append_next(self.tracks.iter().collect()); - queue.play(index, true); + self.load_tracks(queue.get_spotify()); + + if let Some(tracks) = self.tracks.as_ref() { + let index = queue.append_next(tracks.iter().collect()); + queue.play(index, true); + } } fn queue(&mut self, queue: Arc) { - for track in self.tracks.iter() { - queue.append(track); + self.load_tracks(queue.get_spotify()); + + if let Some(tracks) = self.tracks.as_ref() { + for track in tracks.iter() { + queue.append(track); + } } } fn toggle_saved(&mut self, library: Arc) { + // Don't allow users to unsave their own playlists with one keypress + if !library.is_followed_playlist(self) { + return; + } + if library.is_saved_playlist(self) { library.delete_playlist(&self.id); } else { diff --git a/src/spotify.rs b/src/spotify.rs index 081f569..1c0220a 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -21,6 +21,7 @@ use rspotify::spotify::model::search::{ SearchAlbums, SearchArtists, SearchPlaylists, SearchTracks, }; use rspotify::spotify::model::track::{FullTrack, SavedTrack}; +use rspotify::spotify::model::user::PrivateUser; use failure::Error; @@ -590,6 +591,10 @@ impl Spotify { .map(|fa| fa.artists.iter().map(|a| a.into()).collect()) } + pub fn current_user(&self) -> Option { + self.api_with_retry(|api| api.current_user()) + } + pub fn load(&self, track: &Track) { info!("loading track: {:?}", track); self.channel diff --git a/src/ui/listview.rs b/src/ui/listview.rs index acb5566..50da0f3 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -283,6 +283,8 @@ impl ViewExt for ListView { args: &[String], ) -> Result { if cmd == "play" { + self.queue.clear(); + if !self.attempt_play_all_tracks() { let mut content = self.content.write().unwrap(); if let Some(item) = content.get_mut(self.selected) { diff --git a/src/ui/playlist.rs b/src/ui/playlist.rs index c266594..70b6667 100644 --- a/src/ui/playlist.rs +++ b/src/ui/playlist.rs @@ -18,12 +18,17 @@ pub struct PlaylistView { impl PlaylistView { pub fn new(queue: Arc, library: Arc, playlist: &Playlist) -> Self { - let playlist = playlist.clone(); - let list = ListView::new( - Arc::new(RwLock::new(playlist.tracks.clone())), - queue, - library, - ); + let mut playlist = playlist.clone(); + + playlist.load_tracks(queue.get_spotify()); + + let tracks = if let Some(t) = playlist.tracks.as_ref() { + t.clone() + } else { + Vec::new() + }; + + let list = ListView::new(Arc::new(RwLock::new(tracks)), queue, library); Self { playlist, list } } diff --git a/src/ui/search.rs b/src/ui/search.rs index dc985e6..9e19853 100644 --- a/src/ui/search.rs +++ b/src/ui/search.rs @@ -224,8 +224,8 @@ impl SearchView { _offset: usize, _append: bool, ) -> u32 { - if let Some(results) = spotify.playlist(&query) { - let pls = vec![Library::process_full_playlist(&results, &&spotify)]; + if let Some(result) = spotify.playlist(&query).as_ref() { + let pls = vec![result.into()]; let mut r = playlists.write().unwrap(); *r = pls; return 1; @@ -241,12 +241,7 @@ impl SearchView { append: bool, ) -> u32 { if let Some(results) = spotify.search_playlist(&query, 50, offset as u32) { - let mut pls = results - .playlists - .items - .iter() - .map(|sp| Library::process_simplified_playlist(sp, &&spotify)) - .collect(); + let mut pls = results.playlists.items.iter().map(|sp| sp.into()).collect(); let mut r = playlists.write().unwrap(); if append { diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs index 52b4ff9..5a12e0a 100644 --- a/src/ui/statusbar.rs +++ b/src/ui/statusbar.rs @@ -8,21 +8,26 @@ use cursive::vec::Vec2; use cursive::Printer; use unicode_width::UnicodeWidthStr; +use library::Library; use queue::{Queue, RepeatSetting}; use spotify::{PlayerEvent, Spotify}; pub struct StatusBar { queue: Arc, spotify: Arc, + library: Arc, last_size: Vec2, use_nerdfont: bool, } impl StatusBar { - pub fn new(queue: Arc, spotify: Arc, use_nerdfont: bool) -> StatusBar { + pub fn new(queue: Arc, library: Arc, use_nerdfont: bool) -> StatusBar { + let spotify = queue.get_spotify(); + StatusBar { queue, spotify, + library, last_size: Vec2::new(0, 0), use_nerdfont, } @@ -96,8 +101,7 @@ impl View for StatusBar { RepeatSetting::RepeatPlaylist => "[R] ", RepeatSetting::RepeatTrack => "[R1] ", } - } - .to_string(); + }; let shuffle = if self.queue.get_shuffle() { if self.use_nerdfont { @@ -107,8 +111,7 @@ impl View for StatusBar { } } else { "" - } - .to_string(); + }; printer.with_color(style_bar_bg, |printer| { printer.print((0, 0), &"┉".repeat(printer.size.x)); @@ -124,8 +127,20 @@ impl View for StatusBar { elapsed.as_secs() % 60 ); - let right = - repeat + &shuffle + &format!("{} / {} ", formatted_elapsed, t.duration_str()); + let saved = if self.library.is_saved_track(t) { + if self.use_nerdfont { + "\u{f62b} " + } else { + "✓ " + } + } else { + "" + }; + + let right = repeat.to_string() + + shuffle + + saved + + &format!("{} / {} ", formatted_elapsed, t.duration_str()); let offset = HAlign::Right.get_offset(right.width(), printer.size.x); printer.with_color(style, |printer| { @@ -138,7 +153,7 @@ impl View for StatusBar { printer.print((0, 0), &"━".repeat(duration_width + 1)); }); } else { - let right = repeat + &shuffle; + let right = repeat.to_string() + shuffle; let offset = HAlign::Right.get_offset(right.width(), printer.size.x); printer.with_color(style, |printer| {