diff --git a/Cargo.lock b/Cargo.lock index 7a2e3ca..3dc05e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -555,6 +555,7 @@ dependencies = [ "clap", "obws", "tokio", + "url", ] [[package]] @@ -996,9 +997,9 @@ dependencies = [ [[package]] name = "url" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", diff --git a/Cargo.toml b/Cargo.toml index 71daf57..4b41cf1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,5 +13,6 @@ name = "obs-cmd" tokio = { version = "1.28.1", features = ["rt-multi-thread", "macros"] } obws = "0.11.0" clap = { version = "4.4.8", features = ["derive"] } +url = "2.4.1" diff --git a/README.md b/README.md index 585c70b..558b6f9 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,11 @@ obs-cmd virtualcam start obs-cmd replay toggle obs-cmd replay save obs-cmd info -obs-cmd --obsws obsws://localhost:4455/secret info # You can override the default `obsws` url +obs-cmd --websocket obsws://localhost:4455/secret info # You can override the default `obsws` url ``` +You can override the websocket URL, which can be found in OBS -> Tools -> WebSocket Server Settings. `localhost` for the hostname will work for most, instead of the full IP address. + ### Installation @@ -105,4 +107,4 @@ Donations are welcome and will go towards further development of this project monero:88LyqYXn4LdCVDtPWKuton9hJwbo8ZduNEGuARHGdeSJ79BBYWGpMQR8VGWxGDKtTLLM6E9MJm8RvW9VMUgCcSXu19L9FSv bitcoin:bc1q6mh77hfv8x8pa0clzskw6ndysujmr78j6se025 lightning:techonsapevole@getalby.com -``` \ No newline at end of file +``` diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..3c93597 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,124 @@ +use clap::{Parser, Subcommand}; +use std::str::FromStr; +use url::Url; + +#[derive(Clone, Debug)] +pub struct ObsWebsocket { + pub hostname: String, + pub port: u16, + pub password: Option, +} + +impl FromStr for ObsWebsocket { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match Url::parse(s) { + Ok(unvalidated_websocket) => { + if unvalidated_websocket.scheme() != "obsws" { + return Err( + "Invalid URL format, use the format obsws://hostname:port/password", + ); + } + + let hostname = unvalidated_websocket.host().unwrap().to_string(); + + let port = + match unvalidated_websocket.port() { + Some(port) => port, + None => return Err( + "Please specify a port in the format obsws://hostname:port/password", + ), + }; + + let password = match unvalidated_websocket.path() { + "" => None, + _ => { + let mut pass = unvalidated_websocket.path().to_string(); + // Otherwise the `/` part of the password in the URL is included. + let _ = pass.remove(0); + Some(pass) + } + }; + + Ok(ObsWebsocket { + hostname, + port, + password, + }) + } + Err(_) => Err("Invalid URL format, use the format obsws://hostname:port/password"), + } + } +} + +#[derive(Subcommand, Clone, Debug)] +pub enum Replay { + Start, + Stop, + Toggle, + Save, +} + +#[derive(Subcommand, Clone, Debug)] +pub enum VirtualCamera { + Start, + Stop, + Toggle, +} + +#[derive(Subcommand, Clone, Debug)] +pub enum Streaming { + Start, + Stop, + Toggle, +} + +#[derive(Subcommand, Clone, Debug)] +pub enum Recording { + Start, + Stop, + Toggle, +} + +#[derive(Parser)] +#[clap(author, version, about, long_about = None)] +pub struct Cli { + #[clap(short, long)] + /// The default websocket URL is `obsws://localhost:4455/secret` + /// if this argument is not provided + pub websocket: Option, + #[clap(subcommand)] + pub command: Commands, +} + +#[derive(Subcommand)] +pub enum Commands { + Info, + Scene { + switch_placeholder: String, // NOTE: just for args positioning + scene_name: String, + }, + + #[clap(subcommand)] + Replay(Replay), + + #[clap(subcommand)] + VirtualCamera(VirtualCamera), + + #[clap(subcommand)] + Streaming(Streaming), + + #[clap(subcommand)] + Recording(Recording), + + ToggleMute { + device: String, + }, + + Filter { + command: String, + source: String, + filter: String, + }, +} diff --git a/src/main.rs b/src/main.rs index 3d9675d..f47c13c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,73 +1,21 @@ -use clap::{Parser, Subcommand}; -use obws::{requests::filters::SetEnabled, Client}; - -#[derive(Parser)] -#[clap(author, version, about, long_about = None)] -struct Cli { - #[clap(short, long)] - obsws: Option, - #[clap(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - Info, - Scene { - switch_placeholder: String, // NOTE: just for args positioning - scene_name: String, - }, - Replay { - action: String, - }, - Virtualcam { - action: String, - }, - Streaming { - action: String, - }, - Recording { - action: String, - }, - ToggleMute { - device: String, - }, - Filter { - command: String, - source: String, - filter: String, - }, -} +mod command; +use command::*; -fn parse_obsws(input: &str) -> Result<(&str, &str, u16), &'static str> { - if !input.starts_with("obsws://") { - return Err("Invalid URL format, use the format: obsws://hostname:port/password"); - } - - let without_prefix = &input[8..]; - let parts: Vec<&str> = without_prefix.split([':', '/'].as_ref()).collect(); - - if parts.len() < 3 { - return Err("Invalid format"); - } - - let hostname = parts[0]; - let port = parts[1].parse().map_err(|_| "Invalid port number")?; - let password = parts[2]; - - Ok((hostname, password, port)) -} +use clap::Parser; +use obws::{requests::filters::SetEnabled, Client}; #[tokio::main] async fn main() -> Result<(), Box> { - let cli: Cli = Cli::parse(); + let cli = Cli::parse(); - let obs_ws_url = cli - .obsws - .unwrap_or_else(|| String::from("obsws://localhost:4455/secret")); - - let (hostname, password, port) = parse_obsws(&obs_ws_url)?; - let client = Client::connect(hostname, port, Some(password)).await?; + let client = match cli.websocket { + Some(ObsWebsocket { + hostname, + port, + password, + }) => Client::connect(hostname, port, password).await?, + None => Client::connect("localhost", 4455, Some("secret")).await?, + }; match &cli.command { Commands::Scene { @@ -79,110 +27,113 @@ async fn main() -> Result<(), Box> { println!("Set current scene: {} {}", switch_placeholder, scene_name); println!("Result: {:?}", res); } + Commands::Info => { let version = client.general().version().await?; println!("Version: {:?}", version); } - Commands::Recording { action } => { + + Commands::Recording(action) => { + use Recording::*; println!("Recording {:?}", action); - match action.as_str() { - "start" => { + + match action { + Start => { let res = client.recording().start().await; println!("Recording started"); println!("Result: {:?}", res); } - "stop" => { + Stop => { let res = client.recording().stop().await; println!("Recording stopped"); println!("Result: {:?}", res); } - "toggle" => { + Toggle => { let res = client.recording().toggle().await; println!("Recording toggled"); println!("Result: {:?}", res); } - _ => { - println!("Invalid recording command: {}", action); - } } } - Commands::Streaming { action } => { + + Commands::Streaming(action) => { + use Streaming::*; println!("Streaming {:?}", action); - match action.as_str() { - "start" => { + + match action { + Start => { let res = client.streaming().start().await; println!("Streaming started"); println!("Result: {:?}", res); } - "stop" => { + Stop => { let res = client.streaming().stop().await; println!("Streaming stopped"); println!("Result: {:?}", res); } - "toggle" => { + Toggle => { let res = client.streaming().toggle().await?; println!("Streaming toggled"); println!("Result: {:?}", res); } - _ => { - println!("Invalid streaming command: {}", action); - } } } - Commands::Virtualcam { action } => { - println!("Virtualcam {:?}", action); - match action.as_str() { - "start" => { + + Commands::VirtualCamera(action) => { + use VirtualCamera::*; + println!("VirtualCamera {:?}", action); + + match action { + Start => { let res = client.virtual_cam().start().await; println!("Result: {:?}", res); } - "stop" => { + Stop => { let res = client.virtual_cam().stop().await; println!("Result: {:?}", res); } - "toggle" => { + Toggle => { let res = client.virtual_cam().toggle().await?; println!("Result: {:?}", res); } - _ => { - println!("Invalid virtualcam command: {}", action); - } } } - Commands::Replay { action } => { + + Commands::Replay(action) => { + use Replay::*; println!("Replay {:?}", action); - match action.as_str() { - "start" => { + + match action { + Start => { let res = client.replay_buffer().start().await; println!("Replay Buffer started"); println!("Result: {:?}", res); } - "stop" => { + Stop => { let res = client.replay_buffer().stop().await; println!("Replay Buffer stopped"); println!("Result: {:?}", res); } - "toggle" => { + Toggle => { let res = client.replay_buffer().toggle().await?; println!("Replay Buffer toggled"); println!("Result: {:?}", res); } - "save" => { + Save => { let res = client.replay_buffer().save().await; println!("Buffer saved"); println!("Result: {:?}", res); } - _ => { - println!("Invalid replay command: {}", action); - } } } + Commands::ToggleMute { device } => { - println!("Toggle mute device: {:?} ", device); + println!("Toggling mute on device: {:?} ", device); let res = client.inputs().toggle_mute(device).await; println!("Result: {:?}", res); } + Commands::Filter { command, source,