#[macro_use] extern crate clap; #[macro_use] extern crate log; #[macro_use] extern crate serde_derive; #[macro_use] extern crate failure; #[macro_use] mod util; mod discovery; use std::time::Duration; use std::net::IpAddr; use sonos::Speaker; use failure::Fallible; use crate::discovery::find_speaker_by_name; fn argparse<'a, 'b>() -> clap::App<'a, 'b> { use clap::{App, AppSettings, Arg, SubCommand}; App::new("sonos") .version(crate_version!()) .author("Jordan Doyle ") .about("Control your Sonos using the command line") .setting(AppSettings::SubcommandRequired) .arg(Arg::with_name("controller") .help("Set the controller to run operation on") .short("c") .required_unless("rooms") .value_name("IP or Room Name") .takes_value(true)) .arg(Arg::with_name("json") .help("Return back JSON serialised responses for programmatic use of the CLI")) .subcommand(SubCommand::with_name("info").about("Shows information about the speaker")) .subcommand( SubCommand::with_name("track") .about("Commands to manipulate the tracklist") .subcommand(SubCommand::with_name("next").about("Skip to the next track")) .subcommand(SubCommand::with_name("prev").about("Go back to the last track")) .subcommand(SubCommand::with_name("list").about("Get the list of tracks in the queue")) .subcommand( SubCommand::with_name("play") .about("Play a given track") .subcommand(SubCommand::with_name("tv").about("Set the current speaker's input to the SPDIF")) .subcommand(SubCommand::with_name("line-in").about("Set the current speaker's input to the line-in")) .arg(Arg::with_name("uri").help("Queue position to skip to or a Sonos URI to play").index(1).conflicts_with_all(&["tv", "line-in"])) ) ) .subcommand(SubCommand::with_name("group").about("Group this speaker with the given master") .arg(Arg::with_name("MASTER") .help("Name of the speaker to group with") .required(true) .index(1))) .subcommand(SubCommand::with_name("ungroup").about("Ungroup this speaker from the master")) .subcommand(SubCommand::with_name("seek").about("Seek to a specific timestamp on the current track") .arg(Arg::with_name("TIMESTAMP") .help("hh:mm:ss/mm:ss") .required(true) .index(1))) .subcommand(SubCommand::with_name("volume").about("Get or set the volume of the speaker") .arg(Arg::with_name("VOLUME") .help("Percent volume to set speaker to 0-100") .index(1))) .subcommand(SubCommand::with_name("rooms").about("List all of your speakers") .arg(Arg::with_name("invalidate").help("Detect new speakers and room arrangements"))) } #[tokio::main] async fn main() -> Fallible<()> { let args = argparse().get_matches(); util::setup_logger()?; let controller = args.value_of("controller").expect("controller"); let speaker = match controller.parse::() { Ok(ip) => Speaker::from_ip(ip).await?, Err(_) => discovery::find_speaker_by_name(controller).await?, }; match args.subcommand() { ("track", Some(subargs)) => { match subargs.subcommand() { ("next", _) => speaker.queue().next().await?, ("prev", _) => speaker.queue().previous().await?, ("list", _) => print_struct!(args, &TrackList::new(&speaker).await?), ("play", Some(play_subargs)) => match play_subargs.subcommand_name() { Some("tv") => speaker.play_tv().await?, Some("line-in") => speaker.play_line_in().await?, _ => { let uri = play_subargs.value_of("uri") .filter(|s| !s.is_empty()) .ok_or_else(|| format_err!("Must pass [tv], [line-in] or a URI to the play command"))?; if let Ok(pos) = uri.parse::() { speaker.queue().skip_to(&pos).await? } else { speaker.play_track(uri).await? } }, }, _ => print_struct!(args, &Track::new(&speaker).await?) } }, ("group", Some(sub)) => { let master = sub.value_of("MASTER").expect("master"); speaker.group(&match master.parse::() { Ok(ip) => Speaker::from_ip(ip).await?, Err(_) => discovery::find_speaker_by_name(master).await?, }).await? }, ("ungroup", _) => speaker.ungroup().await?, ("info", _) => print_struct!(args, &Info::new(&speaker)), ("volume", Some(sub)) => match sub.value_of("VOLUME") { Some(volume) => speaker.set_volume(volume.parse()?).await?, None => print_struct!(args, &Volume::new(&speaker).await?), }, ("seek", Some(sub)) => { let a = sub.value_of("TIMESTAMP").expect("timestamp"); let mut multiplier = 1; let secs = a.split(":").collect::>().iter().rfold(0, |curr, iter_val| { let section_value = iter_val.parse::().expect("can't parse int") * multiplier; multiplier *= 60; curr + section_value }); let duration = Duration::new(secs, 0); speaker.seek(&duration).await?; }, ("rooms", Some(sub)) => { let devices = discovery::discover(true, sub.is_present("invalidate")).await?; let mut rooms = std::collections::HashMap::new(); for device in devices { let coordinator = device.coordinator().await?; let mut room = rooms.entry(coordinator).or_insert(Vec::new()); room.push(device); } for (key, value) in rooms { info!("Controller: {}", key); for device in value { info!("d: {}", device.name); } } }, _ => { panic!(); } } Ok(()) } #[derive(Serialize, Deserialize, Debug)] struct TrackListItem { pub position: u64, pub title: String, pub artist: String, pub album: String, pub duration: Duration } #[derive(Serialize, Deserialize, Debug)] struct TrackList(Vec); impl TrackList { pub async fn new(speaker: &Speaker) -> Fallible { Ok(Self( speaker.queue().list().await? .into_iter() .map(|v| TrackListItem { position: v.position, title: v.title, artist: v.artist, album: v.album, duration: v.duration }) .collect() )) } } impl std::fmt::Display for TrackList { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { Ok(for item in &self.0 { writeln!(f, "{}: {} - {} ({})", item.position, item.artist, item.title, util::duration_to_hms(item.duration))? }) } } #[derive(Serialize, Deserialize, Debug)] struct Track { pub title: String, pub artist: String, pub album: Option, pub running_time: Duration, pub duration: Duration } impl Track { pub async fn new(speaker: &Speaker) -> Fallible { let track = speaker.track().await?; Ok(Self { title: track.title, artist: track.artist, album: track.album, running_time: track.running_time, duration: track.duration }) } } impl std::fmt::Display for Track { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { writeln!(f, "\u{1F3A4} {}", self.artist)?; writeln!(f, "\u{1F3B5} {}", self.title)?; if let Some(album) = &self.album { writeln!(f, "\u{1F4BF} {}", album)?; } let running_time = util::duration_to_hms(self.running_time); let duration = util::duration_to_hms(self.duration); write!(f, "\u{23F1}\u{FE0F} {}/{}", running_time, duration)?; const PROG_BAR_LEN: usize = 25; let percent_played = ((self.running_time.as_secs() as f64 / self.duration.as_secs() as f64) * PROG_BAR_LEN as f64) as usize; write!(f, " [{}{}]", "\u{2587}".repeat(percent_played), "-".repeat(PROG_BAR_LEN - percent_played)) } } #[derive(Serialize, Deserialize, Debug)] struct Volume { volume: u8, muted: bool, } impl Volume { pub async fn new(speaker: &Speaker) -> Result { Ok(Self { volume: speaker.volume().await?, muted: speaker.muted().await?, }) } } impl std::fmt::Display for Volume { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { const MAX_VOLUME: usize = 100; const PROG_BAR_LEN: usize = 25; let pictogram = if self.muted { "\u{1F507}" } else { "\u{1F50A}" }; write!(f, "{} {}/{}", pictogram, self.volume, MAX_VOLUME)?; let percent = (self.volume as usize * PROG_BAR_LEN) / MAX_VOLUME; write!(f, " [{}{}]", "\u{2587}".repeat(percent), "-".repeat(PROG_BAR_LEN - percent)) } } #[derive(Serialize, Deserialize, Debug)] struct Info { pub ip: IpAddr, pub model: String, pub model_number: String, pub software_version: String, pub hardware_version: String, pub serial_number: String, pub name: String, pub uuid: String, } impl Info { pub fn new(speaker: &Speaker) -> Info { Info { ip: speaker.ip.clone(), model: speaker.model.clone(), model_number: speaker.model_number.clone(), software_version: speaker.software_version.clone(), hardware_version: speaker.hardware_version.clone(), serial_number: speaker.serial_number.clone(), name: speaker.name.clone(), uuid: speaker.uuid.clone(), } } } impl std::fmt::Display for Info { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { writeln!(f, "\u{1F508} {}", self.name)?; writeln!(f, "{}", "=".repeat(self.name.len() + 3))?; writeln!(f, "Model: {} ({})", self.model, self.model_number)?; writeln!(f, "Versions: Software {}, Hardware {}", self.software_version, self.hardware_version)?; writeln!(f, "Serial number: {}", self.serial_number)?; writeln!(f, "UUID: {}", self.uuid) } }