Refactor, added config file for custom players and cli interface

This commit is contained in:
Ryze 2023-03-14 20:48:43 +03:00
parent 7de78329fb
commit ab586ba197
Signed by: ryze
GPG key ID: 9B296C5CEAEAAAC1
7 changed files with 258 additions and 62 deletions

View file

@ -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"

44
src/browser.rs Normal file
View file

@ -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<FF2MpvMessage, FF2MpvError> {
let message = read_message()?;
let ff2mpv_message = serde_json::from_str(&message)?;
Ok(ff2mpv_message)
}
fn read_message() -> Result<String, io::Error> {
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(())
}

79
src/command.rs Normal file
View file

@ -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 <command>");
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<String>, url: &str) -> Result<(), io::Error> {
process::Command::new(command)
.args(args)
.arg(url)
.spawn()?;
Ok(())
}
}

81
src/config.rs Normal file
View file

@ -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<String>,
}
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<Self, FF2MpvError> {
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<String> {
vec![]
}

31
src/error.rs Normal file
View file

@ -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<io::Error> for FF2MpvError {
fn from(value: io::Error) -> Self {
Self::IOError(value)
}
}
impl From<serde_json::Error> 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}"),
}
}
}

View file

@ -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<MpvMessage, String> {
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<String> {
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;

View file

@ -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,
}
}