Skip to content

Commit

Permalink
Sync state of the player with Spotify on startup (related #18)
Browse files Browse the repository at this point in the history
  • Loading branch information
udoprog committed Apr 1, 2019
1 parent 5f47ef1 commit d0bde83
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 88 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Web-based overlay with current song ([#22]).
- Player will no longer pause the current song (if it's playing) and will instead synchronize the state of the player with Spotify ([#18]).

### Changed
- Cleaned up old cruft in the codebase (`gfx` module).
- Moved log configuration to external file (see [example log4rs.yaml]).

[example log4rs.yaml]: log4rs.yaml

[#18]: https://github.com/udoprog/setmod/issues/18
[#22]: https://github.com/udoprog/setmod/issues/22

[Unreleased]: https://github.com/udoprog/setmod/compare/0.2.3...HEAD
Expand Down
30 changes: 4 additions & 26 deletions bot/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,30 +34,9 @@ fn opts() -> clap::App<'static, 'static> {
}

/// Configure logging.
fn setup_logs(root: &Path) -> Result<log4rs::Handle, failure::Error> {
use log4rs::{
append::{console::ConsoleAppender, file::FileAppender},
config::{Appender, Config, Root},
encode::pattern::PatternEncoder,
};

let stdout = ConsoleAppender::builder().build();

let file = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d} - {m}{n}")))
.build(root.join("setmod.log"))?;

let config = Config::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.appender(Appender::builder().build("file", Box::new(file)))
.build(
Root::builder()
.appender("stdout")
.appender("file")
.build(log::LevelFilter::Info),
)?;

Ok(log4rs::init_config(config)?)
fn setup_logs(root: &Path) -> Result<(), failure::Error> {
log4rs::init_file(root.join("log4rs.yaml"), Default::default())?;
Ok(())
}

fn main() -> Result<(), failure::Error> {
Expand All @@ -78,14 +57,13 @@ fn main() -> Result<(), failure::Error> {
.map(PathBuf::from)
.unwrap_or_else(|| root.join("web"));

let handle = setup_logs(root).context("failed to setup logs")?;
setup_logs(root).context("failed to setup logs")?;

match try_main(&root, &web_root, &config) {
Err(e) => utils::log_err("bot crashed", e),
Ok(()) => log::info!("bot was shut down"),
}

drop(handle);
Ok(())
}

Expand Down
113 changes: 89 additions & 24 deletions bot/src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,19 +118,21 @@ impl Item {

#[derive(Debug)]
pub enum Command {
// Skip the current song.
/// Skip the current song.
Skip(EventKind),
// Toggle playback.
/// Toggle playback.
Toggle(EventKind),
// Pause playback.
/// Pause playback.
Pause(EventKind),
// Start playback.
/// Start playback.
Play(EventKind),
// The queue was modified.
/// Start playback on a specific song state.
PlaySync(EventKind, Song),
/// The queue was modified.
Modified(EventKind),
// Set the gain of the player.
/// Set the gain of the player.
Volume(EventKind, u32),
// Play the given item as a theme at the given offset.
/// Play the given item as a theme at the given offset.
Inject(EventKind, Arc<Item>, Duration),
}

Expand All @@ -154,13 +156,7 @@ pub fn run(
// For sending notifications.
global_bus: Arc<bus::Bus>,
) -> Result<(PlaybackFuture, Player), failure::Error> {
let (commands_tx, commands) = mpsc::unbounded();

let connect_config = self::connect::Config {
device: config.device.clone(),
};

let (player, player_interface) = connect::setup(core, &connect_config, spotify.clone())?;
let (player, player_interface) = connect::setup(spotify.clone())?;

let bus = Arc::new(RwLock::new(Bus::new(1024)));

Expand Down Expand Up @@ -219,6 +215,8 @@ pub fn run(
pop_front: None,
};

let (commands_tx, commands) = mpsc::unbounded();

let future = PlaybackFuture {
player,
commands,
Expand All @@ -236,24 +234,74 @@ pub fn run(
};

let player = Player {
player_interface,
player_interface: player_interface.clone(),
queue,
max_queue_length: config.max_queue_length,
max_songs_per_user: config.max_songs_per_user,
spotify,
spotify: spotify.clone(),
commands_tx,
bus,
volume: Arc::clone(&volume),
volume: volume.clone(),
song: song.clone(),
themes: parent_config.themes.clone(),
closed: closed.clone(),
};

// NB: make sure player is paused.
player.pause(EventKind::Automatic)?;
let set_default = match core.run(spotify.me_player())? {
// make use of the information on the current playback to get the local player into a good state.
Some(playback) => {
if let (Some(progress_ms), Some(track)) = (playback.progress_ms, playback.item) {
let track_id: TrackId = str::parse(&track.id)?;
let elapsed = Duration::from_millis(progress_ms as u64);
let duration = Duration::from_millis(track.duration_ms.into());

let item = Arc::new(Item {
track_id,
track,
user: None,
duration,
});

let new_song = Song::new(item, elapsed);

if let Some(volume) = config.volume {
player.volume(EventKind::Automatic, volume)?;
player.volume(EventKind::Automatic, playback.device.volume_percent)?;

if playback.is_playing {
player.play_sync(EventKind::Automatic, new_song)?;
} else {
player.pause(EventKind::Automatic)?;
}

player_interface.set_device(playback.device, false);
false
} else {
true
}
}
None => true,
};

if set_default {
let devices = core.run(spotify.my_player_devices())?;

for (i, device) in devices.iter().enumerate() {
log::info!("device #{}: {}", i, device.name)
}

let device = match config.device.as_ref() {
Some(device) => devices.into_iter().find(|d| d.name == *device),
None => devices.into_iter().next(),
};

if let Some(device) = device {
player_interface.set_device(device, false);
}

player.pause(EventKind::Automatic)?;

if let Some(volume) = config.volume {
player.volume(EventKind::Automatic, volume)?;
}
}

Ok((future, player))
Expand All @@ -275,7 +323,7 @@ pub enum Event {
}

/// Information on current song.
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct Song {
pub item: Arc<Item>,
/// Since the last time it was unpaused, what was the initial elapsed duration.
Expand Down Expand Up @@ -432,6 +480,11 @@ impl Player {
self.bus.write().add_rx()
}

/// Synchronize playback with the given song.
pub fn play_sync(&self, kind: EventKind, song: Song) -> Result<(), failure::Error> {
self.send(Command::PlaySync(kind, song))
}

/// Pause playback.
pub fn pause(&self, kind: EventKind) -> Result<(), failure::Error> {
self.send(Command::Pause(kind))
Expand Down Expand Up @@ -481,9 +534,11 @@ impl PlayerClient {
self.player_interface.list_devices()
}

/// Set which device to perform playback from.
/// External call to set device.
///
/// Should always notify the player to change.
pub fn set_device(&self, device: spotify::Device) {
self.player_interface.set_device(device)
self.player_interface.set_device(device, true)
}

/// Send the given command.
Expand Down Expand Up @@ -1231,6 +1286,16 @@ impl PlaybackFuture {
self.write_song(None);
}
}
Command::PlaySync(kind, mut song) => {
log::trace!("synchronize the state of the player with the given song");
song.play();
kind.checked(&mut self.player, &self.bus, |p| p.play_sync(&song));
// Notify the global bus that we are playing a song.
self.global_bus
.send(bus::Message::from_song(Some(&song), false));
self.paused = false;
*self.song.write() = Some(song);
}
// queue was modified in some way
Command::Modified(kind) => {
if !self.paused && self.song.read().is_none() {
Expand Down
48 changes: 18 additions & 30 deletions bot/src/player/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,10 @@ use futures::{sync::mpsc, Async, Future, Poll, Stream};
use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
use std::sync::Arc;
use tokio::timer;
use tokio_core::reactor::Core;

#[derive(Debug, serde::Deserialize)]
pub struct Config {
/// Device to use with connect player.
#[serde(default)]
pub device: Option<String>,
}

/// Setup a player.
pub fn setup(
core: &mut Core,
config: &Config,
spotify: Arc<spotify::Spotify>,
) -> Result<(Player, ConnectInterface), failure::Error> {
let devices = core.run(spotify.my_player_devices())?;

for (i, device) in devices.iter().enumerate() {
log::info!("device #{}: {}", i, device.name)
}

let device = match config.device.as_ref() {
Some(device) => devices.into_iter().find(|d| d.name == *device),
None => devices.into_iter().next(),
};

let device = Arc::new(RwLock::new(device));
pub fn setup(spotify: Arc<spotify::Spotify>) -> Result<(Player, ConnectInterface), failure::Error> {
let device = Arc::new(RwLock::new(None));

let (config_tx, config_rx) = mpsc::unbounded();

Expand Down Expand Up @@ -87,6 +64,11 @@ impl PlayerClient<'_> {
*self.timeout = Some(timer::Delay::new(song.deadline()));
}

/// Synchronize the state of the player with the given song.
pub fn play_sync(&mut self, song: &super::Song) {
*self.timeout = Some(timer::Delay::new(song.deadline()));
}

pub fn pause(&mut self, kind: super::EventKind) {
*self.pause = Some((
kind,
Expand Down Expand Up @@ -187,9 +169,14 @@ impl Stream for Player {
.map_err(|_| format_err!("failed to receive configuration event"))?
{
Async::Ready(None) => failure::bail!("configuration received ended"),
Async::Ready(Some(ConfigurationEvent::SetDevice(device))) => {
Async::Ready(Some(ConfigurationEvent::SetDevice(device, notify))) => {
*self.device.write() = Some(device);
return Ok(Async::Ready(Some(super::PlayerEvent::DeviceChanged)));

if notify {
return Ok(Async::Ready(Some(super::PlayerEvent::DeviceChanged)));
}

not_ready = false;
}
Async::NotReady => (),
}
Expand Down Expand Up @@ -226,7 +213,8 @@ fn handle_future(
}

pub enum ConfigurationEvent {
SetDevice(spotify::Device),
/// Set the device, boolean indicates if we should notify the bus of the changes or not.
SetDevice(spotify::Device, bool),
}

#[derive(Clone)]
Expand All @@ -248,10 +236,10 @@ impl ConnectInterface {
}

/// Set which device to perform playback from.
pub fn set_device(&self, device: spotify::Device) {
pub fn set_device(&self, device: spotify::Device, notify: bool) {
if let Err(_) = self
.config_tx
.unbounded_send(ConfigurationEvent::SetDevice(device))
.unbounded_send(ConfigurationEvent::SetDevice(device, notify))
{
log::error!("failed to configure device");
}
Expand Down
Loading

0 comments on commit d0bde83

Please sign in to comment.