.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # ff2mpv-rust
4 |
5 |
6 |
7 | Native messaging host for ff2mpv written in Rust.
8 |
9 | This is a native compiled binary, so it runs much faster and doesn't require external dependencies by itself, unlike Python and Ruby scripts.
10 |
11 | # What is this?
12 | This is a script for hooking mpv and [ff2mpv](https://github.com/woodruffw/ff2mpv), that allows opening any video link in mpv straight from the browser.
13 |
14 | # Installation
15 | First, install ff2mpv extension from [AMO](https://addons.mozilla.org/en-US/firefox/addon/ff2mpv) or [Chrome Store](https://chrome.google.com/webstore/detail/ff2mpv/ephjcajbkgplkjmelpglennepbpmdpjg).
16 |
17 | After that get native messasing host manifest:
18 | ```
19 | ff2mpv-rust manifest
20 | ```
21 | Or for Chromium/Chrome:
22 | ```
23 | ff2mpv-rust manifest_chromium
24 | ```
25 |
26 | Install it following manual installation instructions on [ff2mpv wiki](https://github.com/woodruffw/ff2mpv/wiki).
27 |
28 | # Configuration
29 | On Linux configuration file is searched in such order:
30 |
31 | 1. $XDG_CONFIG_HOME/ff2mpv-rust.json
32 | 2. $HOME/.config/ff2mpv-rust.json
33 | 3. /etc/ff2mpv-rust.json
34 |
35 | On Windows configuration file should be placed at: %APPDATA%/ff2mpv-rust.json.
36 |
37 | See [default configuration](ff2mpv-rust.json).
38 | Note that default configuration passes `--` as last argument in order to prevent argument injection.
39 | When editing you should either keep it or use a similar argument for the player you want to use.
40 |
41 | # Command line interface
42 | ff2mpv-rust provides command line interface with following commands:
43 | ```
44 | help: prints help message
45 | manifest: prints manifest for Firefox configuration
46 | manifest_chromium: prints manifest for Chromium/Chrome configuration
47 | validate: checks configration file for validity
48 | ```
49 | Note that it won't fail on invalid commands, but instead assume it is called from browser, blocking the input.
50 |
51 | # Contributing
52 |
53 | All issues and pull requests are welcome! Feel free to open an issue if you've got an idea or a problem. You can open a pull request if you are able to implement it yourself.
54 |
55 | ---
56 |
57 |
58 | Made with ponies and love!
59 |
60 | GNU GPL © Ryze 2023
61 |
62 |
63 |
--------------------------------------------------------------------------------
/ff2mpv-rust.json:
--------------------------------------------------------------------------------
1 | {
2 | "player_command": "mpv",
3 | "player_args": ["--no-terminal", "--"]
4 | }
5 |
--------------------------------------------------------------------------------
/src/browser.rs:
--------------------------------------------------------------------------------
1 | use std::io::{self, Read, Write};
2 |
3 | use serde::Deserialize;
4 |
5 | use crate::error::FF2MpvError;
6 |
7 | #[derive(Deserialize)]
8 | pub struct FF2MpvMessage {
9 | pub url: String,
10 | pub options: Vec,
11 | }
12 |
13 | pub fn send_reply() -> Result<(), io::Error> {
14 | // "ok" formatted as a JSON string
15 | send_message("\"ok\"")
16 | }
17 |
18 | pub fn get_mpv_message() -> Result {
19 | let message = read_message()?;
20 | let ff2mpv_message = serde_json::from_str(&message)?;
21 | Ok(ff2mpv_message)
22 | }
23 |
24 | fn read_message() -> Result {
25 | let mut stdin = io::stdin().lock();
26 |
27 | let mut len = 0_u32.to_ne_bytes();
28 | stdin.read_exact(&mut len)?;
29 | let len = u32::from_ne_bytes(len);
30 |
31 | let mut reader = stdin.take(len.into());
32 | let mut msg = String::with_capacity(len as usize);
33 | reader.read_to_string(&mut msg)?;
34 | Ok(msg)
35 | }
36 |
37 | #[allow(clippy::cast_possible_truncation, reason = "Truncation is safe, at most it will only truncate the message")]
38 | fn send_message(message: &str) -> Result<(), io::Error> {
39 | let length = (message.len() as u32).to_ne_bytes();
40 | let message = message.as_bytes();
41 |
42 | let mut stdout = io::stdout().lock();
43 | stdout.write_all(&length)?;
44 | stdout.write_all(message)?;
45 | Ok(())
46 | }
47 |
--------------------------------------------------------------------------------
/src/command.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use std::io;
3 | use std::process;
4 |
5 | use serde_json::json;
6 |
7 | use crate::browser;
8 | use crate::config::Config;
9 | use crate::error::FF2MpvError;
10 |
11 | pub enum Command {
12 | ShowHelp,
13 | ShowManifest,
14 | ShowManifestChromium,
15 | ValidateConfig,
16 | FF2Mpv,
17 | }
18 |
19 | #[allow(clippy::unnecessary_wraps, reason = "More readable in show_help")]
20 | impl Command {
21 | pub fn execute(&self) -> Result<(), FF2MpvError> {
22 | match self {
23 | Command::ShowHelp => Self::show_help(),
24 | Command::ShowManifest => Self::show_manifest(false),
25 | Command::ShowManifestChromium => Self::show_manifest(true),
26 | Command::ValidateConfig => Self::validate_config(),
27 | Command::FF2Mpv => Self::ff2mpv(),
28 | }
29 | }
30 |
31 | fn show_help() -> Result<(), FF2MpvError> {
32 | println!("Usage: ff2mpv-rust ");
33 | println!("Commands:");
34 | println!(" help: prints help message");
35 | println!(" manifest: prints manifest for Firefox configuration");
36 | println!(" manifest_chromium: prints manifest for Chromium/Chrome configuration");
37 | println!(" validate: checks configration file for validity");
38 | println!("Note: Invalid commands won't fail");
39 | println!("Note: It will assume that binary is called from browser, blocking for input");
40 |
41 | Ok(())
42 | }
43 |
44 | fn show_manifest(chromium: bool) -> Result<(), FF2MpvError> {
45 | let executable_path = env::current_exe()?;
46 | let allowed_keyvalue = if chromium {
47 | (
48 | "allowed_origins",
49 | "chrome-extension://ephjcajbkgplkjmelpglennepbpmdpjg/",
50 | )
51 | } else {
52 | ("allowed_extensions", "ff2mpv@yossarian.net")
53 | };
54 |
55 | let manifest = json!({
56 | "name": "ff2mpv",
57 | "description": "ff2mpv's external manifest",
58 | "path": executable_path,
59 | "type": "stdio",
60 | allowed_keyvalue.0: [allowed_keyvalue.1]
61 | });
62 |
63 | let manifest = serde_json::to_string_pretty(&manifest)?;
64 | println!("{manifest}");
65 |
66 | Ok(())
67 | }
68 |
69 | fn validate_config() -> Result<(), FF2MpvError> {
70 | Config::parse_config_file()?;
71 | println!("Config is valid!");
72 |
73 | Ok(())
74 | }
75 |
76 | fn ff2mpv() -> Result<(), FF2MpvError> {
77 | let config = Config::build();
78 | let ff2mpv_message = browser::get_mpv_message()?;
79 | let args = [config.player_args, ff2mpv_message.options].concat();
80 | Command::launch_mpv(config.player_command, args, &ff2mpv_message.url)?;
81 | browser::send_reply()?;
82 |
83 | Ok(())
84 | }
85 |
86 | fn launch_mpv(command: String, args: Vec, url: &str) -> Result<(), io::Error> {
87 | let mut command = process::Command::new(command);
88 |
89 | command.stdout(process::Stdio::null());
90 | command.stderr(process::Stdio::null());
91 | command.args(args);
92 | command.arg(url);
93 |
94 | Command::detach_mpv(&mut command);
95 |
96 | command.spawn()?;
97 |
98 | Ok(())
99 | }
100 |
101 | // NOTE: Make sure the subprocess is not killed.
102 | // See https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging#closing_the_native_app
103 |
104 | #[cfg(unix)]
105 | fn detach_mpv(command: &mut process::Command) {
106 | use std::os::unix::process::CommandExt;
107 | command.process_group(0);
108 | }
109 |
110 | #[cfg(windows)]
111 | fn detach_mpv(command: &mut process::Command) {
112 | use std::os::windows::process::CommandExt;
113 | use windows::Win32::System::Threading::CREATE_BREAKAWAY_FROM_JOB;
114 | command.creation_flags(CREATE_BREAKAWAY_FROM_JOB.0);
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/config.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use std::fs;
3 | use std::io::ErrorKind;
4 | use std::path::PathBuf;
5 |
6 | use serde::Deserialize;
7 |
8 | use crate::error::FF2MpvError;
9 |
10 | #[derive(Deserialize)]
11 | #[serde(default)]
12 | pub struct Config {
13 | pub player_command: String,
14 | pub player_args: Vec,
15 | }
16 |
17 | impl Default for Config {
18 | fn default() -> Self {
19 | Self {
20 | player_command: "mpv".to_owned(),
21 | player_args: vec![String::from("--no-terminal"), String::from("--")],
22 | }
23 | }
24 | }
25 |
26 | impl Config {
27 | const CONFIG_FILENAME: &str = "ff2mpv-rust.json";
28 |
29 | #[must_use]
30 | pub fn build() -> Self {
31 | match Config::parse_config_file() {
32 | Ok(config) => config,
33 |
34 | Err(FF2MpvError::NoConfig) => {
35 | eprintln!("Config not found, using defaults");
36 | Config::default()
37 | }
38 |
39 | Err(e) => {
40 | eprintln!("Error occured while parsing config, using defaults");
41 | eprintln!("{e}");
42 |
43 | Config::default()
44 | }
45 | }
46 | }
47 |
48 | pub fn parse_config_file() -> Result {
49 | let config_path = Config::get_config_location();
50 | let string = match fs::read_to_string(config_path) {
51 | Ok(string) => string,
52 |
53 | Err(e) if e.kind() == ErrorKind::NotFound => {
54 | return Err(FF2MpvError::NoConfig);
55 | }
56 |
57 | Err(e) => {
58 | return Err(FF2MpvError::IOError(e));
59 | }
60 | };
61 |
62 | let config = serde_json::from_str(&string)?;
63 |
64 | Ok(config)
65 | }
66 |
67 | #[cfg(unix)]
68 | fn get_config_location() -> PathBuf {
69 | let mut path = PathBuf::new();
70 |
71 | if let Ok(home) = env::var("XDG_CONFIG_HOME") {
72 | path.push(home);
73 | } else if let Ok(home) = env::var("HOME") {
74 | path.push(home);
75 | path.push(".config");
76 | } else {
77 | path.push("/etc");
78 | }
79 |
80 | path.push(Self::CONFIG_FILENAME);
81 | path
82 | }
83 |
84 | #[cfg(windows)]
85 | fn get_config_location() -> PathBuf {
86 | let mut path = PathBuf::new();
87 | let appdata = env::var("APPDATA").unwrap();
88 |
89 | path.push(appdata);
90 | path.push(Self::CONFIG_FILENAME);
91 | path
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | use std::fmt;
2 | use std::fmt::Display;
3 | use std::io;
4 |
5 | pub enum FF2MpvError {
6 | NoConfig,
7 | IOError(io::Error),
8 | JSONError(serde_json::Error),
9 | }
10 |
11 | impl From for FF2MpvError {
12 | fn from(value: io::Error) -> Self {
13 | Self::IOError(value)
14 | }
15 | }
16 |
17 | impl From for FF2MpvError {
18 | fn from(value: serde_json::Error) -> Self {
19 | Self::JSONError(value)
20 | }
21 | }
22 |
23 | impl Display for FF2MpvError {
24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 | match self {
26 | Self::NoConfig => write!(f, "Config doesn't exist"),
27 | Self::IOError(e) => write!(f, "IO Error: {e}"),
28 | Self::JSONError(e) => write!(f, "JSON Error: {e}"),
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod browser;
2 | pub mod command;
3 | pub mod config;
4 | pub mod error;
5 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use std::process;
3 |
4 | use ff2mpv_rust::command::Command;
5 |
6 | fn main() {
7 | let mut args = env::args();
8 | args.next(); // Skip binary path
9 |
10 | let command_name = args.next().unwrap_or_default();
11 | let command = get_command(&command_name);
12 | if let Err(e) = command.execute() {
13 | eprintln!("{e}");
14 | process::exit(-1);
15 | }
16 | }
17 |
18 | fn get_command(name: &str) -> Command {
19 | match name {
20 | "help" => Command::ShowHelp,
21 | "manifest" => Command::ShowManifest,
22 | "manifest_chromium" => Command::ShowManifestChromium,
23 | "validate" => Command::ValidateConfig,
24 | _ => Command::FF2Mpv,
25 | }
26 | }
27 |
--------------------------------------------------------------------------------