implement playlist caching
This commit is contained in:
@@ -20,13 +20,14 @@ futures = "0.1"
|
||||
log = "0.4.0"
|
||||
rspotify = "0.2.5"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
toml = "0.4"
|
||||
tokio = "0.1.7"
|
||||
tokio-core = "0.1"
|
||||
tokio-timer = "0.2"
|
||||
unicode-width = "0.1.5"
|
||||
dbus = { version = "0.6.4", optional = true }
|
||||
xdg = "^2.1"
|
||||
|
||||
[dependencies.librespot]
|
||||
git = "https://github.com/librespot-org/librespot.git"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crossbeam_channel::{unbounded, Receiver, Sender, TryIter};
|
||||
use cursive::{CbFunc, Cursive};
|
||||
|
||||
use playlists::PlaylistEvent;
|
||||
use spotify::PlayerEvent;
|
||||
use ui::playlist::PlaylistEvent;
|
||||
|
||||
pub enum Event {
|
||||
Player(PlayerEvent),
|
||||
|
||||
38
src/main.rs
38
src/main.rs
@@ -8,13 +8,14 @@ extern crate tokio;
|
||||
extern crate tokio_core;
|
||||
extern crate tokio_timer;
|
||||
extern crate unicode_width;
|
||||
extern crate xdg;
|
||||
|
||||
#[cfg(feature = "mpris")]
|
||||
extern crate dbus;
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
extern crate toml;
|
||||
|
||||
#[macro_use]
|
||||
@@ -28,6 +29,7 @@ use std::path::PathBuf;
|
||||
use std::process;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::thread;
|
||||
|
||||
use cursive::event::Key;
|
||||
use cursive::traits::{Identifiable, View};
|
||||
@@ -38,6 +40,7 @@ use cursive::Cursive;
|
||||
mod commands;
|
||||
mod config;
|
||||
mod events;
|
||||
mod playlists;
|
||||
mod queue;
|
||||
mod spotify;
|
||||
mod theme;
|
||||
@@ -49,6 +52,7 @@ mod mpris;
|
||||
|
||||
use commands::CommandManager;
|
||||
use events::{Event, EventManager};
|
||||
use playlists::Playlists;
|
||||
use spotify::PlayerEvent;
|
||||
|
||||
fn init_logger(content: TextContent, write_to_file: bool) {
|
||||
@@ -121,7 +125,7 @@ fn main() {
|
||||
|
||||
let logbuf = TextContent::new("Welcome to ncspot\n");
|
||||
let logview = TextView::new_with_content(logbuf.clone());
|
||||
init_logger(logbuf, false);
|
||||
init_logger(logbuf, true);
|
||||
|
||||
let mut cursive = Cursive::default();
|
||||
cursive.set_theme(theme::default());
|
||||
@@ -146,8 +150,24 @@ fn main() {
|
||||
|
||||
let search = ui::search::SearchView::new(spotify.clone(), queue.clone());
|
||||
|
||||
let mut playlists =
|
||||
ui::playlist::PlaylistView::new(event_manager.clone(), queue.clone(), spotify.clone());
|
||||
let playlists = Arc::new(Playlists::new(&event_manager, &spotify));
|
||||
|
||||
{
|
||||
// download playlists via web api in a background thread
|
||||
let playlists = playlists.clone();
|
||||
thread::spawn(move || {
|
||||
// load cache (if existing)
|
||||
playlists.load_cache();
|
||||
|
||||
// fetch or update cached playlists
|
||||
playlists.fetch_playlists();
|
||||
|
||||
// re-cache for next startup
|
||||
playlists.save_cache();
|
||||
});
|
||||
}
|
||||
|
||||
let mut playlists_view = ui::playlist::PlaylistView::new(&playlists, queue.clone());
|
||||
|
||||
let mut queueview = ui::queue::QueueView::new(queue.clone());
|
||||
|
||||
@@ -158,7 +178,11 @@ fn main() {
|
||||
let mut layout = ui::layout::Layout::new(status, &event_manager)
|
||||
.view("search", search.view.with_id("search"), "Search")
|
||||
.view("log", logview_scroller, "Log")
|
||||
.view("playlists", playlists.view.take().unwrap(), "Playlists")
|
||||
.view(
|
||||
"playlists",
|
||||
playlists_view.view.take().unwrap(),
|
||||
"Playlists",
|
||||
)
|
||||
.view("queue", queueview.view.take().unwrap(), "Queue");
|
||||
|
||||
// initial view is queue
|
||||
@@ -343,7 +367,7 @@ fn main() {
|
||||
#[cfg(feature = "mpris")]
|
||||
mpris_manager.update();
|
||||
}
|
||||
Event::Playlist(event) => playlists.handle_ev(&mut cursive, event),
|
||||
Event::Playlist(event) => playlists_view.handle_ev(&mut cursive, event),
|
||||
Event::Command(cmd) => {
|
||||
// TODO: handle non-error output as well
|
||||
if let Err(e) = cmd_manager.handle(&mut cursive, cmd) {
|
||||
@@ -356,7 +380,7 @@ fn main() {
|
||||
mpris_manager.update();
|
||||
}
|
||||
Event::ScreenChange(name) => match name.as_ref() {
|
||||
"playlists" => playlists.repopulate(&mut cursive),
|
||||
"playlists" => playlists_view.repopulate(&mut cursive),
|
||||
"queue" => queueview.repopulate(&mut cursive),
|
||||
_ => (),
|
||||
},
|
||||
|
||||
198
src/mpris.rs
198
src/mpris.rs
@@ -1,11 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
|
||||
use dbus::{Path, SignalArgs};
|
||||
use dbus::arg::{Variant, RefArg};
|
||||
use dbus::tree::{Access, Factory};
|
||||
use dbus::arg::{RefArg, Variant};
|
||||
use dbus::stdintf::org_freedesktop_dbus::PropertiesPropertiesChanged;
|
||||
use dbus::tree::{Access, Factory};
|
||||
use dbus::{Path, SignalArgs};
|
||||
|
||||
use queue::Queue;
|
||||
use spotify::{PlayerEvent, Spotify};
|
||||
@@ -14,8 +14,9 @@ fn get_playbackstatus(spotify: Arc<Spotify>) -> String {
|
||||
match spotify.get_current_status() {
|
||||
PlayerEvent::Playing => "Playing",
|
||||
PlayerEvent::Paused => "Paused",
|
||||
_ => "Stopped"
|
||||
}.to_string()
|
||||
_ => "Stopped",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn get_metadata(queue: Arc<Mutex<Queue>>) -> HashMap<String, Variant<Box<RefArg>>> {
|
||||
@@ -24,92 +25,129 @@ fn get_metadata(queue: Arc<Mutex<Queue>>) -> HashMap<String, Variant<Box<RefArg>
|
||||
let queue = queue.lock().expect("could not lock queue");
|
||||
let track = queue.get_current();
|
||||
|
||||
hm.insert("mpris:trackid".to_string(), Variant(Box::new(
|
||||
track.map(|t| format!("spotify:track:{}", t.id)).unwrap_or("".to_string())
|
||||
)));
|
||||
hm.insert("mpris:length".to_string(), Variant(Box::new(
|
||||
track.map(|t| t.duration * 1_000_000).unwrap_or(0)
|
||||
)));
|
||||
hm.insert("mpris:artUrl".to_string(), Variant(Box::new(
|
||||
track.map(|t| t.cover_url.clone()).unwrap_or("".to_string())
|
||||
)));
|
||||
hm.insert(
|
||||
"mpris:trackid".to_string(),
|
||||
Variant(Box::new(
|
||||
track
|
||||
.map(|t| format!("spotify:track:{}", t.id))
|
||||
.unwrap_or("".to_string()),
|
||||
)),
|
||||
);
|
||||
hm.insert(
|
||||
"mpris:length".to_string(),
|
||||
Variant(Box::new(track.map(|t| t.duration * 1_000_000).unwrap_or(0))),
|
||||
);
|
||||
hm.insert(
|
||||
"mpris:artUrl".to_string(),
|
||||
Variant(Box::new(
|
||||
track.map(|t| t.cover_url.clone()).unwrap_or("".to_string()),
|
||||
)),
|
||||
);
|
||||
|
||||
hm.insert("xesam:album".to_string(), Variant(Box::new(
|
||||
track.map(|t| t.album.clone()).unwrap_or("".to_string())
|
||||
)));
|
||||
hm.insert("xesam:albumArtist".to_string(), Variant(Box::new(
|
||||
track.map(|t| t.album_artists.clone()).unwrap_or(Vec::new())
|
||||
)));
|
||||
hm.insert("xesam:artist".to_string(), Variant(Box::new(
|
||||
track.map(|t| t.artists.clone()).unwrap_or(Vec::new())
|
||||
)));
|
||||
hm.insert("xesam:discNumber".to_string(), Variant(Box::new(
|
||||
track.map(|t| t.disc_number).unwrap_or(0)
|
||||
)));
|
||||
hm.insert("xesam:title".to_string(), Variant(Box::new(
|
||||
track.map(|t| t.title.clone()).unwrap_or("".to_string())
|
||||
)));
|
||||
hm.insert("xesam:trackNumber".to_string(), Variant(Box::new(
|
||||
track.map(|t| t.track_number).unwrap_or(0)
|
||||
)));
|
||||
hm.insert("xesam:url".to_string(), Variant(Box::new(
|
||||
track.map(|t| t.url.clone()).unwrap_or("".to_string())
|
||||
)));
|
||||
hm.insert(
|
||||
"xesam:album".to_string(),
|
||||
Variant(Box::new(
|
||||
track.map(|t| t.album.clone()).unwrap_or("".to_string()),
|
||||
)),
|
||||
);
|
||||
hm.insert(
|
||||
"xesam:albumArtist".to_string(),
|
||||
Variant(Box::new(
|
||||
track.map(|t| t.album_artists.clone()).unwrap_or(Vec::new()),
|
||||
)),
|
||||
);
|
||||
hm.insert(
|
||||
"xesam:artist".to_string(),
|
||||
Variant(Box::new(
|
||||
track.map(|t| t.artists.clone()).unwrap_or(Vec::new()),
|
||||
)),
|
||||
);
|
||||
hm.insert(
|
||||
"xesam:discNumber".to_string(),
|
||||
Variant(Box::new(track.map(|t| t.disc_number).unwrap_or(0))),
|
||||
);
|
||||
hm.insert(
|
||||
"xesam:title".to_string(),
|
||||
Variant(Box::new(
|
||||
track.map(|t| t.title.clone()).unwrap_or("".to_string()),
|
||||
)),
|
||||
);
|
||||
hm.insert(
|
||||
"xesam:trackNumber".to_string(),
|
||||
Variant(Box::new(track.map(|t| t.track_number).unwrap_or(0))),
|
||||
);
|
||||
hm.insert(
|
||||
"xesam:url".to_string(),
|
||||
Variant(Box::new(
|
||||
track.map(|t| t.url.clone()).unwrap_or("".to_string()),
|
||||
)),
|
||||
);
|
||||
|
||||
hm
|
||||
}
|
||||
|
||||
fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Mutex<Queue>>, rx: mpsc::Receiver<()>) {
|
||||
let conn = Rc::new(dbus::Connection::get_private(dbus::BusType::Session)
|
||||
.expect("Failed to connect to dbus"));
|
||||
conn.register_name("org.mpris.MediaPlayer2.ncspot", dbus::NameFlag::ReplaceExisting as u32)
|
||||
.expect("Failed to register dbus player name");
|
||||
let conn = Rc::new(
|
||||
dbus::Connection::get_private(dbus::BusType::Session).expect("Failed to connect to dbus"),
|
||||
);
|
||||
conn.register_name(
|
||||
"org.mpris.MediaPlayer2.ncspot",
|
||||
dbus::NameFlag::ReplaceExisting as u32,
|
||||
)
|
||||
.expect("Failed to register dbus player name");
|
||||
|
||||
let f = Factory::new_fn::<()>();
|
||||
|
||||
let property_canquit = f.property::<bool, _>("CanQuit", ())
|
||||
let property_canquit = f
|
||||
.property::<bool, _>("CanQuit", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(false); // TODO
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_canraise = f.property::<bool, _>("CanRaise", ())
|
||||
let property_canraise = f
|
||||
.property::<bool, _>("CanRaise", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(false);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_cansetfullscreen = f.property::<bool, _>("CanSetFullscreen", ())
|
||||
let property_cansetfullscreen = f
|
||||
.property::<bool, _>("CanSetFullscreen", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(false);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_hastracklist = f.property::<bool, _>("HasTrackList", ())
|
||||
let property_hastracklist = f
|
||||
.property::<bool, _>("HasTrackList", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(false); // TODO
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_identity = f.property::<String, _>("Identity", ())
|
||||
let property_identity = f
|
||||
.property::<String, _>("Identity", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append("ncspot".to_string());
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_urischemes = f.property::<Vec<String>, _>("SupportedUriSchemes", ())
|
||||
let property_urischemes = f
|
||||
.property::<Vec<String>, _>("SupportedUriSchemes", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(vec!["spotify".to_string()]);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_mimetypes = f.property::<Vec<String>, _>("SupportedMimeTypes", ())
|
||||
let property_mimetypes = f
|
||||
.property::<Vec<String>, _>("SupportedMimeTypes", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(Vec::new() as Vec<String>);
|
||||
@@ -117,7 +155,8 @@ fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Mutex<Queue>>, rx: mpsc::Re
|
||||
});
|
||||
|
||||
// https://specifications.freedesktop.org/mpris-spec/latest/Media_Player.html
|
||||
let interface = f.interface("org.mpris.MediaPlayer2", ())
|
||||
let interface = f
|
||||
.interface("org.mpris.MediaPlayer2", ())
|
||||
.add_p(property_canquit)
|
||||
.add_p(property_canraise)
|
||||
.add_p(property_cansetfullscreen)
|
||||
@@ -137,7 +176,8 @@ fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Mutex<Queue>>, rx: mpsc::Re
|
||||
})
|
||||
};
|
||||
|
||||
let property_loopstatus = f.property::<String, _>("LoopStatus", ())
|
||||
let property_loopstatus = f
|
||||
.property::<String, _>("LoopStatus", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append("None".to_string()); // TODO
|
||||
@@ -166,70 +206,80 @@ fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Mutex<Queue>>, rx: mpsc::Re
|
||||
})
|
||||
};
|
||||
|
||||
let property_volume = f.property::<f64, _>("Volume", ())
|
||||
let property_volume = f
|
||||
.property::<f64, _>("Volume", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(1.0);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_rate = f.property::<f64, _>("Rate", ())
|
||||
let property_rate = f
|
||||
.property::<f64, _>("Rate", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(1.0);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_minrate = f.property::<f64, _>("MinimumRate", ())
|
||||
let property_minrate = f
|
||||
.property::<f64, _>("MinimumRate", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(1.0);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_maxrate = f.property::<f64, _>("MaximumRate", ())
|
||||
let property_maxrate = f
|
||||
.property::<f64, _>("MaximumRate", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(1.0);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_canplay = f.property::<bool, _>("CanPlay", ())
|
||||
let property_canplay = f
|
||||
.property::<bool, _>("CanPlay", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(true);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_canpause = f.property::<bool, _>("CanPause", ())
|
||||
let property_canpause = f
|
||||
.property::<bool, _>("CanPause", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(true);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_canseek = f.property::<bool, _>("CanSeek", ())
|
||||
let property_canseek = f
|
||||
.property::<bool, _>("CanSeek", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(false); // TODO
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_cancontrol = f.property::<bool, _>("CanControl", ())
|
||||
let property_cancontrol = f
|
||||
.property::<bool, _>("CanControl", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(true);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_cangonext = f.property::<bool, _>("CanGoNext", ())
|
||||
let property_cangonext = f
|
||||
.property::<bool, _>("CanGoNext", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(true);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let property_cangoprevious = f.property::<bool, _>("CanGoPrevious", ())
|
||||
let property_cangoprevious = f
|
||||
.property::<bool, _>("CanGoPrevious", ())
|
||||
.access(Access::Read)
|
||||
.on_get(|iter, _| {
|
||||
iter.append(true);
|
||||
@@ -287,7 +337,8 @@ fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Mutex<Queue>>, rx: mpsc::Re
|
||||
// TODO: Seek, SetPosition, Shuffle, OpenUri (?)
|
||||
|
||||
// https://specifications.freedesktop.org/mpris-spec/latest/Player_Interface.html
|
||||
let interface_player = f.interface("org.mpris.MediaPlayer2.Player", ())
|
||||
let interface_player = f
|
||||
.interface("org.mpris.MediaPlayer2.Player", ())
|
||||
.add_p(property_playbackstatus)
|
||||
.add_p(property_loopstatus)
|
||||
.add_p(property_metadata)
|
||||
@@ -309,13 +360,15 @@ fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Mutex<Queue>>, rx: mpsc::Re
|
||||
.add_m(method_next)
|
||||
.add_m(method_previous);
|
||||
|
||||
let tree = f.tree(())
|
||||
.add(f.object_path("/org/mpris/MediaPlayer2", ()).introspectable()
|
||||
let tree = f.tree(()).add(
|
||||
f.object_path("/org/mpris/MediaPlayer2", ())
|
||||
.introspectable()
|
||||
.add(interface)
|
||||
.add(interface_player)
|
||||
);
|
||||
.add(interface_player),
|
||||
);
|
||||
|
||||
tree.set_registered(&conn, true).expect("failed to register tree");
|
||||
tree.set_registered(&conn, true)
|
||||
.expect("failed to register tree");
|
||||
|
||||
conn.add_handler(tree);
|
||||
loop {
|
||||
@@ -328,20 +381,23 @@ fn run_dbus_server(spotify: Arc<Spotify>, queue: Arc<Mutex<Queue>>, rx: mpsc::Re
|
||||
changed.interface_name = "org.mpris.MediaPlayer2.Player".to_string();
|
||||
changed.changed_properties.insert(
|
||||
"Metadata".to_string(),
|
||||
Variant(Box::new(get_metadata(queue.clone())))
|
||||
Variant(Box::new(get_metadata(queue.clone()))),
|
||||
);
|
||||
changed.changed_properties.insert(
|
||||
"PlaybackStatus".to_string(),
|
||||
Variant(Box::new(get_playbackstatus(spotify.clone())))
|
||||
Variant(Box::new(get_playbackstatus(spotify.clone()))),
|
||||
);
|
||||
|
||||
conn.send(changed.to_emit_message(&Path::new("/org/mpris/MediaPlayer2".to_string()).unwrap())).unwrap();
|
||||
conn.send(
|
||||
changed.to_emit_message(&Path::new("/org/mpris/MediaPlayer2".to_string()).unwrap()),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MprisManager {
|
||||
tx: mpsc::Sender<()>
|
||||
tx: mpsc::Sender<()>,
|
||||
}
|
||||
|
||||
impl MprisManager {
|
||||
@@ -352,9 +408,7 @@ impl MprisManager {
|
||||
run_dbus_server(spotify, queue, rx);
|
||||
});
|
||||
|
||||
MprisManager {
|
||||
tx: tx
|
||||
}
|
||||
MprisManager { tx: tx }
|
||||
}
|
||||
|
||||
pub fn update(&self) {
|
||||
|
||||
160
src/playlists.rs
Normal file
160
src/playlists.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use std::iter::Iterator;
|
||||
use std::ops::Deref;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use rspotify::spotify::model::playlist::SimplifiedPlaylist;
|
||||
|
||||
use events::{Event, EventManager};
|
||||
use spotify::Spotify;
|
||||
use track::Track;
|
||||
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct Playlist {
|
||||
pub meta: SimplifiedPlaylist,
|
||||
pub tracks: Vec<Track>,
|
||||
}
|
||||
|
||||
pub enum PlaylistEvent {
|
||||
NewList(usize, Playlist),
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
pub struct PlaylistStore {
|
||||
pub playlists: Vec<Playlist>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Playlists {
|
||||
pub store: Arc<RwLock<PlaylistStore>>,
|
||||
ev: EventManager,
|
||||
spotify: Arc<Spotify>,
|
||||
}
|
||||
|
||||
impl Playlists {
|
||||
pub fn new(ev: &EventManager, spotify: &Arc<Spotify>) -> Playlists {
|
||||
Playlists {
|
||||
store: Arc::new(RwLock::new(PlaylistStore::default())),
|
||||
ev: ev.clone(),
|
||||
spotify: spotify.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_cache(&self) {
|
||||
let xdg_dirs = xdg::BaseDirectories::with_prefix("ncspot").unwrap();
|
||||
if let Ok(cache_path) = xdg_dirs.place_cache_file("playlists.db") {
|
||||
if let Ok(contents) = std::fs::read_to_string(&cache_path) {
|
||||
debug!(
|
||||
"loading playlist cache from {}",
|
||||
cache_path.to_str().unwrap()
|
||||
);
|
||||
let parsed: Result<PlaylistStore, _> = serde_json::from_str(&contents);
|
||||
if let Ok(cache) = parsed {
|
||||
debug!("playlist cache loaded ({} lists)", cache.playlists.len());
|
||||
let mut store = self.store.write().expect("can't writelock playlist store");
|
||||
store.playlists.clear();
|
||||
store.playlists.extend(cache.playlists);
|
||||
|
||||
// force refresh of UI (if visible)
|
||||
self.ev.send(Event::ScreenChange("playlists".to_owned()));
|
||||
} else {
|
||||
error!("playlist cache corrupted?");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_cache(&self) {
|
||||
let xdg_dirs = xdg::BaseDirectories::with_prefix("ncspot").unwrap();
|
||||
if let Ok(cache_path) = xdg_dirs.place_cache_file("playlists.db") {
|
||||
match serde_json::to_string(&self.store.deref()) {
|
||||
Ok(contents) => std::fs::write(cache_path, contents).unwrap(),
|
||||
Err(e) => error!("could not write playlist cache: {:?}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_playlist(list: &SimplifiedPlaylist, spotify: &Spotify) -> Playlist {
|
||||
debug!("got list: {}", list.name);
|
||||
let id = list.id.clone();
|
||||
|
||||
let mut collected_tracks = Vec::new();
|
||||
|
||||
let mut tracks_result = spotify.user_playlist_tracks(&id, 100, 0).ok();
|
||||
while let Some(ref tracks) = tracks_result.clone() {
|
||||
for listtrack in &tracks.items {
|
||||
collected_tracks.push(Track::new(&listtrack.track));
|
||||
}
|
||||
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)
|
||||
.ok()
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
Playlist {
|
||||
meta: list.clone(),
|
||||
tracks: collected_tracks,
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_download(&self, remote: &SimplifiedPlaylist) -> bool {
|
||||
for local in &self
|
||||
.store
|
||||
.read()
|
||||
.expect("can't readlock playlists")
|
||||
.playlists
|
||||
{
|
||||
if local.meta.id == remote.id {
|
||||
return local.meta.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.playlists.iter_mut().enumerate() {
|
||||
if local.meta.id == updated.meta.id {
|
||||
*local = updated.clone();
|
||||
return index;
|
||||
}
|
||||
}
|
||||
store.playlists.push(updated.clone());
|
||||
store.playlists.len() - 1
|
||||
}
|
||||
|
||||
pub fn fetch_playlists(&self) {
|
||||
debug!("loading playlists");
|
||||
let mut lists_result = self.spotify.current_user_playlist(50, 0).ok();
|
||||
while let Some(ref lists) = lists_result.clone() {
|
||||
for remote in &lists.items {
|
||||
if self.needs_download(remote) {
|
||||
info!("updating playlist {}", remote.name);
|
||||
let playlist = Self::process_playlist(&remote, &self.spotify);
|
||||
let index = self.append_or_update(&playlist);
|
||||
self.ev.send(Event::Playlist(PlaylistEvent::NewList(
|
||||
index,
|
||||
playlist.clone(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
.ok()
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use std::fmt;
|
||||
|
||||
use rspotify::spotify::model::track::FullTrack;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Deserialize, Serialize)]
|
||||
pub struct Track {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
@@ -24,7 +24,8 @@ impl Track {
|
||||
.map(|ref artist| artist.name.clone())
|
||||
.collect::<Vec<String>>();
|
||||
let album_artists = track
|
||||
.album.artists
|
||||
.album
|
||||
.artists
|
||||
.iter()
|
||||
.map(|ref artist| artist.name.clone())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::thread;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use cursive::direction::Orientation;
|
||||
use cursive::event::Key;
|
||||
@@ -7,46 +6,26 @@ use cursive::traits::Boxable;
|
||||
use cursive::traits::Identifiable;
|
||||
use cursive::views::*;
|
||||
use cursive::Cursive;
|
||||
use rspotify::spotify::model::playlist::SimplifiedPlaylist;
|
||||
|
||||
use events::{Event, EventManager};
|
||||
use playlists::{Playlist, PlaylistEvent, Playlists};
|
||||
use queue::Queue;
|
||||
use spotify::Spotify;
|
||||
use track::Track;
|
||||
use ui::splitbutton::SplitButton;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Playlist {
|
||||
meta: SimplifiedPlaylist,
|
||||
tracks: Vec<Track>,
|
||||
}
|
||||
|
||||
pub enum PlaylistEvent {
|
||||
NewList(Playlist),
|
||||
}
|
||||
|
||||
pub struct PlaylistView {
|
||||
pub view: Option<BoxView<ScrollView<IdView<LinearLayout>>>>,
|
||||
queue: Arc<Mutex<Queue>>,
|
||||
playlists: Arc<RwLock<Vec<Playlist>>>,
|
||||
playlists: Playlists,
|
||||
}
|
||||
|
||||
impl PlaylistView {
|
||||
pub fn new(ev: EventManager, queue: Arc<Mutex<Queue>>, spotify: Arc<Spotify>) -> PlaylistView {
|
||||
pub fn new(playlists: &Playlists, queue: Arc<Mutex<Queue>>) -> PlaylistView {
|
||||
let playlists_view = LinearLayout::new(Orientation::Vertical).with_id("playlists");
|
||||
let scrollable = ScrollView::new(playlists_view).full_screen();
|
||||
let playlists = Arc::new(RwLock::new(Vec::new()));
|
||||
|
||||
{
|
||||
let spotify = spotify.clone();
|
||||
let playlists = playlists.clone();
|
||||
Self::load_playlists(ev, spotify, playlists);
|
||||
}
|
||||
|
||||
PlaylistView {
|
||||
view: Some(scrollable),
|
||||
queue: queue,
|
||||
playlists: playlists,
|
||||
playlists: playlists.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,68 +65,6 @@ impl PlaylistView {
|
||||
button
|
||||
}
|
||||
|
||||
fn load_playlist(list: &SimplifiedPlaylist, spotify: Arc<Spotify>) -> Playlist {
|
||||
debug!("got list: {}", list.name);
|
||||
let id = list.id.clone();
|
||||
|
||||
let mut collected_tracks = Vec::new();
|
||||
|
||||
let mut tracks_result = spotify.user_playlist_tracks(&id, 100, 0).ok();
|
||||
while let Some(ref tracks) = tracks_result.clone() {
|
||||
for listtrack in &tracks.items {
|
||||
collected_tracks.push(Track::new(&listtrack.track));
|
||||
}
|
||||
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)
|
||||
.ok()
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
Playlist {
|
||||
meta: list.clone(),
|
||||
tracks: collected_tracks,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_playlists(
|
||||
ev: EventManager,
|
||||
spotify: Arc<Spotify>,
|
||||
playlists: Arc<RwLock<Vec<Playlist>>>,
|
||||
) {
|
||||
thread::spawn(move || {
|
||||
debug!("loading playlists");
|
||||
let mut lists_result = spotify.current_user_playlist(50, 0).ok();
|
||||
while let Some(ref lists) = lists_result.clone() {
|
||||
for list in &lists.items {
|
||||
let playlist = Self::load_playlist(&list, spotify.clone());
|
||||
ev.send(Event::Playlist(PlaylistEvent::NewList(playlist.clone())));
|
||||
playlists
|
||||
.write()
|
||||
.expect("could not acquire write lock on playlists")
|
||||
.push(playlist);
|
||||
}
|
||||
|
||||
// load next batch if necessary
|
||||
lists_result = match lists.next {
|
||||
Some(_) => {
|
||||
debug!("requesting playlists again..");
|
||||
spotify
|
||||
.current_user_playlist(50, lists.offset + lists.items.len() as u32)
|
||||
.ok()
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn clear_playlists(&self, playlists: &mut ViewRef<LinearLayout>) {
|
||||
while playlists.len() > 0 {
|
||||
playlists.remove_child(0);
|
||||
@@ -159,12 +76,13 @@ impl PlaylistView {
|
||||
if let Some(mut playlists) = view_ref {
|
||||
self.clear_playlists(&mut playlists);
|
||||
|
||||
for list in self
|
||||
let playlist_store = &self
|
||||
.playlists
|
||||
.store
|
||||
.read()
|
||||
.expect("could not acquire read lock on playlists")
|
||||
.iter()
|
||||
{
|
||||
.expect("can't readlock playlists");
|
||||
info!("repopulating {} lists", playlist_store.playlists.len());
|
||||
for list in &playlist_store.playlists {
|
||||
let button = self.create_button(&list);
|
||||
playlists.add_child(button);
|
||||
}
|
||||
@@ -176,9 +94,13 @@ impl PlaylistView {
|
||||
|
||||
if let Some(mut playlists) = view_ref {
|
||||
match event {
|
||||
PlaylistEvent::NewList(list) => {
|
||||
PlaylistEvent::NewList(index, list) => {
|
||||
let button = self.create_button(&list);
|
||||
playlists.add_child(button);
|
||||
|
||||
if let Some(_) = playlists.get_child(index) {
|
||||
playlists.remove_child(index);
|
||||
}
|
||||
playlists.insert_child(index, button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user