From 8ddf4498ba37c13f67a30c9a99388095fe2ae224 Mon Sep 17 00:00:00 2001 From: Henrik Friedrichsen Date: Sun, 11 Nov 2018 00:01:00 +0100 Subject: [PATCH] initial commit --- .gitignore | 10 +++ Cargo.toml | 18 +++++ src/config.rs | 18 +++++ src/main.rs | 78 ++++++++++++++++++++++ src/spotify.rs | 173 +++++++++++++++++++++++++++++++++++++++++++++++++ src/theme.rs | 16 +++++ 6 files changed, 313 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/config.rs create mode 100644 src/main.rs create mode 100644 src/spotify.rs create mode 100644 src/theme.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..50c8301 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ec6ccf5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "ncspot" +version = "0.1.0" +authors = ["Henrik Friedrichsen "] + +[dependencies] +cursive = "0.9" +crossbeam-channel = "0.2" +env_logger = "0.5.13" +futures = "0.1" +log = "0.4.6" +serde = "1.0" +serde_derive = "1.0" +toml = "0.4" +tokio-core = "0.1" + +[dependencies.librespot] +git = "https://github.com/librespot-org/librespot.git" \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..4ca5b84 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,18 @@ +use std::fs::File; +use std::io::prelude::*; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Config { + pub username: String, + pub password: String, + pub client_id: String, +} + +pub fn load(filename: &str) -> Result { + let mut f = File::open(filename).expect("ncspot configuration file not found"); + let mut contents = String::new(); + f.read_to_string(&mut contents) + .expect("something went wrong reading the file"); + + toml::from_str(&contents) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f55ffc4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,78 @@ +extern crate crossbeam_channel; +extern crate cursive; +extern crate futures; +extern crate librespot; +extern crate tokio_core; + +#[macro_use] +extern crate serde_derive; +extern crate serde; +extern crate toml; + +#[macro_use] +extern crate log; +extern crate env_logger; + +use std::env; +use std::io; +use std::io::prelude::*; +use std::path::PathBuf; +use std::process; + +use cursive::views::*; +use cursive::CbFunc; +use cursive::Cursive; + +use librespot::core::spotify_id::SpotifyId; + +mod config; +mod spotify; +mod theme; + +pub trait CbSpotify: Send { + fn call_box(self: Box, &mut spotify::Spotify); +} + +impl () + Send> CbSpotify for F { + fn call_box(self: Box, s: &mut spotify::Spotify) { + (*self)(s) + } +} + +fn main() { + std::env::set_var("RUST_LOG", "ncspot=trace"); + env_logger::init(); + + // let mut cursive = Cursive::default(); + // cursive.add_global_callback('q', |s| s.quit()); + // cursive.set_theme(theme::default()); + + let path = match env::var_os("HOME") { + None => { + println!("$HOME not set."); + process::exit(1) + } + Some(path) => PathBuf::from(format!("{0}/.config/ncspot", path.into_string().unwrap())), + }; + + let cfg = config::load(path.to_str().unwrap()).expect("could not load configuration file"); + + let spotify = spotify::Spotify::new(cfg.username, cfg.password, cfg.client_id); + + // let track = SpotifyId::from_base62("24zYR2ozYbnhhwulk2NLD4").expect("could not load track"); + + // spotify.load(track); + // thread::sleep(time::Duration::new(3, 0)); + // spotify.play(); + // thread::sleep(time::Duration::new(3, 0)); + // spotify.pause(); + // thread::sleep(time::Duration::new(3, 0)); + // spotify.play(); + + // thread::sleep(time::Duration::new(8, 0)); + // spotify.load(track); + // spotify.play(); + + let _ = io::stdin().read(&mut [0u8]).unwrap(); + // cursive.run(); +} diff --git a/src/spotify.rs b/src/spotify.rs new file mode 100644 index 0000000..fe7fc3b --- /dev/null +++ b/src/spotify.rs @@ -0,0 +1,173 @@ +use librespot::core::authentication::Credentials; +use librespot::core::config::SessionConfig; +use librespot::core::keymaster::get_token; +use librespot::core::keymaster::Token; +use librespot::core::session::Session; +use librespot::core::spotify_id::SpotifyId; +use librespot::playback::config::PlayerConfig; + +use librespot::playback::audio_backend; +use librespot::playback::config::Bitrate; +use librespot::playback::player::Player; + +use futures; +use futures::sync::mpsc; +use futures::sync::oneshot; +use futures::Async; +use futures::Future; +use futures::Stream; +use tokio_core::reactor::Core; + +use std::thread; + +enum WorkerCommand { + Load(SpotifyId), + Play, + Pause, + Stop, +} + +pub struct Spotify { + channel: mpsc::UnboundedSender, + token: Token, +} + +struct Worker { + commands: mpsc::UnboundedReceiver, + player: Player, + play_task: Box>, +} + +impl Worker { + fn new(commands: mpsc::UnboundedReceiver, player: Player) -> Worker { + Worker { + commands: commands, + player: player, + play_task: Box::new(futures::empty()), + } + } +} + +impl futures::Future for Worker { + type Item = (); + type Error = (); + + fn poll(&mut self) -> futures::Poll<(), ()> { + loop { + let mut progress = false; + + trace!("Worker is polling"); + if let Async::Ready(Some(cmd)) = self.commands.poll().unwrap() { + progress = true; + debug!("message received!"); + match cmd { + WorkerCommand::Load(track) => { + self.play_task = Box::new(self.player.load(track, false, 0)); + info!("player loading track.."); + } + WorkerCommand::Play => self.player.play(), + WorkerCommand::Pause => self.player.pause(), + WorkerCommand::Stop => self.player.stop(), + } + } + match self.play_task.poll() { + Ok(Async::Ready(())) => { + debug!("end of track!"); + progress = true; + } + Ok(Async::NotReady) => (), + Err(oneshot::Canceled) => { + debug!("player task is over!"); + self.play_task = Box::new(futures::empty()); + } + } + + info!("worker done"); + if !progress { + trace!("handing executor to other tasks"); + return Ok(Async::NotReady); + } + } + } +} + +impl Spotify { + pub fn new(user: String, password: String, client_id: String) -> Spotify { + let session_config = SessionConfig::default(); + let player_config = PlayerConfig { + bitrate: Bitrate::Bitrate320, + normalisation: false, + normalisation_pregain: 0.0, + }; + let credentials = Credentials::with_password(user.clone(), password.clone()); + + let (tx, rx) = mpsc::unbounded(); + let (p, c) = oneshot::channel(); + thread::spawn(move || { + Spotify::worker(rx, p, session_config, player_config, credentials, client_id) + }); + + let spotify = Spotify { + channel: tx, + token: c.wait().unwrap(), + }; + + info!("token received: {:?}", spotify.token); + + spotify + } + + fn worker( + commands: mpsc::UnboundedReceiver, + token_channel: oneshot::Sender, + session_config: SessionConfig, + player_config: PlayerConfig, + credentials: Credentials, + client_id: String, + ) { + let mut core = Core::new().unwrap(); + let handle = core.handle(); + + let session = core + .run(Session::connect(session_config, credentials, None, handle)) + .ok() + .unwrap(); + + let scopes = "user-read-private,playlist-read-private,playlist-read-collaborative,playlist-modify-public,playlist-modify-private,user-follow-modify,user-follow-read,user-library-read,user-library-modify,user-top-read,user-read-recently-played"; + let token = core.run(get_token(&session, &client_id, &scopes)).unwrap(); + token_channel.send(token).unwrap(); + + let backend = audio_backend::find(None).unwrap(); + let (player, _eventchannel) = + Player::new(player_config, session, None, move || (backend)(None)); + + let worker = Worker::new(commands, player); + debug!("worker thread ready."); + core.run(worker).unwrap(); + debug!("worker thread finished."); + } + + pub fn run(&mut self) { + println!("Spotify::run() finished"); + } + + pub fn load(&mut self, track: SpotifyId) { + info!("loading track: {:?}", track); + self.channel.unbounded_send(WorkerCommand::Load(track)).unwrap(); + } + + pub fn play(&mut self) { + info!("play()"); + self.channel.unbounded_send(WorkerCommand::Play).unwrap(); + } + + pub fn pause(&mut self) { + info!("pause()"); + self.channel.unbounded_send(WorkerCommand::Pause).unwrap(); + } + + pub fn stop(&mut self) { + info!("stop()"); + self.channel.unbounded_send(WorkerCommand::Stop).unwrap(); + } +} diff --git a/src/theme.rs b/src/theme.rs new file mode 100644 index 0000000..f84b7f9 --- /dev/null +++ b/src/theme.rs @@ -0,0 +1,16 @@ +use cursive::theme::Color::*; +use cursive::theme::PaletteColor::*; +use cursive::theme::*; + +pub fn default() -> Theme { + let mut palette = Palette::default(); + let borders = BorderStyle::None; + + palette[Background] = TerminalDefault; + + Theme { + shadow: false, + palette: palette, + borders: borders, + } +}