From 0b14fc5da7b8f21e133d50630f1e041dca983a4e Mon Sep 17 00:00:00 2001 From: Henrik Friedrichsen Date: Wed, 6 Mar 2019 23:56:11 +0100 Subject: [PATCH] transform simple queue to a preserving, more complex kind this is a pretty big but necessary change and might not be stable yet. some key points: - the queue is now responsible for playback controls and track management, as this was scattered between the queue and spotify objects. - because the queue is now retained, it should be easier to save it as a spotify playlist closes #12 --- src/events.rs | 10 ++-- src/main.rs | 40 +++++++++----- src/queue.rs | 125 ++++++++++++++++++++++++++++++++++---------- src/spotify.rs | 89 ++++++++----------------------- src/ui/playlist.rs | 46 ++++++++++++---- src/ui/queue.rs | 34 +++++------- src/ui/search.rs | 35 +++++++------ src/ui/statusbar.rs | 26 ++++++--- 8 files changed, 232 insertions(+), 173 deletions(-) diff --git a/src/events.rs b/src/events.rs index 2588538..4a0f850 100644 --- a/src/events.rs +++ b/src/events.rs @@ -1,15 +1,13 @@ use crossbeam_channel::{unbounded, Receiver, Sender, TryIter}; use cursive::{CbFunc, Cursive}; -use queue::QueueChange; -use spotify::PlayerStatus; -use track::Track; +use queue::QueueEvent; +use spotify::PlayerEvent; use ui::playlist::PlaylistEvent; pub enum Event { - Queue(QueueChange), - PlayerStatus(PlayerStatus), - PlayerTrack(Option), + Queue(QueueEvent), + Player(PlayerEvent), Playlist(PlaylistEvent), } diff --git a/src/main.rs b/src/main.rs index f1b2670..eced564 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,7 +39,8 @@ mod track; mod ui; use events::{Event, EventManager}; -use queue::QueueChange; +use queue::QueueEvent; +use spotify::PlayerEvent; use ui::playlist::PlaylistEvent; fn init_logger(content: TextContent) { @@ -106,28 +107,37 @@ fn main() { cursive.set_theme(theme::default()); cursive.set_autorefresh(true); - let queue = Arc::new(Mutex::new(queue::Queue::new(event_manager.clone()))); - let spotify = Arc::new(spotify::Spotify::new( event_manager.clone(), cfg.username, cfg.password, config::CLIENT_ID.to_string(), - queue.clone(), )); + let queue = Arc::new(Mutex::new(queue::Queue::new( + event_manager.clone(), + spotify.clone(), + ))); + // global player keybindings (play, pause, stop) { - let spotify = spotify.clone(); + let queue = queue.clone(); cursive.add_global_callback('P', move |_s| { - spotify.toggleplayback(); + queue.lock().expect("could not lock queue").toggleplayback(); }); } { - let spotify = spotify.clone(); + let queue = queue.clone(); cursive.add_global_callback('S', move |_s| { - spotify.stop(); + queue.lock().expect("could not lock queue").stop(); + }); + } + + { + let queue = queue.clone(); + cursive.add_global_callback('>', move |_s| { + queue.lock().expect("could not lock queue").next(); }); } @@ -135,12 +145,12 @@ fn main() { let mut playlists = ui::playlist::PlaylistView::new(queue.clone(), spotify.clone()); - let mut queueview = ui::queue::QueueView::new(queue.clone(), spotify.clone()); + let mut queueview = ui::queue::QueueView::new(queue.clone()); let logview_scroller = ScrollView::new(logview).scroll_strategy(ScrollStrategy::StickToBottom); let logpanel = Panel::new(logview_scroller).title("Log"); - let status = ui::statusbar::StatusBar::new(spotify.clone()); + let status = ui::statusbar::StatusBar::new(queue.clone(), spotify.clone()); let layout = ui::layout::Layout::new(status) .view("search", BoxView::with_full_height(search.view)) @@ -162,7 +172,7 @@ fn main() { s.call_on_id("main", |v: &mut ui::layout::Layout| { v.set_view("queue"); }); - ev.send(Event::Queue(QueueChange::Show)); + ev.send(Event::Queue(QueueEvent::Show)); }); } @@ -189,8 +199,12 @@ fn main() { trace!("event received"); match event { Event::Queue(ev) => queueview.handle_ev(&mut cursive, ev), - Event::PlayerStatus(state) => spotify.update_status(state), - Event::PlayerTrack(track) => spotify.update_track(track), + Event::Player(state) => { + if state == PlayerEvent::FinishedTrack { + queue.lock().expect("could not lock queue").next(); + } + spotify.update_status(state); + } Event::Playlist(event) => playlists.handle_ev(&mut cursive, event), } } diff --git a/src/queue.rs b/src/queue.rs index fae6c1b..f1e4180 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -1,57 +1,124 @@ -use std::collections::vec_deque::Iter; -use std::collections::VecDeque; - -use track::Track; +use std::slice::Iter; +use std::sync::Arc; use events::{Event, EventManager}; +use spotify::Spotify; +use track::Track; pub struct Queue { - queue: VecDeque, + // TODO: put this in an RwLock instead of locking the whole Queue struct + queue: Vec, + current_track: Option, + spotify: Arc, ev: EventManager, } -pub enum QueueChange { - Dequeue, - Enqueue, +pub enum QueueEvent { + Add(usize), Remove(usize), Show, } impl Queue { - pub fn new(ev: EventManager) -> Queue { + pub fn new(ev: EventManager, spotify: Arc) -> Queue { Queue { - queue: VecDeque::new(), + queue: Vec::new(), + current_track: None, + spotify: spotify, ev: ev, } } - pub fn remove(&mut self, index: usize) -> Option { - match self.queue.remove(index) { - Some(track) => { - debug!("Removed from queue: {}", &track); - self.ev.send(Event::Queue(QueueChange::Remove(index))); - Some(track) + + pub fn next_index(&self) -> Option { + match self.current_track { + Some(index) => { + let next_index = index + 1; + if next_index < self.queue.len() { + Some(next_index) + } else { + None + } } None => None, } } - pub fn enqueue(&mut self, track: Track) { - debug!("Queued: {}", &track); - self.queue.push_back(track); - self.ev.send(Event::Queue(QueueChange::Enqueue)); + + pub fn get(&self, index: usize) -> &Track { + &self.queue[index] } - pub fn dequeue(&mut self) -> Option { - match self.queue.pop_front() { - Some(track) => { - debug!("Dequeued : {}", track); - self.ev.send(Event::Queue(QueueChange::Dequeue)); - Some(track) - } + + pub fn get_current(&self) -> Option<&Track> { + match self.current_track { + Some(index) => Some(&self.queue[index]), None => None, } } - pub fn peek(&self) -> Option<&Track> { - self.queue.get(0) + + pub fn append(&mut self, track: &Track) { + self.queue.push(track.clone()); + self.ev + .send(Event::Queue(QueueEvent::Add(self.queue.len()))); } + + pub fn append_next(&mut self, track: &Track) -> usize { + if let Some(next_index) = self.next_index() { + self.queue.insert(next_index, track.clone()); + self.ev.send(Event::Queue(QueueEvent::Add(next_index))); + next_index + } else { + self.queue.push(track.clone()); + self.ev + .send(Event::Queue(QueueEvent::Add(self.queue.len() - 1))); + self.queue.len() - 1 + } + } + + pub fn remove(&mut self, index: usize) { + self.queue.remove(index); + self.ev.send(Event::Queue(QueueEvent::Remove(index))); + + // if the queue is empty or we are at the end of the queue, stop + // playback + if self.queue.len() == 0 || index == self.queue.len() { + self.stop(); + return; + } + + // if we are deleting the currently playing track, play the track with + // the same index again, because the next track is now at the position + // of the one we deleted + if let Some(current_track) = self.current_track { + if current_track == index { + self.play(index); + } + } + } + + pub fn play(&mut self, index: usize) { + let track = &self.queue[index]; + self.spotify.load(&track); + self.current_track = Some(index); + self.spotify.play(); + self.spotify.update_track(); + } + + pub fn toggleplayback(&self) { + self.spotify.toggleplayback(); + } + + pub fn stop(&mut self) { + self.spotify.stop(); + self.current_track = None; + } + + pub fn next(&mut self) { + if let Some(next_index) = self.next_index() { + self.play(next_index); + } else { + self.spotify.stop(); + } + } + pub fn iter(&self) -> Iter { self.queue.iter() } diff --git a/src/spotify.rs b/src/spotify.rs index f578384..8bb6515 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -24,14 +24,11 @@ use futures::Future; use futures::Stream; use tokio_core::reactor::Core; -use std::sync::Arc; -use std::sync::Mutex; use std::sync::RwLock; use std::thread; use std::time::{Duration, SystemTime}; use events::{Event, EventManager}; -use queue::Queue; use track::Track; enum WorkerCommand { @@ -41,21 +38,20 @@ enum WorkerCommand { Stop, } -#[derive(Clone)] -pub enum PlayerStatus { +#[derive(Clone, PartialEq)] +pub enum PlayerEvent { Playing, Paused, Stopped, + FinishedTrack, } pub struct Spotify { - status: RwLock, - track: RwLock>, + status: RwLock, pub api: SpotifyAPI, elapsed: RwLock>, since: RwLock>, channel: mpsc::UnboundedSender, - events: EventManager, user: String, } @@ -64,7 +60,6 @@ struct Worker { commands: mpsc::UnboundedReceiver, player: Player, play_task: Box>, - queue: Arc>, } impl Worker { @@ -72,14 +67,12 @@ impl Worker { events: EventManager, commands: mpsc::UnboundedReceiver, player: Player, - queue: Arc>, ) -> Worker { Worker { events: events, commands: commands, player: player, play_task: Box::new(futures::empty()), - queue: queue, } } } @@ -99,21 +92,19 @@ impl futures::Future for Worker { match cmd { WorkerCommand::Load(track) => { self.play_task = Box::new(self.player.load(track.id, false, 0)); - info!("player loading track.."); - self.events.send(Event::PlayerTrack(Some(track))); + info!("player loading track: {:?}", track); } WorkerCommand::Play => { self.player.play(); - self.events.send(Event::PlayerStatus(PlayerStatus::Playing)); + self.events.send(Event::Player(PlayerEvent::Playing)); } WorkerCommand::Pause => { self.player.pause(); - self.events.send(Event::PlayerStatus(PlayerStatus::Paused)); + self.events.send(Event::Player(PlayerEvent::Paused)); } WorkerCommand::Stop => { self.player.stop(); - self.events.send(Event::PlayerTrack(None)); - self.events.send(Event::PlayerStatus(PlayerStatus::Stopped)); + self.events.send(Event::Player(PlayerEvent::Stopped)); } } } @@ -121,19 +112,7 @@ impl futures::Future for Worker { Ok(Async::Ready(())) => { debug!("end of track!"); progress = true; - - let mut queue = self.queue.lock().unwrap(); - if let Some(track) = queue.dequeue() { - debug!("next track in queue: {}", track); - self.play_task = Box::new(self.player.load(track.id, false, 0)); - self.player.play(); - - self.events.send(Event::PlayerTrack(Some(track))); - self.events.send(Event::PlayerStatus(PlayerStatus::Playing)); - } else { - self.events.send(Event::PlayerTrack(None)); - self.events.send(Event::PlayerStatus(PlayerStatus::Stopped)); - } + self.events.send(Event::Player(PlayerEvent::FinishedTrack)); } Ok(Async::NotReady) => (), Err(oneshot::Canceled) => { @@ -152,13 +131,7 @@ impl futures::Future for Worker { } impl Spotify { - pub fn new( - events: EventManager, - user: String, - password: String, - client_id: String, - queue: Arc>, - ) -> Spotify { + pub fn new(events: EventManager, user: String, password: String, client_id: String) -> Spotify { let session_config = SessionConfig::default(); let player_config = PlayerConfig { bitrate: Bitrate::Bitrate320, @@ -180,7 +153,6 @@ impl Spotify { player_config, credentials, client_id, - queue, ) }); } @@ -190,13 +162,11 @@ impl Spotify { let api = SpotifyAPI::default().access_token(&token.access_token); Spotify { - status: RwLock::new(PlayerStatus::Stopped), - track: RwLock::new(None), + status: RwLock::new(PlayerEvent::Stopped), api: api, elapsed: RwLock::new(None), since: RwLock::new(None), channel: tx, - events: events, user: user, } } @@ -209,7 +179,6 @@ impl Spotify { player_config: PlayerConfig, credentials: Credentials, client_id: String, - queue: Arc>, ) { let mut core = Core::new().unwrap(); let handle = core.handle(); @@ -227,13 +196,13 @@ impl Spotify { let (player, _eventchannel) = Player::new(player_config, session, None, move || (backend)(None)); - let worker = Worker::new(events, commands, player, queue); + let worker = Worker::new(events, commands, player); debug!("worker thread ready."); core.run(worker).unwrap(); debug!("worker thread finished."); } - pub fn get_current_status(&self) -> PlayerStatus { + pub fn get_current_status(&self) -> PlayerEvent { let status = self .status .read() @@ -241,14 +210,6 @@ impl Spotify { (*status).clone() } - pub fn get_current_track(&self) -> Option { - let track = self - .track - .read() - .expect("could not acquire read lock on current track"); - (*track).clone() - } - pub fn get_current_progress(&self) -> Duration { self.get_elapsed().unwrap_or(Duration::from_secs(0)) + self @@ -306,23 +267,23 @@ impl Spotify { .user_playlist_tracks(&self.user, playlist_id, None, 50, 0, None) } - pub fn load(&self, track: Track) { + pub fn load(&self, track: &Track) { info!("loading track: {:?}", track); self.channel - .unbounded_send(WorkerCommand::Load(track)) + .unbounded_send(WorkerCommand::Load(track.clone())) .unwrap(); } - pub fn update_status(&self, new_status: PlayerStatus) { + pub fn update_status(&self, new_status: PlayerEvent) { match new_status { - PlayerStatus::Paused => { + PlayerEvent::Paused => { self.set_elapsed(Some(self.get_current_progress())); self.set_since(None); } - PlayerStatus::Playing => { + PlayerEvent::Playing => { self.set_since(Some(SystemTime::now())); } - PlayerStatus::Stopped => { + PlayerEvent::Stopped | PlayerEvent::FinishedTrack => { self.set_elapsed(None); self.set_since(None); } @@ -335,15 +296,9 @@ impl Spotify { *status = new_status; } - pub fn update_track(&self, new_track: Option) { + pub fn update_track(&self) { self.set_elapsed(None); self.set_since(None); - - let mut track = self - .track - .write() - .expect("could not acquire write lock on current track"); - *track = new_track; } pub fn play(&self) { @@ -357,8 +312,8 @@ impl Spotify { .read() .expect("could not acquire read lock on player state"); match *status { - PlayerStatus::Playing => self.pause(), - PlayerStatus::Paused => self.play(), + PlayerEvent::Playing => self.pause(), + PlayerEvent::Paused => self.play(), _ => (), } } diff --git a/src/ui/playlist.rs b/src/ui/playlist.rs index fbc3770..17e39ab 100644 --- a/src/ui/playlist.rs +++ b/src/ui/playlist.rs @@ -1,6 +1,7 @@ use std::sync::{Arc, Mutex}; use cursive::direction::Orientation; +use cursive::event::Key; use cursive::traits::Boxable; use cursive::traits::Identifiable; use cursive::views::*; @@ -41,23 +42,46 @@ impl PlaylistView { } fn create_button(&self, playlist: &SimplifiedPlaylist) -> SplitButton { - let spotify_ref = self.spotify.clone(); - let queue_ref = self.queue.clone(); - - let id = playlist.id.clone(); let collab = match playlist.collaborative { true => "collaborative", false => "", }; let mut button = SplitButton::new(&playlist.name, collab); - button.add_callback(' ', move |_s| { - let tracks = spotify_ref.user_playlist_tracks(&id).unwrap().items; - let mut locked_queue = queue_ref.lock().expect("Could not aquire lock"); - for playlist_track in tracks { - locked_queue.enqueue(Track::new(&playlist_track.track)); - } - }); + + // plays the selected playlist + { + let id = playlist.id.clone(); + let spotify_ref = self.spotify.clone(); + let queue_ref = self.queue.clone(); + button.add_callback(Key::Enter, move |_s| { + let tracks = spotify_ref.user_playlist_tracks(&id).unwrap().items; + let mut locked_queue = queue_ref.lock().expect("Could not aquire lock"); + + let mut first_played = false; + for playlist_track in tracks { + let index = locked_queue.append_next(&Track::new(&playlist_track.track)); + if !first_played { + locked_queue.play(index); + first_played = true; + } + } + }); + } + + // queues the selected playlist + { + let id = playlist.id.clone(); + let spotify_ref = self.spotify.clone(); + let queue_ref = self.queue.clone(); + button.add_callback(' ', move |_s| { + let tracks = spotify_ref.user_playlist_tracks(&id).unwrap().items; + let mut locked_queue = queue_ref.lock().expect("Could not aquire lock"); + for playlist_track in tracks { + locked_queue.append(&Track::new(&playlist_track.track)); + } + }); + } button } diff --git a/src/ui/queue.rs b/src/ui/queue.rs index f4d3889..56f368c 100644 --- a/src/ui/queue.rs +++ b/src/ui/queue.rs @@ -8,8 +8,7 @@ use cursive::Cursive; use std::sync::Arc; use std::sync::Mutex; -use queue::{Queue, QueueChange}; -use spotify::Spotify; +use queue::{Queue, QueueEvent}; use track::Track; use ui::splitbutton::SplitButton; use ui::trackbutton::TrackButton; @@ -17,11 +16,10 @@ use ui::trackbutton::TrackButton; pub struct QueueView { pub view: Option>>>>>, // FIXME: wow queue: Arc>, - spotify: Arc, } impl QueueView { - pub fn new(queue: Arc>, spotify: Arc) -> QueueView { + pub fn new(queue: Arc>) -> QueueView { let queuelist = LinearLayout::new(Orientation::Vertical).with_id("queue_list"); let scrollable = ScrollView::new(queuelist).full_width().full_height(); let panel = Panel::new(scrollable).title("Queue"); @@ -29,7 +27,6 @@ impl QueueView { QueueView { view: Some(panel), queue: queue, - spotify: spotify, } } @@ -41,33 +38,28 @@ impl QueueView { } } - fn cb_play(cursive: &mut Cursive, queue: &mut Queue, spotify: &Spotify) { + fn cb_play(cursive: &mut Cursive, queue: &mut Queue) { let view_ref: Option> = cursive.find_id("queue_list"); if let Some(queuelist) = view_ref { let index = queuelist.get_focus_index(); - let track = queue.remove(index).expect("could not dequeue track"); - spotify.load(track); - spotify.play(); + queue.play(index); } } - pub fn handle_ev(&self, cursive: &mut Cursive, ev: QueueChange) { + pub fn handle_ev(&self, cursive: &mut Cursive, ev: QueueEvent) { let view_ref: Option> = cursive.find_id("queue_list"); if let Some(mut queuelist) = view_ref { match ev { - QueueChange::Enqueue => { + QueueEvent::Add(index) => { let queue = self.queue.lock().expect("could not lock queue"); - let track = queue.peek().expect("queue is empty"); + let track = queue.get(index); let button = self.create_button(&track); - queuelist.insert_child(0, button); + queuelist.insert_child(index, button); } - QueueChange::Dequeue => { - queuelist.remove_child(0); - } - QueueChange::Remove(index) => { + QueueEvent::Remove(index) => { queuelist.remove_child(index); } - QueueChange::Show => self.populate(&mut queuelist), + QueueEvent::Show => self.populate(&mut queuelist), } } } @@ -85,15 +77,13 @@ impl QueueView { }); } - // dequeues the selected track + // plays the selected track { let queue_ref = self.queue.clone(); - let spotify = self.spotify.clone(); button.add_callback(Key::Enter, move |cursive| { Self::cb_play( cursive, &mut queue_ref.lock().expect("could not lock queue"), - &spotify, ); }); } @@ -107,7 +97,7 @@ impl QueueView { let queue = self.queue.lock().expect("could not lock queue"); for track in queue.iter() { - let button = self.create_button(&track); + let button = self.create_button(track); queuelist.add_child(button); } } diff --git a/src/ui/search.rs b/src/ui/search.rs index 0e61ef0..b731c49 100644 --- a/src/ui/search.rs +++ b/src/ui/search.rs @@ -14,7 +14,6 @@ use ui::trackbutton::TrackButton; pub struct SearchView { pub view: Panel, - queue: Arc>, } impl SearchView { @@ -32,23 +31,28 @@ impl SearchView { if let Ok(tracks) = tracks { for search_track in tracks.tracks.items { let track = Track::new(&search_track); - - let s = spotify.clone(); let mut button = TrackButton::new(&track); // plays the selected track - let t = track.clone(); - button.add_callback(Key::Enter, move |_cursive| { - s.load(t.clone()); - s.play(); - }); + { + let queue = queue.clone(); + let track = track.clone(); + button.add_callback(Key::Enter, move |_cursive| { + let mut queue = queue.lock().unwrap(); + let index = queue.append_next(&track); + queue.play(index); + }); + } // queues the selected track - let queue = queue.clone(); - button.add_callback(' ', move |_cursive| { - let mut queue = queue.lock().unwrap(); - queue.enqueue(track.clone()); - }); + { + let queue = queue.clone(); + let track = track.clone(); + button.add_callback(' ', move |_cursive| { + let mut queue = queue.lock().unwrap(); + queue.append(&track); + }); + } results.add_child("", button); } @@ -72,9 +76,6 @@ impl SearchView { .child(searchfield) .child(scrollable); let rootpanel = Panel::new(layout).title("Search"); - return SearchView { - view: rootpanel, - queue: queue, - }; + return SearchView { view: rootpanel }; } } diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs index 6c48673..982fc57 100644 --- a/src/ui/statusbar.rs +++ b/src/ui/statusbar.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use cursive::align::HAlign; use cursive::theme::ColorStyle; @@ -7,15 +7,20 @@ use cursive::vec::Vec2; use cursive::Printer; use unicode_width::UnicodeWidthStr; -use spotify::{PlayerStatus, Spotify}; +use queue::Queue; +use spotify::{PlayerEvent, Spotify}; pub struct StatusBar { + queue: Arc>, spotify: Arc, } impl StatusBar { - pub fn new(spotify: Arc) -> StatusBar { - StatusBar { spotify: spotify } + pub fn new(queue: Arc>, spotify: Arc) -> StatusBar { + StatusBar { + queue: queue, + spotify: spotify, + } } } @@ -40,9 +45,9 @@ impl View for StatusBar { }); let state_icon = match self.spotify.get_current_status() { - PlayerStatus::Playing => " ▶ ", - PlayerStatus::Paused => " ▮▮ ", - PlayerStatus::Stopped => " ◼ ", + PlayerEvent::Playing => " ▶ ", + PlayerEvent::Paused => " ▮▮ ", + PlayerEvent::Stopped | PlayerEvent::FinishedTrack => " ◼ ", } .to_string(); @@ -50,7 +55,12 @@ impl View for StatusBar { printer.print((0, 1), &state_icon); }); - if let Some(ref t) = self.spotify.get_current_track() { + if let Some(ref t) = self + .queue + .lock() + .expect("could not lock queue") + .get_current() + { let elapsed = self.spotify.get_current_progress(); let formatted_elapsed = format!( "{:02}:{:02}",