From ab586ba197cfcf64f96b11a7da188d1153bd39b8 Mon Sep 17 00:00:00 2001 From: Ryze Date: Tue, 14 Mar 2023 20:48:43 +0300 Subject: [PATCH] Refactor, added config file for custom players and cli interface --- Cargo.toml | 2 +- src/browser.rs | 44 +++++++++++++++++++++++++++ src/command.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++++++ src/config.rs | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 31 +++++++++++++++++++ src/lib.rs | 46 +++------------------------- src/main.rs | 37 +++++++++++------------ 7 files changed, 258 insertions(+), 62 deletions(-) create mode 100644 src/browser.rs create mode 100644 src/command.rs create mode 100644 src/config.rs create mode 100644 src/error.rs diff --git a/Cargo.toml b/Cargo.toml index 4f550f6..a8614cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] serde = { version = "1.0.152", features = ["derive"] } -serde_json = "1.0.93" +serde_json = { version = "1.0.93", features = ["preserve_order"] } [profile.release-full] inherits = "release" diff --git a/src/browser.rs b/src/browser.rs new file mode 100644 index 0000000..fbfc56c --- /dev/null +++ b/src/browser.rs @@ -0,0 +1,44 @@ +use serde::Deserialize; +use std::io; +use std::io::BufReader; +use std::io::{Read, Write}; + +use crate::error::FF2MpvError; + +#[derive(Deserialize)] +pub struct FF2MpvMessage { + pub url: String, +} + +pub fn send_reply() -> Result<(), io::Error> { + send_message("ok") +} + +pub fn get_mpv_message() -> Result { + let message = read_message()?; + let ff2mpv_message = serde_json::from_str(&message)?; + Ok(ff2mpv_message) +} + +fn read_message() -> Result { + let mut stdin = io::stdin(); + let mut buf: [u8; 4] = [0; 4]; + stdin.read_exact(&mut buf)?; + + let length = u32::from_ne_bytes(buf); + let mut reader = BufReader::new(stdin.take(length as u64)); + + let mut string = String::with_capacity(length as usize); + reader.read_to_string(&mut string)?; + Ok(string) +} + +fn send_message(message: &str) -> Result<(), io::Error> { + let length = (message.len() as u32).to_ne_bytes(); + let message = message.as_bytes(); + + let mut stdout = io::stdout(); + stdout.write_all(&length)?; + stdout.write_all(message)?; + Ok(()) +} diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..c255711 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,79 @@ +use std::env; +use std::io; +use std::process; +use serde_json::{self, json}; + +use crate::browser; +use crate::config::Config; +use crate::error::FF2MpvError; + +pub enum Command { + ShowHelp, + ShowManifest, + ValidateConfig, + FF2Mpv +} + + +impl Command { + pub fn execute(&self) -> Result<(), FF2MpvError> { + match self { + Command::ShowHelp => Self::show_help(), + Command::ShowManifest => Self::show_manifest(), + Command::ValidateConfig => Self::validate_config(), + Command::FF2Mpv => Self::ff2mpv() + } + } + + fn show_help() -> Result<(), FF2MpvError> { + println!("Usage: ff2mpv-rust "); + println!("Commands:"); + println!(" help: prints help message"); + println!(" manifest: prints manifest for browser configuration"); + println!(" validate: checks configration file for validity"); + println!("Note: Invalid commands won't fail"); + println!("Note: It will assume that binary is called from browser, blocking for input"); + + Ok(()) + } + + fn show_manifest() -> Result<(), FF2MpvError>{ + let executable_path = env::current_exe()?; + let manifest = json!({ + "name": "ff2mpv", + "description": "ff2mpv's external manifest", + "path": executable_path, + "type": "stdio", + "allowed_extensions": ["ff2mpv@yossarian.net"] + }); + + let manifest = serde_json::to_string_pretty(&manifest)?; + println!("{manifest}"); + + Ok(()) + } + + fn validate_config() -> Result<(), FF2MpvError> { + Config::parse_config_file()?; + println!("Config is valid!"); + + Ok(()) + } + + fn ff2mpv() -> Result<(), FF2MpvError> { + let config = Config::build(); + let ff2mpv_message = browser::get_mpv_message()?; + Command::launch_mpv(config.player_command, config.player_args, &ff2mpv_message.url)?; + + Ok(()) + } + + fn launch_mpv(command: String, args: Vec, url: &str) -> Result<(), io::Error> { + process::Command::new(command) + .args(args) + .arg(url) + .spawn()?; + + Ok(()) + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..35331ba --- /dev/null +++ b/src/config.rs @@ -0,0 +1,81 @@ +use std::env; +use std::fs; +use std::path::PathBuf; +use serde::Deserialize; + +use crate::error::FF2MpvError; + +#[derive(Deserialize)] +pub struct Config { + #[serde(default = "default_player_command")] + pub player_command: String, + + #[serde(default = "default_player_args")] + pub player_args: Vec, +} + +impl Default for Config { + fn default() -> Self { + Self { + player_command: default_player_command(), + player_args: default_player_args(), + } + } +} + +impl Config { + pub fn build() -> Self { + if let Ok(config) = Config::parse_config_file() { + config + } else { + Config::default() + } + } + + pub fn parse_config_file() -> Result { + let config_path = Config::get_config_location(); + if !config_path.exists() { + return Err(FF2MpvError::NoConfig); + } + + let string = fs::read_to_string(config_path)?; + let config = serde_json::from_str(&string)?; + + Ok(config) + } + + #[cfg(target_family = "unix")] + fn get_config_location() -> PathBuf { + let mut path = PathBuf::new(); + + if let Ok(home) = env::var("XDG_CONFIG_HOME") { + path.push(home); + } else if let Ok(home) = env::var("HOME") { + path.push(home); + path.push(".config"); + } else { + path.push("/etc"); + } + + path.push("ff2mpv-rust.json"); + path + } + + #[cfg(target_family = "windows")] + fn get_config_location() -> PathBuf { + let mut path = PathBuf::new(); + let appdata = env::var("APPDATA").unwrap(); + + path.push(appdata); + path.push("ff2mpv-rust.json"); + path + } +} + +fn default_player_command() -> String { + "mpv".to_owned() +} + +fn default_player_args() -> Vec { + vec![] +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..d39c5fa --- /dev/null +++ b/src/error.rs @@ -0,0 +1,31 @@ +use std::io; +use std::fmt; +use std::fmt::Display; + +pub enum FF2MpvError { + NoConfig, + IOError(io::Error), + JSONError(serde_json::Error), +} + +impl From for FF2MpvError { + fn from(value: io::Error) -> Self { + Self::IOError(value) + } +} + +impl From for FF2MpvError { + fn from(value: serde_json::Error) -> Self { + Self::JSONError(value) + } +} + +impl Display for FF2MpvError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NoConfig => write!(f, "Config doesn't exist"), + Self::IOError(e) => write!(f, "IO Error: {e}"), + Self::JSONError(e) => write!(f, "JSON Error: {e}"), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 975c397..d03caf6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,42 +1,4 @@ -use std::io::{self, Read, Write}; -use serde::Deserialize; - -#[derive(Deserialize)] -pub struct MpvMessage { - pub url: String -} - -pub fn get_mpv_message() -> Result { - let message = match get_browser_message() { - Ok(msg) => msg, - Err(e) => return Err(format!("IO Error: {e}")) - }; - - match serde_json::from_str(&message) { - Ok(msg) => Ok(msg), - Err(e) => Err(format!("JSON Error: {e}")) - } -} - -pub fn get_browser_message() -> io::Result { - let mut stdin = io::stdin(); - let mut buf: [u8; 4] = [0; 4]; - stdin.read_exact(&mut buf)?; - - let length = u32::from_ne_bytes(buf); - let mut reader = io::BufReader::new(stdin.take(length as u64)); - - let mut string = String::with_capacity(length as usize); - reader.read_to_string(&mut string)?; - Ok(string) -} - -pub fn send_browser_message(message: &str) -> io::Result<()> { - let length = (message.len() as u32).to_ne_bytes(); - let message = message.as_bytes(); - - let mut stdout = io::stdout(); - stdout.write_all(&length)?; - stdout.write_all(message)?; - Ok(()) -} +pub mod browser; +pub mod command; +pub mod config; +pub mod error; diff --git a/src/main.rs b/src/main.rs index f504690..1fbc2a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,26 +1,25 @@ -use std::process::{Command, self}; -use ff2mpv_rust::{get_mpv_message, send_browser_message}; +use std::env; +use std::process; + +use ff2mpv_rust::command::Command; fn main() { - let message = match get_mpv_message() { - Ok(msg) => msg, - Err(e) => { - eprintln!("{e}"); - process::exit(-1) - } - }; + let mut args = env::args(); + args.next(); // Skip binary path - let mpv = Command::new("mpv") - .arg(message.url) - .spawn(); - - if let Err(e) = mpv { - eprintln!("{e}"); - process::exit(-1); - } - - if let Err(e) = send_browser_message("ok") { + let command_name = args.next().unwrap_or_default(); + let command = get_command(&command_name); + if let Err(e) = command.execute() { eprintln!("{e}"); process::exit(-1); } } + +fn get_command(name: &str) -> Command { + match name { + "help" => Command::ShowHelp, + "manifest" => Command::ShowManifest, + "validate" => Command::ValidateConfig, + _ => Command::FF2Mpv, + } +}