mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Merge pull request #394 from ashthespy/autoplay
Autoplay similar songs when your music ends
This commit is contained in:
commit
78bd0a1f7b
3 changed files with 73 additions and 16 deletions
|
@ -25,7 +25,7 @@ use protocol::spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackR
|
||||||
pub struct SpircTask {
|
pub struct SpircTask {
|
||||||
player: Player,
|
player: Player,
|
||||||
mixer: Box<Mixer>,
|
mixer: Box<Mixer>,
|
||||||
linear_volume: bool,
|
config: SpircTaskConfig,
|
||||||
|
|
||||||
sequence: SeqGenerator<u32>,
|
sequence: SeqGenerator<u32>,
|
||||||
|
|
||||||
|
@ -41,6 +41,7 @@ pub struct SpircTask {
|
||||||
shutdown: bool,
|
shutdown: bool,
|
||||||
session: Session,
|
session: Session,
|
||||||
context_fut: Box<Future<Item = serde_json::Value, Error = MercuryError>>,
|
context_fut: Box<Future<Item = serde_json::Value, Error = MercuryError>>,
|
||||||
|
autoplay_fut: Box<Future<Item = String, Error = MercuryError>>,
|
||||||
context: Option<StationContext>,
|
context: Option<StationContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +56,11 @@ pub enum SpircCommand {
|
||||||
Shutdown,
|
Shutdown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SpircTaskConfig {
|
||||||
|
linear_volume: bool,
|
||||||
|
autoplay: bool,
|
||||||
|
}
|
||||||
|
|
||||||
const CONTEXT_TRACKS_HISTORY: usize = 10;
|
const CONTEXT_TRACKS_HISTORY: usize = 10;
|
||||||
const CONTEXT_FETCH_THRESHOLD: u32 = 5;
|
const CONTEXT_FETCH_THRESHOLD: u32 = 5;
|
||||||
|
|
||||||
|
@ -243,14 +249,16 @@ impl Spirc {
|
||||||
let (cmd_tx, cmd_rx) = mpsc::unbounded();
|
let (cmd_tx, cmd_rx) = mpsc::unbounded();
|
||||||
|
|
||||||
let volume = config.volume;
|
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 device = initial_device_state(config);
|
||||||
|
|
||||||
let mut task = SpircTask {
|
let mut task = SpircTask {
|
||||||
player: player,
|
player: player,
|
||||||
mixer: mixer,
|
mixer: mixer,
|
||||||
linear_volume: linear_volume,
|
config: task_config,
|
||||||
|
|
||||||
sequence: SeqGenerator::new(1),
|
sequence: SeqGenerator::new(1),
|
||||||
|
|
||||||
|
@ -268,6 +276,7 @@ impl Spirc {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
|
|
||||||
context_fut: Box::new(future::empty()),
|
context_fut: Box::new(future::empty()),
|
||||||
|
autoplay_fut: Box::new(future::empty()),
|
||||||
context: None,
|
context: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -345,7 +354,7 @@ impl Future for SpircTask {
|
||||||
Ok(Async::NotReady) => (),
|
Ok(Async::NotReady) => (),
|
||||||
Err(oneshot::Canceled) => self.end_of_track = Box::new(future::empty()),
|
Err(oneshot::Canceled) => self.end_of_track = Box::new(future::empty()),
|
||||||
}
|
}
|
||||||
|
// TODO: Refactor
|
||||||
match self.context_fut.poll() {
|
match self.context_fut.poll() {
|
||||||
Ok(Async::Ready(value)) => {
|
Ok(Async::Ready(value)) => {
|
||||||
let r_context = serde_json::from_value::<StationContext>(value.clone());
|
let r_context = serde_json::from_value::<StationContext>(value.clone());
|
||||||
|
@ -378,6 +387,20 @@ impl Future for SpircTask {
|
||||||
error!("ContextError: {:?}", err)
|
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();
|
let poll_sender = self.sender.poll_complete().unwrap();
|
||||||
|
@ -657,12 +680,13 @@ impl SpircTask {
|
||||||
fn handle_next(&mut self) {
|
fn handle_next(&mut self) {
|
||||||
let mut new_index = self.consume_queued_track() as u32;
|
let mut new_index = self.consume_queued_track() as u32;
|
||||||
let mut continue_playing = true;
|
let mut continue_playing = true;
|
||||||
|
let tracks_len = self.state.get_track().len() as u32;
|
||||||
debug!(
|
debug!(
|
||||||
"At track {:?} of {:?} <{:?}> update [{}]",
|
"At track {:?} of {:?} <{:?}> update [{}]",
|
||||||
new_index,
|
new_index,
|
||||||
self.state.get_track().len(),
|
self.state.get_track().len(),
|
||||||
self.state.get_context_uri(),
|
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();
|
let context_uri = self.state.get_context_uri().to_owned();
|
||||||
if (context_uri.starts_with("spotify:station:") || context_uri.starts_with("spotify:dailymix:"))
|
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.context_fut = self.resolve_station(&context_uri);
|
||||||
self.update_tracks_from_context();
|
self.update_tracks_from_context();
|
||||||
}
|
}
|
||||||
|
if self.config.autoplay && new_index == tracks_len - 1 {
|
||||||
if new_index >= self.state.get_track().len() as u32 {
|
// 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
|
new_index = 0; // Loop around back to start
|
||||||
continue_playing = self.state.get_repeat();
|
continue_playing = self.state.get_repeat();
|
||||||
}
|
}
|
||||||
|
@ -761,6 +791,17 @@ impl SpircTask {
|
||||||
self.resolve_uri(&radio_uri)
|
self.resolve_uri(&radio_uri)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_autoplay_uri(&self, uri: &str) -> Box<Future<Item = String, Error = MercuryError>> {
|
||||||
|
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<Future<Item = serde_json::Value, Error = MercuryError>> {
|
fn resolve_uri(&self, uri: &str) -> Box<Future<Item = serde_json::Value, Error = MercuryError>> {
|
||||||
let request = self.session.mercury().get(uri);
|
let request = self.session.mercury().get(uri);
|
||||||
|
|
||||||
|
@ -793,6 +834,8 @@ impl SpircTask {
|
||||||
{
|
{
|
||||||
self.state.set_playing_track_index(new_index);
|
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());
|
debug!("Frame has {:?} tracks", tracks.len());
|
||||||
if context_uri.starts_with("spotify:station:") || context_uri.starts_with("spotify:dailymix:") {
|
if context_uri.starts_with("spotify:station:") || context_uri.starts_with("spotify:dailymix:") {
|
||||||
self.context_fut = self.resolve_station(&context_uri);
|
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);
|
self.state.set_playing_track_index(index);
|
||||||
|
@ -884,7 +931,8 @@ impl SpircTask {
|
||||||
|
|
||||||
fn set_volume(&mut self, volume: u16) {
|
fn set_volume(&mut self, volume: u16) {
|
||||||
self.device.set_volume(volume as u32);
|
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() {
|
if let Some(cache) = self.session.cache() {
|
||||||
cache.save_volume(Volume { volume })
|
cache.save_volume(Volume { volume })
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,4 +85,5 @@ pub struct ConnectConfig {
|
||||||
pub device_type: DeviceType,
|
pub device_type: DeviceType,
|
||||||
pub volume: u16,
|
pub volume: u16,
|
||||||
pub linear_volume: bool,
|
pub linear_volume: bool,
|
||||||
|
pub autoplay: bool,
|
||||||
}
|
}
|
||||||
|
|
20
src/main.rs
20
src/main.rs
|
@ -4,18 +4,18 @@ extern crate getopts;
|
||||||
extern crate librespot;
|
extern crate librespot;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
|
extern crate hex;
|
||||||
extern crate rpassword;
|
extern crate rpassword;
|
||||||
|
extern crate sha1;
|
||||||
extern crate tokio_core;
|
extern crate tokio_core;
|
||||||
extern crate tokio_io;
|
extern crate tokio_io;
|
||||||
extern crate tokio_process;
|
extern crate tokio_process;
|
||||||
extern crate tokio_signal;
|
extern crate tokio_signal;
|
||||||
extern crate url;
|
extern crate url;
|
||||||
extern crate sha1;
|
|
||||||
extern crate hex;
|
|
||||||
|
|
||||||
use sha1::{Sha1, Digest};
|
|
||||||
use futures::sync::mpsc::UnboundedReceiver;
|
use futures::sync::mpsc::UnboundedReceiver;
|
||||||
use futures::{Async, Future, Poll, Stream};
|
use futures::{Async, Future, Poll, Stream};
|
||||||
|
use sha1::{Digest, Sha1};
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::io::{self, stderr, Write};
|
use std::io::{self, stderr, Write};
|
||||||
use std::mem;
|
use std::mem;
|
||||||
|
@ -188,6 +188,11 @@ fn setup(args: &[String]) -> Setup {
|
||||||
"",
|
"",
|
||||||
"linear-volume",
|
"linear-volume",
|
||||||
"increase volume linear instead of logarithmic.",
|
"increase volume linear instead of logarithmic.",
|
||||||
|
)
|
||||||
|
.optflag(
|
||||||
|
"",
|
||||||
|
"autoplay",
|
||||||
|
"autoplay similar songs when your music ends.",
|
||||||
);
|
);
|
||||||
|
|
||||||
let matches = match opts.parse(&args[1..]) {
|
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");
|
panic!("Initial volume must be in the range 0-100");
|
||||||
}
|
}
|
||||||
(volume as i32 * 0xFFFF / 100) as u16
|
(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);
|
.unwrap_or(0x8000);
|
||||||
|
|
||||||
let zeroconf_port = matches
|
let zeroconf_port = matches
|
||||||
|
@ -334,6 +340,7 @@ fn setup(args: &[String]) -> Setup {
|
||||||
device_type: device_type,
|
device_type: device_type,
|
||||||
volume: initial_volume,
|
volume: initial_volume,
|
||||||
linear_volume: matches.opt_present("linear-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 {
|
if let Some(ref program) = self.player_event_program {
|
||||||
let child = run_program_on_events(event, program)
|
let child = run_program_on_events(event, program)
|
||||||
.expect("program failed to start")
|
.expect("program failed to start")
|
||||||
.map(|status| if !status.success() {
|
.map(|status| {
|
||||||
|
if !status.success() {
|
||||||
error!("child exited with status {:?}", status.code());
|
error!("child exited with status {:?}", status.code());
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.map_err(|e| error!("failed to wait on child process: {}", e));
|
.map_err(|e| error!("failed to wait on child process: {}", e));
|
||||||
|
|
||||||
self.handle.spawn(child);
|
self.handle.spawn(child);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue