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
This commit is contained in:
Henrik Friedrichsen
2019-03-06 23:56:11 +01:00
parent c3fcb3ce21
commit 0b14fc5da7
8 changed files with 232 additions and 173 deletions

View File

@@ -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<Track>),
Queue(QueueEvent),
Player(PlayerEvent),
Playlist(PlaylistEvent),
}

View File

@@ -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),
}
}

View File

@@ -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<Track>,
// TODO: put this in an RwLock instead of locking the whole Queue struct
queue: Vec<Track>,
current_track: Option<usize>,
spotify: Arc<Spotify>,
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<Spotify>) -> Queue {
Queue {
queue: VecDeque::new(),
queue: Vec::new(),
current_track: None,
spotify: spotify,
ev: ev,
}
}
pub fn remove(&mut self, index: usize) -> Option<Track> {
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<usize> {
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<Track> {
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<Track> {
self.queue.iter()
}

View File

@@ -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<PlayerStatus>,
track: RwLock<Option<Track>>,
status: RwLock<PlayerEvent>,
pub api: SpotifyAPI,
elapsed: RwLock<Option<Duration>>,
since: RwLock<Option<SystemTime>>,
channel: mpsc::UnboundedSender<WorkerCommand>,
events: EventManager,
user: String,
}
@@ -64,7 +60,6 @@ struct Worker {
commands: mpsc::UnboundedReceiver<WorkerCommand>,
player: Player,
play_task: Box<futures::Future<Item = (), Error = oneshot::Canceled>>,
queue: Arc<Mutex<Queue>>,
}
impl Worker {
@@ -72,14 +67,12 @@ impl Worker {
events: EventManager,
commands: mpsc::UnboundedReceiver<WorkerCommand>,
player: Player,
queue: Arc<Mutex<Queue>>,
) -> 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<Mutex<Queue>>,
) -> 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<Mutex<Queue>>,
) {
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<Track> {
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<Track>) {
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(),
_ => (),
}
}

View File

@@ -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));
}
});
// <enter> 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;
}
}
});
}
// <space> 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
}

View File

@@ -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<Panel<BoxView<BoxView<ScrollView<IdView<LinearLayout>>>>>>, // FIXME: wow
queue: Arc<Mutex<Queue>>,
spotify: Arc<Spotify>,
}
impl QueueView {
pub fn new(queue: Arc<Mutex<Queue>>, spotify: Arc<Spotify>) -> QueueView {
pub fn new(queue: Arc<Mutex<Queue>>) -> 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<ViewRef<LinearLayout>> = 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<ViewRef<LinearLayout>> = 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 {
});
}
// <enter> dequeues the selected track
// <enter> 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);
}
}

View File

@@ -14,7 +14,6 @@ use ui::trackbutton::TrackButton;
pub struct SearchView {
pub view: Panel<LinearLayout>,
queue: Arc<Mutex<Queue>>,
}
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);
// <enter> 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);
});
}
// <space> 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 };
}
}

View File

@@ -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<Mutex<Queue>>,
spotify: Arc<Spotify>,
}
impl StatusBar {
pub fn new(spotify: Arc<Spotify>) -> StatusBar {
StatusBar { spotify: spotify }
pub fn new(queue: Arc<Mutex<Queue>>, spotify: Arc<Spotify>) -> 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}",