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