diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 299bae0..4fd4288 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -25,7 +25,7 @@ use protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackR pub struct SpircTask { player: Player, mixer: Box, - linear_volume: bool, + config: SpircTaskConfig, sequence: SeqGenerator, @@ -41,6 +41,7 @@ pub struct SpircTask { shutdown: bool, session: Session, context_fut: Box>, + autoplay_fut: Box>, context: Option, } @@ -55,6 +56,11 @@ pub enum SpircCommand { Shutdown, } +struct SpircTaskConfig { + linear_volume: bool, + autoplay: bool, +} + const CONTEXT_TRACKS_HISTORY: usize = 10; const CONTEXT_FETCH_THRESHOLD: u32 = 5; @@ -243,14 +249,16 @@ impl Spirc { let (cmd_tx, cmd_rx) = mpsc::unbounded(); let volume = config.volume; - let linear_volume = config.linear_volume; - + let task_config = SpircTaskConfig { + linear_volume: config.linear_volume, + autoplay: config.autoplay, + }; let device = initial_device_state(config); let mut task = SpircTask { player: player, mixer: mixer, - linear_volume: linear_volume, + config: task_config, sequence: SeqGenerator::new(1), @@ -268,6 +276,7 @@ impl Spirc { session: session.clone(), context_fut: Box::new(future::empty()), + autoplay_fut: Box::new(future::empty()), context: None, }; @@ -345,7 +354,7 @@ impl Future for SpircTask { Ok(Async::NotReady) => (), Err(oneshot::Canceled) => self.end_of_track = Box::new(future::empty()), } - + // TODO: Refactor match self.context_fut.poll() { Ok(Async::Ready(value)) => { let r_context = serde_json::from_value::(value.clone()); @@ -378,6 +387,20 @@ impl Future for SpircTask { error!("ContextError: {:?}", err) } } + + match self.autoplay_fut.poll() { + Ok(Async::Ready(autoplay_station_uri)) => { + info!("Autoplay uri resolved to <{:?}>", autoplay_station_uri); + self.context_fut = self.resolve_station(&autoplay_station_uri); + progress = true; + self.autoplay_fut = Box::new(future::empty()); + } + Ok(Async::NotReady) => (), + Err(err) => { + self.autoplay_fut = Box::new(future::empty()); + error!("AutoplayError: {:?}", err) + } + } } let poll_sender = self.sender.poll_complete().unwrap(); @@ -657,12 +680,13 @@ impl SpircTask { fn handle_next(&mut self) { let mut new_index = self.consume_queued_track() as u32; let mut continue_playing = true; + let tracks_len = self.state.get_track().len() as u32; debug!( "At track {:?} of {:?} <{:?}> update [{}]", new_index, self.state.get_track().len(), self.state.get_context_uri(), - self.state.get_track().len() as u32 - new_index < CONTEXT_FETCH_THRESHOLD + tracks_len - new_index < CONTEXT_FETCH_THRESHOLD ); let context_uri = self.state.get_context_uri().to_owned(); if (context_uri.starts_with("spotify:station:") || context_uri.starts_with("spotify:dailymix:")) @@ -671,8 +695,14 @@ impl SpircTask { self.context_fut = self.resolve_station(&context_uri); self.update_tracks_from_context(); } - - if new_index >= self.state.get_track().len() as u32 { + if self.config.autoplay && new_index == tracks_len - 1 { + // Extend the playlist + // Note: This doesn't seem to reflect in the UI + // the additional tracks in the frame don't show up as with station view + debug!("Extending playlist <{}>", context_uri); + self.update_tracks_from_context(); + } + if new_index >= tracks_len { new_index = 0; // Loop around back to start continue_playing = self.state.get_repeat(); } @@ -761,6 +791,17 @@ impl SpircTask { self.resolve_uri(&radio_uri) } + fn resolve_autoplay_uri(&self, uri: &str) -> Box> { + let query_uri = format!("hm://autoplay-enabled/query?uri={}", uri); + let request = self.session.mercury().get(query_uri); + Box::new(request.and_then(move |response| { + let data = response.payload.first().expect("Empty autoplay uri").to_vec(); + let autoplay_uri = String::from_utf8(data).unwrap(); + + Ok(autoplay_uri) + })) + } + fn resolve_uri(&self, uri: &str) -> Box> { let request = self.session.mercury().get(uri); @@ -793,6 +834,8 @@ impl SpircTask { { self.state.set_playing_track_index(new_index); } + } else { + warn!("No context to update from!"); } } @@ -804,6 +847,10 @@ impl SpircTask { debug!("Frame has {:?} tracks", tracks.len()); if context_uri.starts_with("spotify:station:") || context_uri.starts_with("spotify:dailymix:") { self.context_fut = self.resolve_station(&context_uri); + } else if self.config.autoplay { + info!("Fetching autoplay context uri"); + // Get autoplay_station_uri for regular playlists + self.autoplay_fut = self.resolve_autoplay_uri(&context_uri); } self.state.set_playing_track_index(index); @@ -884,7 +931,8 @@ impl SpircTask { fn set_volume(&mut self, volume: u16) { self.device.set_volume(volume as u32); - self.mixer.set_volume(volume_to_mixer(volume, self.linear_volume)); + self.mixer + .set_volume(volume_to_mixer(volume, self.config.linear_volume)); if let Some(cache) = self.session.cache() { cache.save_volume(Volume { volume }) } diff --git a/core/src/config.rs b/core/src/config.rs index 293e2e9..7c05321 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -85,4 +85,5 @@ pub struct ConnectConfig { pub device_type: DeviceType, pub volume: u16, pub linear_volume: bool, + pub autoplay: bool, } diff --git a/src/main.rs b/src/main.rs index e3718fb..e193257 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,18 +4,18 @@ extern crate getopts; extern crate librespot; #[macro_use] extern crate log; +extern crate hex; extern crate rpassword; +extern crate sha1; extern crate tokio_core; extern crate tokio_io; extern crate tokio_process; extern crate tokio_signal; extern crate url; -extern crate sha1; -extern crate hex; -use sha1::{Sha1, Digest}; use futures::sync::mpsc::UnboundedReceiver; use futures::{Async, Future, Poll, Stream}; +use sha1::{Digest, Sha1}; use std::env; use std::io::{self, stderr, Write}; use std::mem; @@ -188,6 +188,11 @@ fn setup(args: &[String]) -> Setup { "", "linear-volume", "increase volume linear instead of logarithmic.", + ) + .optflag( + "", + "autoplay", + "autoplay similar songs when your music ends.", ); let matches = match opts.parse(&args[1..]) { @@ -249,7 +254,8 @@ fn setup(args: &[String]) -> Setup { panic!("Initial volume must be in the range 0-100"); } (volume as i32 * 0xFFFF / 100) as u16 - }).or_else(|| cache.as_ref().and_then(Cache::volume)) + }) + .or_else(|| cache.as_ref().and_then(Cache::volume)) .unwrap_or(0x8000); let zeroconf_port = matches @@ -334,6 +340,7 @@ fn setup(args: &[String]) -> Setup { device_type: device_type, volume: initial_volume, linear_volume: matches.opt_present("linear-volume"), + autoplay: matches.opt_present("autoplay"), } }; @@ -503,13 +510,14 @@ impl Future for Main { if let Some(ref program) = self.player_event_program { let child = run_program_on_events(event, program) .expect("program failed to start") - .map(|status| if !status.success() { - error!("child exited with status {:?}", status.code()); + .map(|status| { + if !status.success() { + error!("child exited with status {:?}", status.code()); + } }) .map_err(|e| error!("failed to wait on child process: {}", e)); self.handle.spawn(child); - } } }