├── .gitignore ├── .travis.yml ├── Cargo.toml ├── src ├── screens │ ├── mod.rs │ ├── station_delete.rs │ ├── station_add_variety.rs │ ├── track_rate.rs │ ├── station_rename.rs │ ├── station_select.rs │ ├── station_create.rs │ └── station.rs ├── player │ ├── error.rs │ ├── audio.rs │ ├── track_loader.rs │ ├── state.rs │ ├── mod.rs │ └── thread.rs ├── ui │ └── mod.rs ├── main.rs └── state.rs ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target 3 | *.swp 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: nightly 3 | dist: precise 4 | os: linux 5 | before_install: 6 | sudo apt-get install libssl-dev libavcodec-dev libavformat-dev libavdevice-dev libao-dev 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "dobro" 3 | version = "0.2.5" 4 | authors = ["Daniel Rivas "] 5 | 6 | [dependencies] 7 | ncurses = "5.84.0" 8 | ao_rs = "0.1.5" 9 | earwax = "0.1.7" 10 | pandora = "0.1.0" 11 | -------------------------------------------------------------------------------- /src/screens/mod.rs: -------------------------------------------------------------------------------- 1 | mod station; 2 | mod station_add_variety; 3 | mod station_create; 4 | mod station_delete; 5 | mod station_rename; 6 | mod station_select; 7 | mod track_rate; 8 | 9 | pub use self::station::StationScreen; 10 | pub use self::station_add_variety::StationAddVarietyScreen; 11 | pub use self::station_create::StationCreateScreen; 12 | pub use self::station_delete::StationDeleteScreen; 13 | pub use self::station_rename::StationRenameScreen; 14 | pub use self::station_select::StationSelectScreen; 15 | pub use self::track_rate::TrackRateScreen; 16 | -------------------------------------------------------------------------------- /src/screens/station_delete.rs: -------------------------------------------------------------------------------- 1 | use super::super::Dobro; 2 | 3 | use state::*; 4 | 5 | use ncurses as nc; 6 | 7 | pub struct StationDeleteScreen {} 8 | 9 | impl StationDeleteScreen { 10 | pub fn new() -> Self { 11 | StationDeleteScreen {} 12 | } 13 | } 14 | 15 | impl State for StationDeleteScreen { 16 | fn start(&mut self, ctx: &mut Dobro) { 17 | let station = ctx.player().state().station().clone(); 18 | if let Some(station) = station { 19 | nc::printw(&format!("Deleting \"{}\"... ", station.station_name)); 20 | nc::refresh(); 21 | 22 | if let Ok(_) = ctx.pandora().stations().delete(&station) { 23 | nc::printw("Done\n"); 24 | ctx.player_mut().stop(); 25 | } else { 26 | nc::printw("Unable to delete\n"); 27 | } 28 | } 29 | } 30 | 31 | fn update(&mut self, _ctx: &mut Dobro) -> Trans { 32 | Trans::Pop 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Daniel Rivas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/screens/station_add_variety.rs: -------------------------------------------------------------------------------- 1 | use super::super::Dobro; 2 | 3 | use state::*; 4 | 5 | use screens::station_create::StationMusicScreen; 6 | use pandora::music::ToMusicToken; 7 | 8 | use ncurses as nc; 9 | 10 | pub struct StationAddVarietyScreen {} 11 | 12 | impl StationAddVarietyScreen { 13 | pub fn new() -> Self { 14 | StationAddVarietyScreen {} 15 | } 16 | } 17 | 18 | impl StationMusicScreen for StationAddVarietyScreen { 19 | fn message(&self) -> &'static str { 20 | "Add variety from artist or song: " 21 | } 22 | 23 | fn on_choice(&mut self, ctx: &mut Dobro, music_token: &T) 24 | where T: ToMusicToken 25 | { 26 | let station = ctx.player().state().station(); 27 | if let Some(ref station) = station { 28 | nc::printw(&format!("Adding variety to \"{}\"... ", station.station_name)); 29 | nc::refresh(); 30 | if let Ok(_) = ctx.pandora().stations().add_seed(station, music_token) { 31 | nc::printw("Done\n"); 32 | } else { 33 | nc::printw("Unable to add variety to station\n"); 34 | } 35 | } 36 | } 37 | } 38 | 39 | impl State for StationAddVarietyScreen { 40 | fn start(&mut self, ctx: &mut Dobro) { 41 | StationMusicScreen::start(self, ctx); 42 | } 43 | 44 | fn update(&mut self, _ctx: &mut Dobro) -> Trans { 45 | Trans::Pop 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/player/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | 3 | use ao::error::Error as AoError; 4 | use earwax::error::Error as EarwaxError; 5 | use pandora::error::Error as PandoraError; 6 | 7 | /// Composite error type for the player. 8 | #[derive(Debug)] 9 | pub enum Error { 10 | Ao(AoError), 11 | Earwax(EarwaxError), 12 | Pandora(PandoraError), 13 | } 14 | 15 | impl StdError for Error { 16 | fn description(&self) -> &str { 17 | match *self { 18 | Error::Ao(ref e) => e.description(), 19 | Error::Earwax(ref e) => e.description(), 20 | Error::Pandora(ref e) => e.description(), 21 | } 22 | } 23 | 24 | fn cause(&self) -> Option<&StdError> { 25 | match *self { 26 | Error::Ao(ref e) => Some(e), 27 | Error::Earwax(ref e) => Some(e), 28 | Error::Pandora(ref e) => Some(e), 29 | } 30 | } 31 | } 32 | 33 | impl ::std::fmt::Display for Error { 34 | fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { 35 | write!(f, "{:?}", self) 36 | } 37 | } 38 | 39 | impl From for Error { 40 | fn from(error: AoError) -> Error { 41 | Error::Ao(error) 42 | } 43 | } 44 | 45 | impl From for Error { 46 | fn from(error: EarwaxError) -> Error { 47 | Error::Earwax(error) 48 | } 49 | } 50 | 51 | impl From for Error { 52 | fn from(error: PandoraError) -> Error { 53 | Error::Pandora(error) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/screens/track_rate.rs: -------------------------------------------------------------------------------- 1 | use super::super::Dobro; 2 | 3 | use ui::*; 4 | use state::*; 5 | 6 | use ncurses as nc; 7 | 8 | pub struct TrackRateScreen { 9 | is_positive: bool, 10 | } 11 | 12 | impl TrackRateScreen { 13 | pub fn new(is_positive: bool) -> Self { 14 | TrackRateScreen { is_positive: is_positive } 15 | } 16 | } 17 | 18 | impl State for TrackRateScreen { 19 | fn start(&mut self, ctx: &mut Dobro) { 20 | let station = ctx.player().state().station(); 21 | let track = ctx.player().state().track(); 22 | if let Some(station) = station { 23 | if let Some(track) = track { 24 | mvrel(-1, 0); 25 | nc::printw("Rating track... "); 26 | nc::refresh(); 27 | 28 | let res = ctx.pandora() 29 | .stations() 30 | .playlist(&station) 31 | .rate(track, self.is_positive); 32 | match res { 33 | Ok(_) => { 34 | nc::printw("Done\n"); 35 | if !self.is_positive { 36 | ctx.player_mut().skip(); 37 | } else { 38 | ctx.player().report(); 39 | } 40 | } 41 | _ => { 42 | nc::printw("Error\n"); 43 | } 44 | }; 45 | } 46 | } 47 | } 48 | 49 | fn update(&mut self, _ctx: &mut Dobro) -> Trans { 50 | Trans::Pop 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/screens/station_rename.rs: -------------------------------------------------------------------------------- 1 | use super::super::Dobro; 2 | 3 | use ui::*; 4 | use state::*; 5 | 6 | use ncurses as nc; 7 | 8 | pub struct StationRenameScreen {} 9 | 10 | impl StationRenameScreen { 11 | pub fn new() -> Self { 12 | StationRenameScreen {} 13 | } 14 | } 15 | 16 | impl State for StationRenameScreen { 17 | fn start(&mut self, ctx: &mut Dobro) { 18 | let station = ctx.player().state().station(); 19 | if let Some(station) = station { 20 | nc::attron(nc::A_BOLD()); 21 | nc::printw(&format!("Renaming station \"{}\"\n", station.station_name)); 22 | nc::attroff(nc::A_BOLD()); 23 | 24 | nc::printw("New name (blank to cancel): "); 25 | let new_name = getstring().trim().to_owned(); 26 | nc::printw("\n"); 27 | 28 | if new_name.len() > 0 { 29 | nc::printw("Renaming... "); 30 | nc::refresh(); 31 | 32 | if let Ok(_) = ctx.pandora().stations().rename(&station, &new_name) { 33 | nc::printw(&format!("Renamed station to \"{}\"\n", new_name)); 34 | // if let Some(ref mut st) = ctx.player_mut().state().station { 35 | // st.station_name = new_name; 36 | // } 37 | } else { 38 | nc::printw(&format!("Unable to use the name \"{}\"\n", &new_name)); 39 | } 40 | } else { 41 | nc::printw("Leaving old name\n"); 42 | } 43 | } 44 | } 45 | 46 | fn update(&mut self, _ctx: &mut Dobro) -> Trans { 47 | Trans::Pop 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/screens/station_select.rs: -------------------------------------------------------------------------------- 1 | use super::super::Dobro; 2 | use super::StationCreateScreen; 3 | 4 | use ui::*; 5 | use state::*; 6 | 7 | use ncurses as nc; 8 | 9 | pub struct StationSelectScreen {} 10 | 11 | impl StationSelectScreen { 12 | pub fn new() -> Self { 13 | StationSelectScreen {} 14 | } 15 | } 16 | 17 | impl State for StationSelectScreen { 18 | fn update(&mut self, ctx: &mut Dobro) -> Trans { 19 | nc::printw("Fetching Stations... "); 20 | nc::refresh(); 21 | 22 | let stations = ctx.pandora().stations().list().unwrap(); 23 | 24 | nc::printw("Done\n"); 25 | 26 | nc::attron(nc::A_BOLD()); 27 | nc::printw("Stations\n"); 28 | nc::attroff(nc::A_BOLD()); 29 | 30 | if stations.len() <= 0 { 31 | return Trans::Push(Box::new(StationCreateScreen::new())); 32 | } else { 33 | for (index, station) in stations.iter().enumerate() { 34 | nc::printw(&format!("{} - {}\n", index, station.station_name)); 35 | } 36 | 37 | let mut choice; 38 | loop { 39 | nc::attron(nc::A_BOLD()); 40 | nc::printw("Station choice (blank to cancel): "); 41 | nc::attroff(nc::A_BOLD()); 42 | choice = getchoice(); 43 | nc::printw("\n"); 44 | 45 | if choice >= 0 && choice < stations.len() as i32 { 46 | break; 47 | } else if choice < 0 { 48 | return Trans::Pop; 49 | } 50 | } 51 | 52 | ctx.player_mut().play(stations[choice as usize].clone()); 53 | } 54 | 55 | Trans::Pop 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/player/audio.rs: -------------------------------------------------------------------------------- 1 | use super::error::Error; 2 | 3 | use ao; 4 | use earwax::{Earwax, Timestamp, LogLevel}; 5 | 6 | use std::sync::{Once, ONCE_INIT}; 7 | static START: Once = ONCE_INIT; 8 | 9 | /// Type for audio streaming audio that hides the details of earwax and ao-rs handling. 10 | pub struct Audio { 11 | earwax: Earwax, 12 | driver: ao::Driver, 13 | format: ao::Format, 14 | device: ao::Device, 15 | } 16 | 17 | impl Audio { 18 | /// Tries to initialize a new stream for the given URL. 19 | pub fn new(url: &str) -> Result { 20 | // #[cfg(not(debug_assertions))] 21 | START.call_once(|| { Earwax::set_log_level(LogLevel::Error); }); 22 | 23 | let earwax = try!(Earwax::new(url)); 24 | let driver = try!(ao::Driver::new()); 25 | let format = ao::Format::new(); 26 | let device = try!(ao::Device::new(&driver, &format, None)); 27 | 28 | Ok(Audio { 29 | earwax: earwax, 30 | driver: driver, 31 | format: format, 32 | device: device, 33 | }) 34 | } 35 | 36 | /// Plays the next chunk of the stream to the default audio device. 37 | /// # Returns 38 | /// If playback was successful (at getting next stream chunk), the value returned 39 | /// is a tuple where the first element is the current timestamp, and the second 40 | /// element is the total timestamp. 41 | pub fn play(&mut self) -> Result<(Timestamp, Timestamp), ()> { 42 | let duration = self.earwax.info().duration; 43 | if let Some(chunk) = self.earwax.spit() { 44 | self.device.play(chunk.data); 45 | Ok((chunk.time, duration)) 46 | } else { 47 | Err(()) 48 | } 49 | } 50 | 51 | /// Plays all the chunks remaining in the stream to the default audio the device. 52 | pub fn play_all(&mut self) { 53 | while let Some(chunk) = self.earwax.spit() { 54 | self.device.play(chunk.data); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/danielrs/dobro.svg?branch=master)](https://travis-ci.org/danielrs/dobro) 2 | 3 | # dobro 4 | Unofficial Pandora terminal client written in Rust. 5 | 6 | ### Building 7 | 8 | #### Required libraries 9 | 10 | Some modules of the terminal client uses modules that depend on some C libraries for dynamic linking. 11 | 12 | * [earwax][earwax]: Requires ffmpeg 2.8. 13 | * [ao-rs][ao-rs]: Requires libao 1.1. 14 | 15 | #### Compiling 16 | 17 | If everything is installed, a simple `cargo run` with the nightly compiler should suffice for testing the player. 18 | 19 | ### What's going on right now? 20 | 21 | This an app that I'm building during my free time. It will consist of the following main components (most to least important): 22 | 23 | - API interaction (pandora-rs). 24 | - Audio playback. 25 | - Text-based user interface (TUI). 26 | - User Settings. 27 | 28 | Local crates for components can be found at [src/lib](https://github.com/DanielRS/dobro/tree/master/src/lib). 29 | 30 | #### API Interaction (pandora-rs) 31 | Most of the work for this module is already done. It interacts with the API in a very rusty way using [hyper][hyper]; all the requests/responses are serialized/deserialized using [serde][serde] and [serde_json][serde_json]. The pandora-rs module interacts with the API found [here](https://6xq.net/pandora-apidoc/json/). 32 | 33 | #### Audio playback (earwax, ao-rs) 34 | For **audio decoding** I made a small C library with Rust bindings based on [ffmpeg 2.8][ffmpeg] called Earwax. For audio playpack I'm using [libao][libao] with safe ffi bindings. 35 | 36 | #### TUI 37 | Simple interface made with ncurses. This would be the "main" Dobro application, and it builds on the lower-level components. 38 | 39 | #### User settings 40 | After everything else is done. Should load from simple configuration files (preferably in toml format). 41 | 42 | [earwax]: https://github.com/danielrs/earwax 43 | [ao-rs]: https://github.com/danielrs/ao-rs 44 | 45 | [hyper]: https://github.com/hyperium/hyper 46 | [serde]: https://github.com/serde-rs/serde 47 | [serde_json]: https://github.com/serde-rs/json 48 | 49 | [ffmpeg]: https://www.ffmpeg.org/ 50 | [libao]: https://www.xiph.org/ao/ 51 | -------------------------------------------------------------------------------- /src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | use ncurses as nc; 2 | use std::char; 3 | 4 | pub fn mvrel(rel_y: i32, rel_x: i32) { 5 | wmvrel(nc::stdscr(), rel_y, rel_x); 6 | } 7 | 8 | pub fn wmvrel(window: nc::WINDOW, rel_y: i32, rel_x: i32) { 9 | let mut y = 0; 10 | let mut x = 0; 11 | nc::getyx(window, &mut y, &mut x); 12 | nc::wmove(window, y + rel_y, x + rel_x); 13 | } 14 | 15 | pub fn getstring() -> String { 16 | wgetstring(nc::stdscr()) 17 | } 18 | 19 | pub fn wgetstring(window: nc::WINDOW) -> String { 20 | nc::noecho(); 21 | 22 | let mut string = String::with_capacity(32); 23 | let mut pos_history = Vec::new(); 24 | 25 | let mut y = 0; 26 | let mut x = 0; 27 | 28 | nc::getyx(window, &mut y, &mut x); 29 | pos_history.push((y, x)); 30 | 31 | let mut ch = nc::wgetch(window); 32 | while ch != '\n' as i32 && ch != '\r' as i32 { 33 | if ch == 127 { 34 | if let Some((y, x)) = pos_history.pop() { 35 | nc::mvwdelch(window, y, x); 36 | string.pop(); 37 | } 38 | } else if let Some(c) = char::from_u32(ch as u32) { 39 | nc::getyx(window, &mut y, &mut x); 40 | pos_history.push((y, x)); 41 | string.push(c); 42 | nc::wechochar(window, ch as nc::chtype); 43 | } 44 | ch = nc::wgetch(window); 45 | } 46 | 47 | string.shrink_to_fit(); 48 | string 49 | } 50 | 51 | pub fn getsecretstring() -> String { 52 | wgetsecretstring(nc::stdscr()) 53 | } 54 | 55 | pub fn wgetsecretstring(window: nc::WINDOW) -> String { 56 | nc::noecho(); 57 | let mut string = String::with_capacity(32); 58 | 59 | let mut ch = nc::wgetch(window); 60 | while ch != '\n' as i32 && ch != '\r' as i32 { 61 | if ch == 127 { 62 | string.pop(); 63 | } else if let Some(c) = char::from_u32(ch as u32) { 64 | string.push(c); 65 | } 66 | ch = nc::wgetch(window); 67 | } 68 | 69 | string.shrink_to_fit(); 70 | string 71 | } 72 | 73 | pub fn getchoice() -> i32 { 74 | wgetchoice(nc::stdscr()) 75 | } 76 | 77 | pub fn wgetchoice(window: nc::WINDOW) -> i32 { 78 | wgetstring(window).trim().parse::().unwrap_or(-1) 79 | } 80 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! This example asks for user login information, shows the available stations, and lets 2 | //! the user select which station to play. 3 | //! 4 | //! **Becareful**, this example is still too simple. It doesn't handle reconnection 5 | //! to pandora when credentials expire. 6 | 7 | extern crate ncurses; 8 | 9 | extern crate ao_rs as ao; 10 | extern crate earwax; 11 | extern crate pandora; 12 | 13 | mod player; 14 | mod screens; 15 | mod ui; 16 | mod state; 17 | 18 | use ncurses as nc; 19 | 20 | use pandora::Pandora; 21 | 22 | use player::Player; 23 | use state::Automaton; 24 | use screens::StationScreen; 25 | 26 | use std::sync::Arc; 27 | 28 | use ui::*; 29 | 30 | fn main() { 31 | nc::initscr(); 32 | nc::scrollok(nc::stdscr(), true); 33 | nc::noecho(); 34 | 35 | nc::attron(nc::A_BOLD()); 36 | nc::printw("Welcome to Dobro! The unofficial pandora terminal client."); 37 | nc::printw("\nPlease login below"); 38 | nc::attroff(nc::A_BOLD()); 39 | 40 | nc::attron(nc::A_BOLD()); 41 | nc::printw("\nEmail: "); 42 | nc::attroff(nc::A_BOLD()); 43 | let email = getstring(); 44 | 45 | nc::attron(nc::A_BOLD()); 46 | nc::printw("\nPassword: "); 47 | nc::attroff(nc::A_BOLD()); 48 | let password = getsecretstring(); 49 | 50 | nc::printw("\nLogging in... "); 51 | nc::refresh(); 52 | 53 | match Pandora::new(&email.trim(), &password.trim()) { 54 | Ok(pandora) => { 55 | nc::printw("Done\n"); 56 | let mut dobro = Dobro::new(pandora); 57 | let mut automaton = Automaton::new(StationScreen::new()); 58 | 59 | automaton.start(&mut dobro); 60 | 61 | while automaton.is_running() { 62 | automaton.update(&mut dobro); 63 | nc::refresh(); 64 | } 65 | } 66 | Err(_) => { 67 | nc::attron(nc::A_BLINK()); 68 | nc::printw("Unable to connect to pandora using the provided credentials\n"); 69 | nc::attroff(nc::A_BLINK()); 70 | nc::getch(); 71 | } 72 | } 73 | 74 | nc::endwin(); 75 | } 76 | 77 | pub struct Dobro { 78 | pandora: Arc, 79 | player: Player, 80 | } 81 | 82 | impl Dobro { 83 | /// Creates a new Dobro instance. 84 | pub fn new(pandora: Pandora) -> Self { 85 | let pandora = Arc::new(pandora); 86 | 87 | Dobro { 88 | player: Player::new(&pandora), 89 | pandora: pandora, 90 | } 91 | } 92 | 93 | /// Returns a reference to the pandora handler. 94 | pub fn pandora(&self) -> &Arc { 95 | &self.pandora 96 | } 97 | 98 | /// Returns a reference to the player. 99 | pub fn player(&self) -> &Player { 100 | &self.player 101 | } 102 | 103 | /// Returns a mutable reference to the player. 104 | pub fn player_mut(&mut self) -> &mut Player { 105 | &mut self.player 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/player/track_loader.rs: -------------------------------------------------------------------------------- 1 | use pandora::Track; 2 | use super::audio::Audio; 3 | 4 | use std::collections::VecDeque; 5 | use std::sync::{Arc, Mutex, Condvar}; 6 | use std::thread; 7 | 8 | /// TrackLoader type for loading tracks in the background. 9 | pub struct TrackLoader { 10 | tracklist: Arc>>, 11 | next: Arc>>, 12 | fetching: Arc<(Mutex, Condvar)>, 13 | } 14 | 15 | impl TrackLoader { 16 | /// Creates a new TrackLoader form the given tracklist. 17 | pub fn new(tracklist: VecDeque) -> Self { 18 | let mut track_loader = TrackLoader { 19 | tracklist: Arc::new(Mutex::new(tracklist)), 20 | next: Arc::new(Mutex::new(None)), 21 | fetching: Arc::new((Mutex::new(false), Condvar::new())), 22 | }; 23 | // track_loader.fetch(); 24 | track_loader 25 | } 26 | 27 | /// Returns the next track and audio, `None` is no more 28 | /// items available. 29 | pub fn next(&mut self) -> Option<(Track, Audio)> { 30 | // // Wait until we are done fetching. 31 | // { 32 | // let &(ref lock, ref cvar) = &*self.fetching; 33 | // let mut fetching = lock.lock().unwrap(); 34 | // while *fetching { 35 | // fetching = cvar.wait(fetching).unwrap(); 36 | // } 37 | // } 38 | 39 | // let next = self.next.lock().unwrap().take(); 40 | // self.fetch(); 41 | let next = pop_tracklist(self.tracklist.clone()); 42 | return next; 43 | } 44 | 45 | // /// Fetches the next track in the background. 46 | // fn fetch(&mut self) { 47 | // let tracklist = self.tracklist.clone(); 48 | // let next = self.next.clone(); 49 | // let pair = self.fetching.clone(); 50 | 51 | // if tracklist.lock().unwrap().len() > 0 { 52 | // let &(ref lock, ref cvar) = &*self.fetching; 53 | // let mut fetching = lock.lock().unwrap(); 54 | // *fetching = true; 55 | 56 | // thread::spawn(move || { 57 | // if next.lock().unwrap().is_none() { 58 | // *next.lock().unwrap() = pop_tracklist(tracklist); 59 | // } 60 | // let &(ref lock, ref cvar) = &*pair; 61 | // let mut fetching = lock.lock().unwrap(); 62 | // *fetching = false; 63 | // cvar.notify_one(); 64 | // }); 65 | // } 66 | // } 67 | } 68 | 69 | /// Pops the next track from the tracklist and returns it along 70 | /// with the audio. 71 | fn pop_tracklist(tracklist: Arc>>) -> Option<(Track, Audio)> { 72 | if let Some((track, audio)) = 73 | tracklist 74 | .lock() 75 | .unwrap() 76 | .pop_front() 77 | .and_then(|track| track.track_audio.clone().map(|audio| (track, audio))) { 78 | let audio = match Audio::new(&audio.high_quality.audio_url) { 79 | Ok(audio) => audio, 80 | Err(e) => { 81 | return None; 82 | } 83 | }; 84 | return Some((track, audio)); 85 | } 86 | None 87 | } 88 | -------------------------------------------------------------------------------- /src/player/state.rs: -------------------------------------------------------------------------------- 1 | use pandora::{Station, Track}; 2 | 3 | /// Player state. It holds the information for the station, track, progress, 4 | /// and status (Playing, Paused, etc). 5 | #[derive(Debug)] 6 | pub struct PlayerState { 7 | station: Option, 8 | track: Option, 9 | progress: Option<(i64, i64)>, 10 | status: PlayerStatus, 11 | } 12 | 13 | impl PlayerState { 14 | /// Returns a new PlayerState. 15 | pub fn new() -> Self { 16 | PlayerState { 17 | station: None, 18 | track: None, 19 | progress: None, 20 | status: PlayerStatus::Shutdown, 21 | } 22 | } 23 | 24 | pub fn clear_info(&mut self) { 25 | self.station = None; 26 | self.track = None; 27 | self.progress = None; 28 | } 29 | 30 | pub fn station(&self) -> Option { 31 | self.station.clone() 32 | } 33 | 34 | pub fn set_station(&mut self, station: Station) { 35 | self.station = Some(station); 36 | } 37 | 38 | pub fn clear_station(&mut self) { 39 | self.station = None; 40 | } 41 | 42 | pub fn track(&self) -> Option { 43 | self.track.clone() 44 | } 45 | 46 | pub fn set_track(&mut self, track: Track) { 47 | self.track = Some(track); 48 | } 49 | 50 | pub fn clear_track(&mut self) { 51 | self.track = None; 52 | } 53 | 54 | pub fn progress(&self) -> Option<(i64, i64)> { 55 | self.progress.clone() 56 | } 57 | 58 | pub fn set_progress(&mut self, current: i64, end: i64) { 59 | self.progress = Some((current, end)); 60 | } 61 | 62 | pub fn clear_progress(&mut self) { 63 | self.progress = None; 64 | } 65 | 66 | pub fn status(&self) -> PlayerStatus { 67 | self.status.clone() 68 | } 69 | 70 | pub fn set_status(&mut self, status: PlayerStatus) { 71 | self.status = status; 72 | } 73 | } 74 | 75 | /// Enumeration type for showing player status to the user. 76 | #[derive(Debug, Clone)] 77 | pub enum PlayerStatus { 78 | // Station-related statuses. 79 | Standby, 80 | 81 | // Station-related statuses. 82 | Started(Station), 83 | Stopped(Station), 84 | Fetching(Station), 85 | 86 | // Track related statuses. 87 | Playing(Track), 88 | Finished(Track), 89 | Paused(Track), 90 | 91 | // Player not running. 92 | Shutdown, 93 | } 94 | 95 | impl PlayerStatus { 96 | pub fn is_started(&self) -> bool { 97 | match *self { 98 | PlayerStatus::Started(_) => true, 99 | _ => false, 100 | } 101 | } 102 | 103 | pub fn is_stopped(&self) -> bool { 104 | match *self { 105 | PlayerStatus::Stopped(_) => true, 106 | _ => false, 107 | } 108 | } 109 | 110 | pub fn is_fetching(&self) -> bool { 111 | match *self { 112 | PlayerStatus::Fetching(_) => true, 113 | _ => false, 114 | } 115 | } 116 | 117 | pub fn is_playing(&self) -> bool { 118 | match *self { 119 | PlayerStatus::Playing(_) => true, 120 | _ => false, 121 | } 122 | } 123 | 124 | pub fn is_finished(&self) -> bool { 125 | match *self { 126 | PlayerStatus::Finished(_) => true, 127 | _ => false, 128 | } 129 | } 130 | 131 | pub fn is_paused(&self) -> bool { 132 | match *self { 133 | PlayerStatus::Paused(_) => true, 134 | _ => false, 135 | } 136 | } 137 | 138 | pub fn is_shutdown(&self) -> bool { 139 | match *self { 140 | PlayerStatus::Shutdown => true, 141 | _ => false, 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/screens/station_create.rs: -------------------------------------------------------------------------------- 1 | use super::super::Dobro; 2 | 3 | use ui::*; 4 | use state::*; 5 | 6 | use pandora::music::ToMusicToken; 7 | 8 | use ncurses as nc; 9 | 10 | const RESULTS_LENGTH: usize = 3; 11 | 12 | pub struct StationCreateScreen {} 13 | 14 | impl StationCreateScreen { 15 | pub fn new() -> Self { 16 | StationCreateScreen {} 17 | } 18 | } 19 | 20 | impl StationMusicScreen for StationCreateScreen { 21 | fn message(&self) -> &'static str { 22 | "Create station from artist or song: " 23 | } 24 | 25 | fn on_choice(&mut self, ctx: &mut Dobro, music_token: &T) 26 | where T: ToMusicToken 27 | { 28 | nc::printw("Creating station... "); 29 | nc::refresh(); 30 | if let Ok(station) = ctx.pandora().stations().create(music_token) { 31 | nc::printw("Done\n"); 32 | ctx.player_mut().play(station); 33 | } else { 34 | nc::printw("Unable to create station\n"); 35 | } 36 | } 37 | } 38 | 39 | impl State for StationCreateScreen { 40 | fn start(&mut self, ctx: &mut Dobro) { 41 | StationMusicScreen::start(self, ctx); 42 | } 43 | 44 | fn update(&mut self, _ctx: &mut Dobro) -> Trans { 45 | Trans::Pop 46 | } 47 | } 48 | 49 | pub trait StationMusicScreen { 50 | fn message(&self) -> &'static str; 51 | 52 | fn on_choice(&mut self, ctx: &mut Dobro, music_token: &T) where T: ToMusicToken; 53 | 54 | fn start(&mut self, ctx: &mut Dobro) { 55 | use std::cmp::min; 56 | 57 | nc::attron(nc::A_BOLD()); 58 | nc::printw(self.message()); 59 | nc::attroff(nc::A_BOLD()); 60 | let search_string = getstring(); 61 | nc::printw("\n"); 62 | 63 | nc::printw("Searching... "); 64 | nc::refresh(); 65 | if let Ok(results) = ctx.pandora().music().search(&search_string) { 66 | 67 | let artists_len = min(RESULTS_LENGTH, results.artists().len()) as i32; 68 | let songs_len = min(RESULTS_LENGTH, results.songs().len()) as i32; 69 | 70 | if artists_len > 0 || songs_len > 0 { 71 | nc::printw("Done\n"); 72 | 73 | nc::printw("Artists:\n"); 74 | for (i, artist) in results.artists().iter().enumerate().take(RESULTS_LENGTH) { 75 | nc::printw(&format!("{} - {}\n", i, artist.artist_name)); 76 | } 77 | nc::printw("Songs:\n"); 78 | for (i, song) in results.songs().iter().enumerate().take(RESULTS_LENGTH) { 79 | nc::printw(&format!("{} - {} by {}\n", 80 | i as i32 + artists_len, 81 | song.song_name, 82 | song.artist_name)); 83 | } 84 | 85 | let mut music_token = None; 86 | loop { 87 | nc::attron(nc::A_BOLD()); 88 | nc::printw("Music choice (blank to cancel): "); 89 | nc::attroff(nc::A_BOLD()); 90 | let choice = getchoice(); 91 | nc::printw("\n"); 92 | 93 | if choice < 0 { 94 | break; 95 | } else if choice < artists_len { 96 | music_token = Some(results.artists()[choice as usize].to_music_token()); 97 | break; 98 | } else if choice < artists_len + songs_len { 99 | music_token = Some(results.songs()[(choice - artists_len) as usize] 100 | .to_music_token()); 101 | break; 102 | } 103 | } 104 | 105 | if let Some(ref music_token) = music_token { 106 | self.on_choice(ctx, music_token); 107 | } 108 | } else { 109 | nc::printw("No results\n"); 110 | } 111 | } else { 112 | nc::printw("Error\n"); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | //! Pushdown automaton for state machine. 2 | 3 | use super::Dobro; 4 | type Context = Dobro; 5 | 6 | /// Transition functions for the automaton. 7 | pub enum Trans { 8 | /// No transition 9 | None, 10 | 11 | /// Removes the current state on the top of the stack and resumes 12 | /// the one below (stop if there's none). 13 | Pop, 14 | 15 | /// Pauses the current state on top of the stack and puts 16 | /// a new one on top of the stack. 17 | Push(Box), 18 | 19 | /// Replaces the current state on top of the stack with a 20 | /// new one. 21 | Replace(Box), 22 | 23 | /// Remotes all the states and quits. 24 | Quit, 25 | } 26 | 27 | /// A trait for types that can be used by the [Automaton](struct.Automaton.html). 28 | pub trait State { 29 | /// Executed when the state is placed on top of the stack. 30 | fn start(&mut self, _ctx: &mut Context) {} 31 | 32 | /// Executed when the state is being removed from the top of the stack. 33 | fn stop(&mut self, _ctx: &mut Context) {} 34 | 35 | /// Executed when a new state is placed on top of this one. 36 | fn pause(&mut self, _ctx: &mut Context) {} 37 | 38 | /// Executed when this state becomes active once again. 39 | fn resume(&mut self, _ctx: &mut Context) {} 40 | 41 | /// Executed every cycle of the main loop. 42 | fn update(&mut self, _ctx: &mut Context) -> Trans { 43 | Trans::None 44 | } 45 | } 46 | 47 | /// Pushdown automaton. 48 | pub struct Automaton { 49 | /// Set to true whenever the automaton is active and running. 50 | running: bool, 51 | 52 | /// Stack of boxed states. They are bosed so our automaton 53 | /// is the sole owner of the data. 54 | /// 55 | /// Also, the [State](trait.State.html) is not sized, so we need 56 | /// to used either Trait objects or pointers. 57 | state_stack: Vec>, 58 | } 59 | 60 | impl Automaton { 61 | /// Creates a new automaton. 62 | pub fn new(initial_state: T) -> Self 63 | where T: State + 'static 64 | { 65 | Automaton { 66 | running: false, 67 | state_stack: vec![Box::new(initial_state)], 68 | } 69 | } 70 | 71 | /// Checks whether the automaton is running. 72 | pub fn is_running(&self) -> bool { 73 | self.running 74 | } 75 | 76 | /// Initializes the automaton. 77 | /// # Panics 78 | /// Panics if no states are on the stack. 79 | pub fn start(&mut self, ctx: &mut Context) { 80 | if !self.running { 81 | let state = self.state_stack.last_mut().unwrap(); 82 | state.start(ctx); 83 | self.running = true; 84 | } 85 | } 86 | 87 | /// Updates the state on top of the stack. 88 | pub fn update(&mut self, ctx: &mut Context) { 89 | if self.running { 90 | let trans = match self.state_stack.last_mut() { 91 | Some(state) => state.update(ctx), 92 | None => Trans::None, 93 | }; 94 | self.transition(trans, ctx); 95 | } 96 | } 97 | 98 | /// Process a transition. 99 | fn transition(&mut self, trans: Trans, ctx: &mut Context) { 100 | if self.running { 101 | match trans { 102 | Trans::None => (), 103 | Trans::Pop => self.pop(ctx), 104 | Trans::Push(state) => self.push(state, ctx), 105 | Trans::Replace(state) => self.replace(state, ctx), 106 | Trans::Quit => self.quit(ctx), 107 | } 108 | } 109 | } 110 | 111 | /// Pops state from the stack and resumes any state 112 | /// still on top. 113 | fn pop(&mut self, ctx: &mut Context) { 114 | if self.running { 115 | if let Some(mut state) = self.state_stack.pop() { 116 | state.stop(ctx); 117 | } 118 | if let Some(mut state) = self.state_stack.last_mut() { 119 | state.resume(ctx); 120 | } else { 121 | self.running = false; 122 | } 123 | } 124 | } 125 | 126 | /// Pushes a new state on top of the stack and pauses any state 127 | /// that was on top. 128 | fn push(&mut self, state: Box, ctx: &mut Context) { 129 | if self.running { 130 | if let Some(mut state) = self.state_stack.last_mut() { 131 | state.pause(ctx); 132 | } 133 | self.state_stack.push(state); 134 | let state = self.state_stack.last_mut().unwrap(); 135 | state.start(ctx); 136 | } 137 | } 138 | 139 | /// Replaces (if any) state on top of the stack. 140 | fn replace(&mut self, state: Box, ctx: &mut Context) { 141 | if self.running { 142 | if let Some(mut state) = self.state_stack.pop() { 143 | state.stop(ctx); 144 | } 145 | self.state_stack.push(state); 146 | let state = self.state_stack.last_mut().unwrap(); 147 | state.start(ctx); 148 | } 149 | } 150 | 151 | /// Quits the automaton. 152 | fn quit(&mut self, ctx: &mut Context) { 153 | if self.running { 154 | while let Some(mut state) = self.state_stack.pop() { 155 | state.stop(ctx); 156 | } 157 | self.running = false; 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/screens/station.rs: -------------------------------------------------------------------------------- 1 | //! Main screen for the application, and initial state for the state machine; 2 | //! Popping this state means the application should end. 3 | 4 | use super::super::Dobro; 5 | use super::StationAddVarietyScreen; 6 | use super::StationCreateScreen; 7 | use super::StationDeleteScreen; 8 | use super::StationRenameScreen; 9 | use super::StationSelectScreen; 10 | use super::TrackRateScreen; 11 | 12 | use player::PlayerStatus; 13 | use ui::*; 14 | use state::*; 15 | 16 | use pandora::playlist::Track; 17 | 18 | use ncurses as nc; 19 | 20 | static HELP_TEXT: &'static str = "Keybindings: 21 | '?' for help; 22 | 'n' to skip; 23 | 'p' to pause; 24 | 'c' to create station; 25 | 'r' to rename station; 26 | 'a' to add variety to station; 27 | 's' to change station; 28 | 'd' to delete station; 29 | '+' or '-' to rate the current track; 30 | 'q' to quit."; 31 | 32 | pub struct StationScreen {} 33 | 34 | impl StationScreen { 35 | pub fn new() -> Self { 36 | StationScreen {} 37 | } 38 | 39 | fn print_song(status: &str, track: &Track) { 40 | let loved = track.song_rating.unwrap_or(0) > 0; 41 | nc::printw(&format!("{} \"{}\" by {}", 42 | status, 43 | track.song_name.as_ref().unwrap_or(&"Unknown".to_owned()), 44 | track.artist_name.as_ref().unwrap_or(&"Unknown".to_owned()))); 45 | nc::printw(&format!("{}\n", if loved { " <3" } else { "" })); 46 | } 47 | 48 | fn print_progress(ctx: &mut Dobro) { 49 | if let Some((current, total)) = ctx.player().state().progress() { 50 | let total_mins = total / 60; 51 | let total_secs = total % 60; 52 | let mins = current / 60; 53 | let secs = current % 60; 54 | 55 | // Print seconds. 56 | let mut y = 0; 57 | let mut x = 0; 58 | nc::getyx(nc::stdscr(), &mut y, &mut x); 59 | nc::mv(y, 0); 60 | nc::clrtoeol(); 61 | nc::printw(&format!("{:02}:{:02}/{:02}:{:02}", 62 | mins, 63 | secs, 64 | total_mins, 65 | total_secs)); 66 | } 67 | nc::printw("\n"); 68 | } 69 | } 70 | 71 | impl State for StationScreen { 72 | fn resume(&mut self, ctx: &mut Dobro) { 73 | let status = ctx.player().state().status().clone(); 74 | 75 | match status { 76 | PlayerStatus::Playing(_) | 77 | PlayerStatus::Paused(_) => { 78 | nc::printw("\n\n"); 79 | ctx.player().report(); 80 | } 81 | _ => (), 82 | }; 83 | } 84 | 85 | fn update(&mut self, ctx: &mut Dobro) -> Trans { 86 | if let Some(mstatus) = ctx.player().try_next_status() { 87 | match mstatus { 88 | Ok(status) => { 89 | match status { 90 | PlayerStatus::Standby => { 91 | return Trans::Push(Box::new(StationSelectScreen::new())); 92 | } 93 | 94 | PlayerStatus::Started(station) => { 95 | nc::printw("Type '?' for help.\n"); 96 | nc::attron(nc::A_BOLD()); 97 | nc::printw(&format!("Station \"{}\"\n", station.station_name)); 98 | nc::attroff(nc::A_BOLD()); 99 | nc::printw("\n\n"); 100 | } 101 | PlayerStatus::Stopped(_) => { 102 | mvrel(-2, 0); 103 | } 104 | PlayerStatus::Fetching(_) => { 105 | mvrel(-2, 0); 106 | nc::printw("Fetching playlist..."); 107 | nc::printw("\n\n"); 108 | } 109 | 110 | PlayerStatus::Playing(track) => { 111 | mvrel(-2, 0); 112 | Self::print_song("Playing", &track); 113 | Self::print_progress(ctx); 114 | } 115 | PlayerStatus::Finished(track) => { 116 | mvrel(-2, 0); 117 | Self::print_song("Finished", &track); 118 | nc::printw("\n\n"); 119 | } 120 | PlayerStatus::Paused(track) => { 121 | mvrel(-2, 0); 122 | Self::print_song("Paused", &track); 123 | Self::print_progress(ctx); 124 | } 125 | 126 | _ => (), 127 | } 128 | } 129 | Err(err) => { 130 | mvrel(-1, 0); 131 | nc::printw(&format!("ERROR: {}", err)); 132 | nc::printw("\n\n"); 133 | } 134 | } 135 | } 136 | if ctx.player().is_playing() { 137 | mvrel(-1, 0); 138 | Self::print_progress(ctx); 139 | } 140 | 141 | nc::timeout(100); 142 | let ch = nc::getch(); 143 | nc::timeout(-1); 144 | 145 | match ch as u8 as char { 146 | '?' => { 147 | nc::printw(&format!("{}\n\n\n", HELP_TEXT)); 148 | ctx.player().report(); 149 | } 150 | 'n' => ctx.player_mut().skip(), 151 | 'p' => ctx.player_mut().toggle_pause(), 152 | 'c' => return Trans::Push(Box::new(StationCreateScreen::new())), 153 | 'r' => return Trans::Push(Box::new(StationRenameScreen::new())), 154 | 'a' => return Trans::Push(Box::new(StationAddVarietyScreen::new())), 155 | 's' => return Trans::Push(Box::new(StationSelectScreen::new())), 156 | 'd' => return Trans::Push(Box::new(StationDeleteScreen::new())), 157 | rate @ '-' | rate @ '+' => { 158 | return Trans::Push(Box::new(TrackRateScreen::new(rate == '+'))) 159 | } 160 | 'q' => return Trans::Quit, 161 | _ => return Trans::None, 162 | }; 163 | 164 | Trans::None 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/player/mod.rs: -------------------------------------------------------------------------------- 1 | mod audio; 2 | mod error; 3 | mod state; 4 | mod thread; 5 | mod track_loader; 6 | 7 | use self::error::Error; 8 | pub use self::state::{PlayerState, PlayerStatus}; 9 | use self::thread::spawn_player; 10 | 11 | use ao; 12 | use pandora::{Pandora, Station}; 13 | 14 | use std::thread::JoinHandle; 15 | use std::sync::{Arc, Mutex, MutexGuard}; 16 | use std::sync::mpsc::{channel, Sender, Receiver}; 17 | 18 | /// Facade type for controlling the player thread, it use channels 19 | /// for communication. 20 | pub struct Player { 21 | #[allow(dead_code)] 22 | ao: ao::Ao, 23 | player_handle: Option>, 24 | 25 | // Player state. 26 | state: Arc>, 27 | 28 | // Sender for notifying the player thread of different actions. 29 | // Receiver for getting player status. 30 | sender: Sender, 31 | receiver: Receiver>, 32 | } 33 | 34 | impl Drop for Player { 35 | fn drop(&mut self) { 36 | // Thread needs to be running to receive a message 37 | // so we need to unpause it. 38 | self.unpause(); 39 | 40 | // Notifies the thread to exit. 41 | self.sender.send(PlayerAction::Exit).unwrap(); 42 | 43 | // Waits for the thread to stop. 44 | if let Some(player_handle) = self.player_handle.take() { 45 | player_handle.join().unwrap(); 46 | } 47 | } 48 | } 49 | 50 | impl Player { 51 | /// Creates a new Player. 52 | pub fn new(pandora: &Arc) -> Self { 53 | // Initialize AO before anything else. 54 | let ao = ao::Ao::new(); 55 | 56 | let state = Arc::new(Mutex::new(PlayerState::new())); 57 | 58 | let (external_sender, receiver) = channel(); 59 | let (sender, external_receiver) = channel(); 60 | let player_handle = spawn_player(pandora, &state, sender, receiver); 61 | 62 | Player { 63 | ao: ao, 64 | player_handle: Some(player_handle), 65 | 66 | state: state, 67 | 68 | sender: external_sender, 69 | receiver: external_receiver, 70 | } 71 | } 72 | 73 | /// Returns the player state. Note that this function is synchronized with the player 74 | /// thread, meaning that it blocks until "state" is available. 75 | pub fn state(&self) -> MutexGuard { 76 | self.state.lock().unwrap() 77 | } 78 | 79 | // 80 | // Player control functions 81 | // 82 | 83 | /// Starts playing the given station. 84 | pub fn play(&mut self, station: Station) { 85 | self.unpause(); 86 | self.sender.send(PlayerAction::Play(station)).unwrap(); 87 | } 88 | 89 | /// Stops the current station. 90 | pub fn stop(&mut self) { 91 | self.sender.send(PlayerAction::Stop).unwrap(); 92 | } 93 | 94 | /// Pauses the audio thread. 95 | pub fn pause(&mut self) { 96 | self.sender.send(PlayerAction::Pause).unwrap();; 97 | } 98 | 99 | /// Unpauses the audio thread. 100 | pub fn unpause(&mut self) { 101 | self.sender.send(PlayerAction::Unpause).unwrap(); 102 | } 103 | 104 | /// Skips the current track (if any is playing). 105 | pub fn skip(&mut self) { 106 | self.unpause(); 107 | self.sender.send(PlayerAction::Skip).unwrap(); 108 | } 109 | 110 | /// Toggles pause / unpause. 111 | pub fn toggle_pause(&mut self) { 112 | if self.is_paused() { 113 | self.unpause(); 114 | } else { 115 | self.pause(); 116 | } 117 | } 118 | 119 | /// Requests the player to send an event reporting its current status. 120 | pub fn report(&self) { 121 | self.sender.send(PlayerAction::Report).unwrap(); 122 | } 123 | 124 | // 125 | // PLayer state functions 126 | // 127 | 128 | /// Returns true if the player is starting. 129 | pub fn is_started(&self) -> bool { 130 | self.state.lock().unwrap().status().is_started() 131 | } 132 | 133 | /// Returns true if the player is stopped (waiting for a Play action). 134 | pub fn is_stopped(&self) -> bool { 135 | self.state.lock().unwrap().status().is_stopped() 136 | } 137 | 138 | /// Returns true if the player is fetching tracks. 139 | pub fn is_fetching(&self) -> bool { 140 | self.state.lock().unwrap().status().is_fetching() 141 | } 142 | 143 | /// Returns true if the player is playing audio. 144 | pub fn is_playing(&self) -> bool { 145 | self.state.lock().unwrap().status().is_playing() 146 | } 147 | 148 | /// Returns true if the player has just finished audio. 149 | pub fn is_finished(&self) -> bool { 150 | self.state.lock().unwrap().status().is_finished() 151 | } 152 | 153 | /// Returns true if the player is paused. 154 | pub fn is_paused(&self) -> bool { 155 | self.state.lock().unwrap().status().is_paused() 156 | } 157 | 158 | /// Returns true if the player is shutdown. 159 | pub fn is_shutdown(&self) -> bool { 160 | self.state.lock().unwrap().status().is_shutdown() 161 | } 162 | 163 | /// Returns the most recent status from the player. 164 | /// 165 | /// # Returns 166 | /// * Result(PlayerStatus) when the thread is running and emitting 167 | /// messages. 168 | /// * Error(err) when the thread sent an error instead. 169 | pub fn next_status(&self) -> Result { 170 | self.receiver.recv().unwrap() 171 | } 172 | 173 | /// Returns the most recent status from the player without blocking. 174 | /// 175 | /// # Returns 176 | /// * Result(PlayerStatus) when the thread is running and emitting 177 | /// messages. 178 | /// * Error(err) when the thread sent an error instead. 179 | pub fn try_next_status(&self) -> Option> { 180 | if let Ok(s) = self.receiver.try_recv() { 181 | Some(s) 182 | } else { 183 | None 184 | } 185 | } 186 | } 187 | 188 | /// Enumeration type for sending player actions. 189 | pub enum PlayerAction { 190 | // Station related actions. 191 | Play(Station), 192 | Stop, 193 | 194 | // Track related actions. 195 | Pause, 196 | Unpause, 197 | Skip, 198 | 199 | // Misc actions. 200 | Report, 201 | Exit, 202 | } 203 | -------------------------------------------------------------------------------- /src/player/thread.rs: -------------------------------------------------------------------------------- 1 | use super::audio::Audio; 2 | use super::error::Error; 3 | use super::track_loader::TrackLoader; 4 | use super::PlayerAction; 5 | use super::state::{PlayerState, PlayerStatus}; 6 | 7 | use ao; 8 | use pandora::{Pandora, Station, Track}; 9 | 10 | use std::thread; 11 | use std::thread::JoinHandle; 12 | use std::sync::{Arc, Mutex, Condvar}; 13 | use std::sync::mpsc::{channel, Sender, Receiver}; 14 | 15 | /// This function starts the event and player thread. 16 | pub fn spawn_player(pandora: &Arc, 17 | main_state: &Arc>, 18 | main_sender: Sender>, 19 | main_receiver: Receiver) 20 | -> JoinHandle<()> { 21 | 22 | // The Condvar used for pausing the player thread. 23 | let main_pause_pair: Arc<(Mutex, Condvar)> = Arc::new((Mutex::new(false), 24 | Condvar::new())); 25 | 26 | // Sender and receiver for communication between the event thread and 27 | // the player thread. 28 | let (event_sender, event_receiver) = channel(); 29 | 30 | // Thread is dedicated to receive the events from the main thread and 31 | // forward player events to the player thread. 32 | let state = main_state.clone(); 33 | let pause_pair = main_pause_pair.clone(); 34 | let receiver = main_receiver; 35 | let sender = main_sender.clone(); 36 | 37 | let event_handle = { 38 | thread::Builder::new() 39 | .name("event".to_string()) 40 | .spawn(move || while let Ok(action) = receiver.recv() { 41 | match action { 42 | PlayerAction::Pause => { 43 | let &(ref lock, _) = &*pause_pair; 44 | let mut paused = lock.lock().unwrap(); 45 | *paused = true; 46 | } 47 | PlayerAction::Unpause => { 48 | let &(ref lock, ref cvar) = &*pause_pair; 49 | let mut paused = lock.lock().unwrap(); 50 | *paused = false; 51 | cvar.notify_one(); 52 | } 53 | 54 | PlayerAction::Report => { 55 | sender 56 | .send(Ok(state.lock().unwrap().status().clone())) 57 | .unwrap(); 58 | } 59 | 60 | PlayerAction::Exit => { 61 | event_sender.send(PlayerAction::Exit).unwrap(); 62 | break; 63 | } 64 | 65 | action => { 66 | event_sender.send(action).unwrap(); 67 | } 68 | } 69 | }) 70 | .unwrap() 71 | }; 72 | 73 | // The 'player' thread runs while the Player is in scope. It plays the given station 74 | // and takes care of fetching the tracks. All the events this thread receives are 75 | // the events forwarded from the 'event' thread. 76 | let pandora = pandora.clone(); 77 | let state = main_state.clone(); 78 | let pause_pair = main_pause_pair.clone(); 79 | let sender = main_sender.clone(); 80 | 81 | thread::Builder::new() 82 | .name("player".to_string()) 83 | .spawn(move || { 84 | // Context of our player. 85 | let mut ctx = ThreadContext { 86 | pandora: pandora, 87 | state: state, 88 | pause_pair: pause_pair, 89 | sender: sender, 90 | receiver: event_receiver, 91 | }; 92 | 93 | // Finite state machine loop. 94 | let mut fsm = ThreadFSM::new(); 95 | ctx.send_status(PlayerStatus::Standby); 96 | while !fsm.is_shutdown() { 97 | fsm = fsm.update(&mut ctx); 98 | } 99 | 100 | event_handle.join().unwrap(); 101 | }) 102 | .unwrap() 103 | } 104 | 105 | // ---------------- 106 | // Finite State Machine 107 | // ---------------- 108 | 109 | /// Context struct for our finite state machine. 110 | struct ThreadContext { 111 | pub pandora: Arc, 112 | pub state: Arc>, 113 | pub pause_pair: Arc<(Mutex, Condvar)>, 114 | pub sender: Sender>, 115 | pub receiver: Receiver, 116 | } 117 | 118 | impl ThreadContext { 119 | /// Sends a status through the sender channel. 120 | pub fn send_status(&mut self, status: PlayerStatus) { 121 | self.state.lock().unwrap().set_status(status.clone()); 122 | self.sender.send(Ok(status)).unwrap(); 123 | } 124 | 125 | /// Sends an error through the sender channel. 126 | pub fn send_error(&self, error: Error) { 127 | self.sender.send(Err(error)).unwrap(); 128 | } 129 | 130 | /// Blocks the current thread and returns the next available 131 | /// action. 132 | pub fn action(&mut self) -> Option { 133 | self.receiver.recv().ok() 134 | } 135 | 136 | /// Checks for a pending action without blocking the current 137 | /// thread. 138 | pub fn try_action(&mut self) -> Option { 139 | self.receiver.try_recv().ok() 140 | } 141 | } 142 | 143 | /// Finite state machine for the thread. 144 | enum ThreadFSM { 145 | Shutdown, 146 | 147 | Standby, 148 | 149 | Station { station: Station }, 150 | 151 | Track { 152 | station: Station, 153 | track_loader: TrackLoader, 154 | }, 155 | 156 | Playing { 157 | station: Station, 158 | track_loader: TrackLoader, 159 | track: Track, 160 | audio: Audio, 161 | }, 162 | } 163 | 164 | impl ThreadFSM { 165 | /// Creates a new state machine in Standby state. 166 | pub fn new() -> ThreadFSM { 167 | ThreadFSM::Standby 168 | } 169 | 170 | /// Returns true if the current state is Standby state. 171 | pub fn is_standby(&self) -> bool { 172 | match *self { 173 | ThreadFSM::Standby => true, 174 | _ => false, 175 | } 176 | } 177 | 178 | /// Returns true if the current state is Shutdown state. 179 | pub fn is_shutdown(&self) -> bool { 180 | match *self { 181 | ThreadFSM::Shutdown => true, 182 | _ => false, 183 | } 184 | } 185 | 186 | // ---------------- 187 | // Updating. 188 | // ---------------- 189 | 190 | /// Consumes the current state and returns a new state. 191 | pub fn update(self, ctx: &mut ThreadContext) -> ThreadFSM { 192 | match self { 193 | ThreadFSM::Standby => Self::update_standby(ctx), 194 | 195 | ThreadFSM::Station { station } => Self::update_station(ctx, station), 196 | 197 | ThreadFSM::Track { 198 | station, 199 | track_loader, 200 | } => Self::update_track(ctx, station, track_loader), 201 | 202 | ThreadFSM::Playing { 203 | station, 204 | track_loader, 205 | track, 206 | audio, 207 | } => Self::update_playing(ctx, station, track_loader, track, audio), 208 | 209 | _ => self, 210 | } 211 | } 212 | 213 | fn update_standby(ctx: &mut ThreadContext) -> ThreadFSM { 214 | if let Some(PlayerAction::Play(station)) = ctx.action() { 215 | ctx.state.lock().unwrap().set_station(station.clone()); 216 | ctx.send_status(PlayerStatus::Started(station.clone())); 217 | return Self::new_station(station); 218 | } 219 | 220 | // Stay in Standby state. 221 | Self::new() 222 | } 223 | 224 | fn update_station(ctx: &mut ThreadContext, station: Station) -> ThreadFSM { 225 | ctx.send_status(PlayerStatus::Fetching(station.clone())); 226 | match ctx.pandora.stations().playlist(&station).list() { 227 | Ok(tracklist) => { 228 | Self::new_track(station, TrackLoader::new(tracklist.into_iter().collect())) 229 | } 230 | Err(e) => { 231 | ctx.send_error(e.into()); 232 | Self::new_station(station) 233 | } 234 | } 235 | } 236 | 237 | fn update_track(ctx: &mut ThreadContext, 238 | station: Station, 239 | mut track_loader: TrackLoader) 240 | -> ThreadFSM { 241 | if let Some((track, audio)) = track_loader.next() { 242 | ctx.state.lock().unwrap().set_track(track.clone()); 243 | ctx.send_status(PlayerStatus::Playing(track.clone())); 244 | return Self::new_playing(station, track_loader, track, audio); 245 | } 246 | Self::new_station(station) 247 | } 248 | 249 | fn update_playing(ctx: &mut ThreadContext, 250 | station: Station, 251 | track_loader: TrackLoader, 252 | track: Track, 253 | mut audio: Audio) 254 | -> ThreadFSM { 255 | // Pauses. 256 | { 257 | let &(ref lock, ref cvar) = &*ctx.pause_pair.clone(); 258 | let mut paused = lock.lock().unwrap(); 259 | while *paused { 260 | ctx.send_status(PlayerStatus::Paused(track.clone())); 261 | paused = cvar.wait(paused).unwrap(); 262 | ctx.send_status(PlayerStatus::Playing(track.clone())); 263 | } 264 | } 265 | 266 | // Actions. 267 | if let Some(action) = ctx.try_action() { 268 | match action { 269 | PlayerAction::Play(new_station) => { 270 | ctx.state.lock().unwrap().clear_info(); 271 | ctx.state.lock().unwrap().set_station(new_station.clone()); 272 | ctx.send_status(PlayerStatus::Finished(track.clone())); 273 | ctx.send_status(PlayerStatus::Stopped(station.clone())); 274 | ctx.send_status(PlayerStatus::Started(new_station.clone())); 275 | return Self::new_station(new_station); 276 | } 277 | PlayerAction::Stop => { 278 | ctx.state.lock().unwrap().clear_info(); 279 | ctx.send_status(PlayerStatus::Finished(track.clone())); 280 | ctx.send_status(PlayerStatus::Stopped(station.clone())); 281 | ctx.send_status(PlayerStatus::Standby); 282 | return Self::new(); 283 | } 284 | 285 | PlayerAction::Skip => { 286 | ctx.state.lock().unwrap().clear_track(); 287 | ctx.state.lock().unwrap().clear_progress(); 288 | ctx.send_status(PlayerStatus::Finished(track.clone())); 289 | return Self::new_track(station, track_loader); 290 | } 291 | 292 | PlayerAction::Exit => { 293 | ctx.state.lock().unwrap().clear_info(); 294 | ctx.send_status(PlayerStatus::Finished(track.clone())); 295 | ctx.send_status(PlayerStatus::Stopped(station.clone())); 296 | ctx.send_status(PlayerStatus::Shutdown); 297 | return Self::new_shutdown(); 298 | } 299 | 300 | _ => (), 301 | } 302 | } 303 | 304 | // Playback. 305 | if let Ok((current, duration)) = audio.play() { 306 | ctx.state 307 | .lock() 308 | .unwrap() 309 | .set_progress(current.seconds(), duration.seconds()); 310 | } else { 311 | ctx.state.lock().unwrap().clear_track(); 312 | ctx.state.lock().unwrap().clear_progress(); 313 | ctx.send_status(PlayerStatus::Finished(track.clone())); 314 | return Self::new_track(station, track_loader); 315 | } 316 | 317 | return Self::new_playing(station, track_loader, track, audio); 318 | } 319 | 320 | // ---------------- 321 | // Creation of different states. 322 | // ---------------- 323 | 324 | fn new_shutdown() -> ThreadFSM { 325 | ThreadFSM::Shutdown 326 | } 327 | 328 | fn new_station(station: Station) -> ThreadFSM { 329 | ThreadFSM::Station { station: station } 330 | } 331 | 332 | fn new_track(station: Station, track_loader: TrackLoader) -> ThreadFSM { 333 | ThreadFSM::Track { 334 | station: station, 335 | track_loader: track_loader, 336 | } 337 | } 338 | 339 | fn new_playing(station: Station, 340 | track_loader: TrackLoader, 341 | track: Track, 342 | audio: Audio) 343 | -> ThreadFSM { 344 | ThreadFSM::Playing { 345 | station: station, 346 | track_loader: track_loader, 347 | track: track, 348 | audio: audio, 349 | } 350 | } 351 | } 352 | --------------------------------------------------------------------------------