initial commit
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -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
|
||||||
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "ncspot"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Henrik Friedrichsen <henrik@affekt.org>"]
|
||||||
|
|
||||||
|
[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"
|
||||||
18
src/config.rs
Normal file
18
src/config.rs
Normal file
@@ -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<Config, toml::de::Error> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
78
src/main.rs
Normal file
78
src/main.rs
Normal file
@@ -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<Self>, &mut spotify::Spotify);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: FnOnce(&mut spotify::Spotify) -> () + Send> CbSpotify for F {
|
||||||
|
fn call_box(self: Box<Self>, 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();
|
||||||
|
}
|
||||||
173
src/spotify.rs
Normal file
173
src/spotify.rs
Normal file
@@ -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<WorkerCommand>,
|
||||||
|
token: Token,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Worker {
|
||||||
|
commands: mpsc::UnboundedReceiver<WorkerCommand>,
|
||||||
|
player: Player,
|
||||||
|
play_task: Box<futures::Future<Item = (), Error = oneshot::Canceled>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Worker {
|
||||||
|
fn new(commands: mpsc::UnboundedReceiver<WorkerCommand>, 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<WorkerCommand>,
|
||||||
|
token_channel: oneshot::Sender<Token>,
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/theme.rs
Normal file
16
src/theme.rs
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user