obs-do 0.1.0

WebSocket control utility for OBS Studio
use anyhow::Context;
use clap::{Parser, Subcommand};
use directories::ProjectDirs;
use obws::Client;

#[derive(Debug, Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
    #[command(subcommand)]
    cmd: Command,
}

#[derive(Debug, Subcommand)]
enum Command {
    ToggleStream,
    ToggleRecord,
    ToggleMute,
    SetScene { scene: String },
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let args = Args::parse();

    let Some(proj_dirs) = ProjectDirs::from("", "", "obs-do") else {
        anyhow::bail!("could not determine configuration file location");
    };
    let cfg = proj_dirs.config_dir().join("websocket-token");

    let exists = tokio::fs::try_exists(&cfg).await;
    if !exists.unwrap_or(false) {
        anyhow::bail!(
            "\
No OBS WebSocket authentication token found.
Open OBS, go to Tools -> WebSocket Server Settings, \
hit 'Show Connect Info', and place that token in the file {}\
            ",
            cfg.display()
        );
    }
    let pw = tokio::fs::read_to_string(&cfg)
        .await
        .context("read OBS WebSocket autheticator from obs-do config file")?;

    let client = if let Ok(client) = Client::connect("localhost", 4455, Some(pw.trim())).await {
        let version = client
            .general()
            .version()
            .await
            .context("get OBS version")?;
        eprintln!(
            "Connected to OBS: {} / {}",
            version.obs_version, version.obs_web_socket_version
        );
        client
    } else {
        anyhow::bail!(
            "\
Could not connect to OBS over WebSocket.
Make sure OBS is running, and that 'Enable WebSocket server' is checked \
under Tools -> WebSocket Server Settings.

If that menu item does not appear for you, your OBS has not been built \
with WebSocket support. On Arch Linux for example, you'll want one of \
the AUR obs-studio packages that build WebSocket, such as obs-studio-git.\
"
        );
    };

    match args.cmd {
        Command::ToggleStream => {
            client
                .streaming()
                .toggle()
                .await
                .context("toggle streaming")?;
        }
        Command::ToggleRecord => {
            client
                .recording()
                .toggle()
                .await
                .context("toggle recording")?;
        }
        Command::ToggleMute => {
            client
                .inputs()
                .toggle_mute("Mic/Aux")
                .await
                .context("toggle-mute Mic/Aux")?;
        }
        Command::SetScene { scene } => {
            client
                .scenes()
                .set_current_program_scene(&scene)
                .await
                .with_context(|| format!("set-scene {scene}"))?;
        }
    }

    Ok(())
}