use std::{io::Read, path::PathBuf}; use clap::Parser; use color_eyre::Report; use serde_json::json; use tracing::{debug, error}; use tracing_subscriber::EnvFilter; /// Bert #[derive(Parser, Debug)] #[clap(version, author, about)] struct Opts { /// Home Assistant host #[clap(short, long)] host: String, /// Media player entity ID #[clap(short, long)] entity: String, /// API token #[clap(short, long)] token: Option, /// File with the API token #[clap(long)] token_file: Option, /// Use HTTP instead of HTTPS #[clap(short, long)] insecure: bool, #[clap(short, long)] debug: bool, #[clap(subcommand)] cmd: Option, } #[derive(Parser, Debug, Clone, Copy)] enum Command { /// Toggle playback PlayPause, /// Raise volume VolumeUp, /// Lower volume VolumeDown, } fn main() { if let Err(e) = do_main() { error!("Fatal error: {}", e); } } fn do_main() -> Result<(), Box> { let opts = Opts::parse(); let Opts { host, entity, insecure, debug, cmd, token, token_file, } = opts; setup(debug)?; let token = if let Some(token) = token { Some(token) } else if let Some(token_file) = token_file { if let Ok(mut file) = std::fs::File::open(token_file) { let mut buf = String::new(); file.read_to_string(&mut buf).ok(); Some(buf) } else { None } } else { eprintln!("No API token given. Use either --token or --token-file"); None }; if let Some(token) = token { if let Some(cmd) = cmd { call_service(cmd, host, entity, insecure, token)?; } else { get_now_playing(host, entity, insecure, token)?; } } // println!("{:#?}", response); Ok(()) } fn setup(debug: bool) -> Result<(), Report> { if debug { std::env::set_var("RUST_LOG", "debug"); } color_eyre::install()?; tracing_subscriber::fmt::fmt() .with_env_filter(EnvFilter::from_default_env()) .with_writer(std::io::stderr) .init(); Ok(()) } fn get_now_playing( host: String, entity: String, insecure: bool, token: String, ) -> Result<(), Box> { let url = format!( "{}://{}/api/states/{}", if insecure { "http" } else { "https" }, host, entity ); let client = reqwest::blocking::Client::new(); let response = client .get(url) .bearer_auth(token) .header("Accept", "application/json") .header("Content-Type", "application/json") .send()? .json::()?; debug!("Response: {:#?}", response); if response["state"] == "playing" { let attributes = &response["attributes"]; let maybe_channel = attributes["media_channel"].as_str(); let artist = attributes["media_artist"] .as_str() .map_or("No artist", |artist| artist); let title = attributes["media_title"] .as_str() .map_or("No title", |title| title); let volume_raw = attributes["volume_level"] .as_f64() .map_or(-1., |volume| volume); let volume = if volume_raw >= 0. { format!("[{:3.0}%] ", volume_raw * 100.) } else { String::new() }; let now_playing = if let Some(channel) = maybe_channel { format!("{volume}[{channel}] {artist} - {title}") } else { format!("{volume}{artist} - {title}") }; println!("{}", now_playing); } else { println!( "Sonos {}", response["state"] .as_str() .map_or("state unknown", |state| state) ); } Ok(()) } fn call_service( command: Command, host: String, entity: String, insecure: bool, token: String, ) -> Result<(), Box> { let cmd = match command { Command::PlayPause => "media_play_pause", Command::VolumeUp => "volume_up", Command::VolumeDown => "volume_down", }; let url = format!( "{}://{}/api/services/media_player/{}", if insecure { "http" } else { "https" }, host, cmd ); let body = json!({ "entity_id": entity }).to_string(); let client = reqwest::blocking::Client::new(); let response = client .post(url) .body(body) .bearer_auth(token) .header("Accept", "application/json") .header("Content-Type", "application/json") .send()? .json::()?; println!("{:#?}", response); Ok(()) }