diff --git a/Cargo.toml b/Cargo.toml index 9bad2f8..6beee8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/events.rs b/src/events.rs index ed61f5a..7a0aff4 100644 --- a/src/events.rs +++ b/src/events.rs @@ -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), diff --git a/src/main.rs b/src/main.rs index a119cc0..aba4915 100644 --- a/src/main.rs +++ b/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), _ => (), }, diff --git a/src/mpris.rs b/src/mpris.rs index 6b8810b..9413305 100644 --- a/src/mpris.rs +++ b/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) -> String { match spotify.get_current_status() { PlayerEvent::Playing => "Playing", PlayerEvent::Paused => "Paused", - _ => "Stopped" - }.to_string() + _ => "Stopped", + } + .to_string() } fn get_metadata(queue: Arc>) -> HashMap>> { @@ -24,92 +25,129 @@ fn get_metadata(queue: Arc>) -> HashMap 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, queue: Arc>, 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::("CanQuit", ()) + let property_canquit = f + .property::("CanQuit", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(false); // TODO Ok(()) }); - let property_canraise = f.property::("CanRaise", ()) + let property_canraise = f + .property::("CanRaise", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(false); Ok(()) }); - let property_cansetfullscreen = f.property::("CanSetFullscreen", ()) + let property_cansetfullscreen = f + .property::("CanSetFullscreen", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(false); Ok(()) }); - let property_hastracklist = f.property::("HasTrackList", ()) + let property_hastracklist = f + .property::("HasTrackList", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(false); // TODO Ok(()) }); - let property_identity = f.property::("Identity", ()) + let property_identity = f + .property::("Identity", ()) .access(Access::Read) .on_get(|iter, _| { iter.append("ncspot".to_string()); Ok(()) }); - let property_urischemes = f.property::, _>("SupportedUriSchemes", ()) + let property_urischemes = f + .property::, _>("SupportedUriSchemes", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(vec!["spotify".to_string()]); Ok(()) }); - let property_mimetypes = f.property::, _>("SupportedMimeTypes", ()) + let property_mimetypes = f + .property::, _>("SupportedMimeTypes", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(Vec::new() as Vec); @@ -117,7 +155,8 @@ fn run_dbus_server(spotify: Arc, queue: Arc>, 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, queue: Arc>, rx: mpsc::Re }) }; - let property_loopstatus = f.property::("LoopStatus", ()) + let property_loopstatus = f + .property::("LoopStatus", ()) .access(Access::Read) .on_get(|iter, _| { iter.append("None".to_string()); // TODO @@ -166,70 +206,80 @@ fn run_dbus_server(spotify: Arc, queue: Arc>, rx: mpsc::Re }) }; - let property_volume = f.property::("Volume", ()) + let property_volume = f + .property::("Volume", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(1.0); Ok(()) }); - let property_rate = f.property::("Rate", ()) + let property_rate = f + .property::("Rate", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(1.0); Ok(()) }); - let property_minrate = f.property::("MinimumRate", ()) + let property_minrate = f + .property::("MinimumRate", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(1.0); Ok(()) }); - let property_maxrate = f.property::("MaximumRate", ()) + let property_maxrate = f + .property::("MaximumRate", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(1.0); Ok(()) }); - let property_canplay = f.property::("CanPlay", ()) + let property_canplay = f + .property::("CanPlay", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(true); Ok(()) }); - let property_canpause = f.property::("CanPause", ()) + let property_canpause = f + .property::("CanPause", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(true); Ok(()) }); - let property_canseek = f.property::("CanSeek", ()) + let property_canseek = f + .property::("CanSeek", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(false); // TODO Ok(()) }); - let property_cancontrol = f.property::("CanControl", ()) + let property_cancontrol = f + .property::("CanControl", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(true); Ok(()) }); - let property_cangonext = f.property::("CanGoNext", ()) + let property_cangonext = f + .property::("CanGoNext", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(true); Ok(()) }); - let property_cangoprevious = f.property::("CanGoPrevious", ()) + let property_cangoprevious = f + .property::("CanGoPrevious", ()) .access(Access::Read) .on_get(|iter, _| { iter.append(true); @@ -287,7 +337,8 @@ fn run_dbus_server(spotify: Arc, queue: Arc>, 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, queue: Arc>, 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, queue: Arc>, 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) { diff --git a/src/playlists.rs b/src/playlists.rs new file mode 100644 index 0000000..c7e0ae3 --- /dev/null +++ b/src/playlists.rs @@ -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, +} + +pub enum PlaylistEvent { + NewList(usize, Playlist), +} + +#[derive(Default, Serialize, Deserialize)] +pub struct PlaylistStore { + pub playlists: Vec, +} + +#[derive(Clone)] +pub struct Playlists { + pub store: Arc>, + ev: EventManager, + spotify: Arc, +} + +impl Playlists { + pub fn new(ev: &EventManager, spotify: &Arc) -> 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 = 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, + } + } + } +} diff --git a/src/track.rs b/src/track.rs index 9e4dd88..c4f6529 100644 --- a/src/track.rs +++ b/src/track.rs @@ -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::>(); let album_artists = track - .album.artists + .album + .artists .iter() .map(|ref artist| artist.name.clone()) .collect::>(); diff --git a/src/ui/playlist.rs b/src/ui/playlist.rs index 207302e..5865810 100644 --- a/src/ui/playlist.rs +++ b/src/ui/playlist.rs @@ -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, -} - -pub enum PlaylistEvent { - NewList(Playlist), -} - pub struct PlaylistView { pub view: Option>>>, queue: Arc>, - playlists: Arc>>, + playlists: Playlists, } impl PlaylistView { - pub fn new(ev: EventManager, queue: Arc>, spotify: Arc) -> PlaylistView { + pub fn new(playlists: &Playlists, queue: Arc>) -> 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) -> 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, - playlists: Arc>>, - ) { - 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) { 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); } } }