ha-now-playing/src/main.rs

207 lines
4.7 KiB
Rust
Raw Normal View History

2021-11-25 23:40:07 +01:00
use std::{io::Read, path::PathBuf};
2021-11-23 14:22:58 +01:00
use clap::Parser;
2022-01-14 10:06:26 +01:00
use color_eyre::Report;
2021-07-31 17:16:00 +02:00
use serde_json::json;
2022-01-14 10:06:26 +01:00
use tracing::{debug, error};
use tracing_subscriber::EnvFilter;
2021-07-31 17:16:00 +02:00
/// Bert
2021-11-23 14:22:58 +01:00
#[derive(Parser, Debug)]
2021-07-31 17:16:00 +02:00
#[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)]
2021-11-25 23:40:07 +01:00
token: Option<String>,
/// File with the API token
#[clap(long)]
token_file: Option<PathBuf>,
2021-07-31 17:16:00 +02:00
/// Use HTTP instead of HTTPS
#[clap(short, long)]
insecure: bool,
2022-01-14 10:06:26 +01:00
#[clap(short, long)]
debug: bool,
2021-07-31 17:16:00 +02:00
#[clap(subcommand)]
cmd: Option<Command>,
}
2021-11-23 14:22:58 +01:00
#[derive(Parser, Debug, Clone, Copy)]
2021-07-31 17:16:00 +02:00
enum Command {
/// Toggle playback
PlayPause,
/// Raise volume
VolumeUp,
/// Lower volume
VolumeDown,
}
2022-01-14 10:06:26 +01:00
fn main() {
if let Err(e) = do_main() {
error!("Fatal error: {}", e);
}
}
2021-07-31 17:16:00 +02:00
2022-01-14 10:06:26 +01:00
fn do_main() -> Result<(), Box<dyn std::error::Error>> {
2021-07-31 17:16:00 +02:00
let opts = Opts::parse();
2021-11-25 23:40:07 +01:00
let Opts {
host,
entity,
insecure,
2022-01-14 10:06:26 +01:00
debug,
2021-11-25 23:40:07 +01:00
cmd,
token,
token_file,
} = opts;
2022-01-14 10:06:26 +01:00
setup(debug)?;
2021-11-25 23:40:07 +01:00
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
}
2021-07-31 17:16:00 +02:00
} else {
2021-11-25 23:40:07 +01:00
eprintln!("No API token given. Use either --token or --token-file");
None
};
2021-07-31 17:16:00 +02:00
2021-11-25 23:40:07 +01:00
if let Some(token) = token {
if let Some(cmd) = cmd {
call_service(cmd, host, entity, insecure, token)?;
2021-07-31 17:16:00 +02:00
} else {
2021-11-25 23:40:07 +01:00
get_now_playing(host, entity, insecure, token)?;
2021-07-31 17:16:00 +02:00
}
}
// println!("{:#?}", response);
Ok(())
}
2022-01-14 10:06:26 +01:00
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(())
}
2021-11-25 23:40:07 +01:00
fn get_now_playing(
host: String,
entity: String,
insecure: bool,
token: String,
) -> Result<(), Box<dyn std::error::Error>> {
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::<serde_json::Value>()?;
2022-01-14 10:06:26 +01:00
debug!("Response: {:#?}", response);
2021-11-25 23:40:07 +01:00
if response["state"] == "playing" {
let attributes = &response["attributes"];
let maybe_channel = attributes["media_channel"].as_str();
2022-01-14 10:06:26 +01:00
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()
};
2021-11-25 23:40:07 +01:00
let now_playing = if let Some(channel) = maybe_channel {
2022-01-14 10:06:26 +01:00
format!("{volume}[{channel}] {artist} - {title}")
2021-11-25 23:40:07 +01:00
} else {
2022-01-14 10:06:26 +01:00
format!("{volume}{artist} - {title}")
2021-11-25 23:40:07 +01:00
};
println!("{}", now_playing);
} else {
println!(
"Sonos {}",
response["state"]
.as_str()
.map_or("state unknown", |state| state)
);
}
Ok(())
}
2021-07-31 17:16:00 +02:00
2021-11-25 23:40:07 +01:00
fn call_service(
command: Command,
host: String,
entity: String,
insecure: bool,
token: String,
) -> Result<(), Box<dyn std::error::Error>> {
2021-07-31 17:16:00 +02:00
let cmd = match command {
Command::PlayPause => "media_play_pause",
Command::VolumeUp => "volume_up",
Command::VolumeDown => "volume_down",
};
let url = format!(
"{}://{}/api/services/media_player/{}",
2021-11-25 23:40:07 +01:00
if insecure { "http" } else { "https" },
host,
2021-07-31 17:16:00 +02:00
cmd
);
2021-11-25 23:40:07 +01:00
let body = json!({ "entity_id": entity }).to_string();
2021-07-31 17:16:00 +02:00
let client = reqwest::blocking::Client::new();
let response = client
.post(url)
.body(body)
2021-11-25 23:40:07 +01:00
.bearer_auth(token)
2021-07-31 17:16:00 +02:00
.header("Accept", "application/json")
.header("Content-Type", "application/json")
.send()?
.json::<serde_json::Value>()?;
println!("{:#?}", response);
Ok(())
}