Merge branch 'minor-stuff' of git://github.com/KoffeinFlummi/ncspot into develop
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -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<RwLock<Vec<Artist>>>,
|
||||
pub playlists: Arc<RwLock<Vec<Playlist>>>,
|
||||
is_done: Arc<RwLock<bool>>,
|
||||
user_id: Option<String>,
|
||||
ev: EventManager,
|
||||
spotify: Arc<Spotify>,
|
||||
pub use_nerdfont: bool,
|
||||
@@ -36,12 +37,15 @@ pub struct Library {
|
||||
|
||||
impl Library {
|
||||
pub fn new(ev: &EventManager, spotify: Arc<Spotify>, 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) {
|
||||
|
||||
@@ -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| {
|
||||
|
||||
136
src/playlist.rs
136
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<Track>,
|
||||
pub num_tracks: usize,
|
||||
pub tracks: Option<Vec<Track>>,
|
||||
}
|
||||
|
||||
impl Playlist {
|
||||
pub fn load_tracks(&mut self, spotify: Arc<Spotify>) {
|
||||
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<Queue>) -> bool {
|
||||
let playing: Vec<String> = queue
|
||||
.queue
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|t| t.id.is_some())
|
||||
.map(|t| t.id.clone().unwrap())
|
||||
.collect();
|
||||
let ids: Vec<String> = 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<String> = queue
|
||||
.queue
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|t| t.id.is_some())
|
||||
.map(|t| t.id.clone().unwrap())
|
||||
.collect();
|
||||
let ids: Vec<String> = 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<Library>) -> 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<Queue>) {
|
||||
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<Queue>) {
|
||||
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<Library>) {
|
||||
// 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 {
|
||||
|
||||
@@ -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<PrivateUser> {
|
||||
self.api_with_retry(|api| api.current_user())
|
||||
}
|
||||
|
||||
pub fn load(&self, track: &Track) {
|
||||
info!("loading track: {:?}", track);
|
||||
self.channel
|
||||
|
||||
@@ -283,6 +283,8 @@ impl<I: ListItem + Clone> ViewExt for ListView<I> {
|
||||
args: &[String],
|
||||
) -> Result<CommandResult, String> {
|
||||
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) {
|
||||
|
||||
@@ -18,12 +18,17 @@ pub struct PlaylistView {
|
||||
|
||||
impl PlaylistView {
|
||||
pub fn new(queue: Arc<Queue>, library: Arc<Library>, 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 }
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Queue>,
|
||||
spotify: Arc<Spotify>,
|
||||
library: Arc<Library>,
|
||||
last_size: Vec2,
|
||||
use_nerdfont: bool,
|
||||
}
|
||||
|
||||
impl StatusBar {
|
||||
pub fn new(queue: Arc<Queue>, spotify: Arc<Spotify>, use_nerdfont: bool) -> StatusBar {
|
||||
pub fn new(queue: Arc<Queue>, library: Arc<Library>, 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| {
|
||||
|
||||
Reference in New Issue
Block a user