feat: add ffmpeg plugin and update plugin interface to support temporary directories and context-dependent metrics
Release / check-release (push) Successful in 15s
Release / build_gnu (push) Skipped
Release / build_musl (push) Skipped
Release / build_windows (push) Skipped
Release / package_gnu (deb) (push) Skipped
Release / package_gnu (rpm) (push) Skipped
Release / package_musl (deb) (push) Skipped
Release / package_musl (rpm) (push) Skipped
Release / publish-release (push) Skipped
CI / Test (push) Successful in 1m31s
CI / Build Linux (push) Successful in 1m47s

This commit is contained in:
Elias Wendland committed 2026-07-15 22:31:00 +02:00
1 parent e46696c521
commit b97a108d1e
10 files changed
+654 -83

No files matched your search

+16 -4
View File
@@ -1,3 +1,17 @@
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 exclusively.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::env;
use std::fs;
use std::path::Path;
@@ -41,10 +55,8 @@ fn main() {
"pub mod {} {{\n include!({:?});\n}}\n",
stem, abs_path_str
));
registry_add_code.push_str(&format!(
"registry.push(Box::new({}::PluginImpl));\n",
stem
));
registry_add_code
.push_str(&format!("registry.push(Box::new({}::PluginImpl));\n", stem));
}
}
}
+146
View File
@@ -0,0 +1,146 @@
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 exclusively.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::plugin::Plugin;
use rust_ffmpeg::FFmpegBuilder;
use std::io::Write;
use std::path::Path;
pub struct PluginImpl;
impl Plugin for PluginImpl {
fn name(&self) -> &'static str {
"ffmpeg_video"
}
fn from_formats(&self) -> Vec<&'static str> {
vec!["mp4", "webm", "mkv", "avi", "mov", "wmv", "flv", "gif"]
}
fn to_formats(&self) -> Vec<&'static str> {
vec!["mp4", "webm", "mkv", "avi", "mov", "wmv", "flv", "gif"]
}
fn familiarity(&self, _from: &str, to: &str) -> u8 {
match to {
"mp4" | "gif" => 255,
"mov" => 240,
"webm" => 220,
"mkv" => 180,
"wmv" => 160,
"flv" => 120,
"avi" => 100,
_ => 128,
}
}
fn quality(&self, _from: &str, to: &str) -> u8 {
match to {
"mkv" => 250,
"mov" => 250,
"mp4" => 240,
"webm" => 210,
"wmv" => 200,
"flv" => 180,
"avi" => 150,
"gif" => 80,
_ => 200,
}
}
fn speed(&self, _from: &str, to: &str) -> u8 {
match to {
"mp4" => 220,
"mov" => 220,
"mkv" => 200,
"flv" => 190,
"avi" => 180,
"wmv" => 170,
"gif" => 150,
"webm" => 100,
_ => 150,
}
}
fn convert(
&self,
input: &[u8],
_from: &str,
to: &str,
temp_dir: &Path,
) -> Result<Vec<u8>, String> {
let mut temp_in = tempfile::Builder::new()
.suffix(&format!(".{}", _from))
.tempfile_in(temp_dir)
.map_err(|e| e.to_string())?;
temp_in.write_all(input).map_err(|e| e.to_string())?;
let temp_out = tempfile::Builder::new()
.suffix(&format!(".{}", to))
.tempfile_in(temp_dir)
.map_err(|e| e.to_string())?;
let temp_out_path = temp_out.into_temp_path();
let in_path = temp_in.path().to_path_buf();
let mut raw_args = vec![];
match to {
"mp4" | "mkv" | "mov" => {
raw_args.extend(vec![
"-c:v", "libx264", "-crf", "23", "-c:a", "aac", "-pix_fmt", "yuv420p",
]);
}
"webm" => {
raw_args.extend(vec![
"-c:v",
"libvpx-vp9",
"-crf",
"30",
"-b:v",
"0",
"-c:a",
"libopus",
]);
}
"avi" => {
raw_args.extend(vec![
"-c:v",
"mpeg4",
"-vtag",
"xvid",
"-qscale:v",
"3",
"-c:a",
"libmp3lame",
]);
}
"gif" => {
raw_args.extend(vec!["-vf", "fps=15,scale=320:-1:flags=lanczos"]);
}
_ => {
raw_args.extend(vec!["-qscale:v", "3"]);
}
}
let rt = tokio::runtime::Runtime::new().map_err(|e| e.to_string())?;
rt.block_on(async {
FFmpegBuilder::new()
.map_err(|e| e.to_string())?
.input_path(in_path)
.output_path(temp_out_path.to_path_buf())
.raw_args(raw_args)
.overwrite()
.run()
.await
.map_err(|e| e.to_string())
})?;
std::fs::read(&temp_out_path).map_err(|e| e.to_string())
}
}
+40 -7
View File
@@ -1,18 +1,51 @@
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 exclusively.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::plugin::Plugin;
use image::ImageFormat;
use std::io::Cursor;
use std::path::Path;
pub struct PluginImpl;
impl Plugin for PluginImpl {
fn name(&self) -> &'static str { "jpeg_to_png" }
fn from_formats(&self) -> Vec<&'static str> { vec!["jpeg"] }
fn to_formats(&self) -> Vec<&'static str> { vec!["png"] }
fn familiarity(&self) -> u8 { 255 }
fn quality(&self) -> u8 { 255 }
fn speed(&self) -> u8 { 200 }
fn name(&self) -> &'static str {
"jpeg_to_png"
}
fn from_formats(&self) -> Vec<&'static str> {
vec!["jpeg"]
}
fn to_formats(&self) -> Vec<&'static str> {
vec!["png"]
}
fn familiarity(&self, _from: &str, _to: &str) -> u8 {
255
}
fn quality(&self, _from: &str, _to: &str) -> u8 {
255
}
fn speed(&self, _from: &str, _to: &str) -> u8 {
200
}
fn convert(&self, input: &[u8], _from: &str, _to: &str) -> Result<Vec<u8>, String> {
fn convert(
&self,
input: &[u8],
_from: &str,
_to: &str,
_temp_dir: &Path,
) -> Result<Vec<u8>, String> {
let img = image::load_from_memory_with_format(input, ImageFormat::Jpeg)
.map_err(|e| e.to_string())?;
let mut buf = Vec::new();
+40 -7
View File
@@ -1,18 +1,51 @@
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 exclusively.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::plugin::Plugin;
use image::ImageFormat;
use std::io::Cursor;
use std::path::Path;
pub struct PluginImpl;
impl Plugin for PluginImpl {
fn name(&self) -> &'static str { "png_to_jpeg" }
fn from_formats(&self) -> Vec<&'static str> { vec!["png"] }
fn to_formats(&self) -> Vec<&'static str> { vec!["jpeg"] }
fn familiarity(&self) -> u8 { 255 }
fn quality(&self) -> u8 { 200 } // JPEG has some compression loss
fn speed(&self) -> u8 { 220 }
fn name(&self) -> &'static str {
"png_to_jpeg"
}
fn from_formats(&self) -> Vec<&'static str> {
vec!["png"]
}
fn to_formats(&self) -> Vec<&'static str> {
vec!["jpeg"]
}
fn familiarity(&self, _from: &str, _to: &str) -> u8 {
255
}
fn quality(&self, _from: &str, _to: &str) -> u8 {
200
} // JPEG has some compression loss
fn speed(&self, _from: &str, _to: &str) -> u8 {
220
}
fn convert(&self, input: &[u8], _from: &str, _to: &str) -> Result<Vec<u8>, String> {
fn convert(
&self,
input: &[u8],
_from: &str,
_to: &str,
_temp_dir: &Path,
) -> Result<Vec<u8>, String> {
let img = image::load_from_memory_with_format(input, ImageFormat::Png)
.map_err(|e| e.to_string())?;
let mut buf = Vec::new();
+45 -4
View File
@@ -1,4 +1,19 @@
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 exclusively.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use clap::Parser;
use std::path::PathBuf;
pub const VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/version.txt"));
@@ -8,16 +23,33 @@ pub struct Args {
#[arg(short = 'v', action = clap::ArgAction::Count, help = "Output information about the conversion. Use -v for path taken, -vv for step info, -vvv for timing, and -vvvv for plugin logs.")]
pub verbose: u8,
#[arg(short, long, help = "Test the conversion process without actually converting the file.")]
#[arg(
short,
long,
help = "Test the conversion process without actually converting the file."
)]
pub test: bool,
#[arg(short, long, help = "Output nothing to the console, silently fail on errors.")]
#[arg(
short,
long,
help = "Output nothing to the console, silently fail on errors."
)]
pub quiet: bool,
#[arg(short = 'c', long, help = "Get rid of the warning message when not piping the file.")]
#[arg(
short = 'c',
long,
help = "Get rid of the warning message when not piping the file."
)]
pub write_to_console: bool,
#[arg(short, long, default_value = "fqs", help = "Set the priority of the conversion process (e.g. -p fqs means \"familiarity, speed, quality\").")]
#[arg(
short,
long,
default_value = "fqs",
help = "Set the priority of the conversion process (e.g. -p fqs means \"familiarity, speed, quality\")."
)]
pub priority: String,
#[arg(help = "The input file path.")]
@@ -25,6 +57,14 @@ pub struct Args {
#[arg(help = "The output file path (optional).")]
pub output_path: Option<String>,
#[arg(
short = 'T',
long,
default_value = "/dev/shm",
help = "Directory to use for temporary files during conversion. Defaults to /dev/shm (RAM). Use a disk path for very large files."
)]
pub temp_dir: PathBuf,
}
impl Args {
@@ -49,6 +89,7 @@ mod tests {
priority: "fqs".to_string(),
input_path: "test".to_string(),
output_path: None,
temp_dir: std::path::PathBuf::from("/dev/shm"),
};
assert_eq!(args.verbosity_level(), $expected);
}
+32 -2
View File
@@ -1,11 +1,37 @@
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 exclusively.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::path::Path;
pub fn identify_format(path: &str) -> Option<&'static str> {
let p = Path::new(path);
let ext = p.extension().and_then(|s| s.to_str()).unwrap_or(path).to_lowercase();
let ext = p
.extension()
.and_then(|s| s.to_str())
.unwrap_or(path)
.to_lowercase();
match ext.as_str() {
"jpg" | "jpeg" => Some("jpeg"),
"png" => Some("png"),
"mp4" => Some("mp4"),
"webm" => Some("webm"),
"mkv" => Some("mkv"),
"avi" => Some("avi"),
"mov" => Some("mov"),
"wmv" => Some("wmv"),
"flv" => Some("flv"),
"gif" => Some("gif"),
_ => None,
}
}
@@ -35,7 +61,11 @@ mod tests {
test_ident!(test_identify_format_10, "test.bmp", None);
test_ident!(test_identify_format_11, "file.JPEG", Some("jpeg"));
test_ident!(test_identify_format_12, "file.PNG", Some("png"));
test_ident!(test_identify_format_13, "complex.file.name.jpg", Some("jpeg"));
test_ident!(
test_identify_format_13,
"complex.file.name.jpg",
Some("jpeg")
);
test_ident!(test_identify_format_14, ".hidden.png", Some("png"));
test_ident!(test_identify_format_15, "jpg", Some("jpeg")); // "jpg" passed without dot, treats as ext if no dot in path but path is "jpg", ext becomes "jpg"
test_ident!(test_identify_format_16, "a.jpg.txt", None);
+25 -4
View File
@@ -1,3 +1,17 @@
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 exclusively.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
pub mod args;
pub mod identifier;
pub mod pathfinder;
@@ -61,7 +75,9 @@ fn main() {
Some(t) => t,
None => {
if !args.quiet {
eprintln!("Error: No output path provided, and no available conversions found.");
eprintln!(
"Error: No output path provided, and no available conversions found."
);
}
process::exit(1);
}
@@ -73,7 +89,10 @@ fn main() {
Some(p) => p,
None => {
if !args.quiet {
eprintln!("Error: No conversion path found from {} to {}.", from_format, to_format);
eprintln!(
"Error: No conversion path found from {} to {}.",
from_format, to_format
);
}
process::exit(1);
}
@@ -88,7 +107,7 @@ fn main() {
}
let verbosity = args.verbosity_level();
let result = runner::run_conversion(&path, &input_bytes, verbosity);
let result = runner::run_conversion(&path, &input_bytes, verbosity, &args.temp_dir);
let output_bytes = match result {
Ok(b) => b,
@@ -111,7 +130,9 @@ fn main() {
let is_piped = !io::stdout().is_terminal();
if !is_piped && !args.write_to_console {
if !args.quiet {
eprintln!("Warning: Outputting directly to console. Use -c to suppress this warning, or redirect to a file.");
eprintln!(
"Warning: Outputting directly to console. Use -c to suppress this warning, or redirect to a file."
);
}
}
+216 -38
View File
@@ -1,3 +1,17 @@
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 exclusively.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::plugin::Plugin;
use std::collections::{HashMap, VecDeque};
use std::rc::Rc;
@@ -73,7 +87,10 @@ pub fn find_best_path<'a>(
for &next_format in &plugin.to_formats() {
let next_depth = current_depth + 1;
let prev_depth = visited_depth.get(next_format).copied().unwrap_or(usize::MAX);
let prev_depth = visited_depth
.get(next_format)
.copied()
.unwrap_or(usize::MAX);
if next_depth <= prev_depth {
visited_depth.insert(next_format, next_depth);
@@ -97,35 +114,57 @@ pub fn find_best_path<'a>(
}
// Evaluate based on priority (maximize score)
best_paths.into_iter().max_by(|path_a, path_b| {
compare_paths(path_a, path_b, priority)
})
best_paths
.into_iter()
.max_by(|path_a, path_b| compare_paths(path_a, path_b, priority))
}
fn compare_paths(path_a: &[(&dyn Plugin, &str, &str)], path_b: &[(&dyn Plugin, &str, &str)], priority: &str) -> std::cmp::Ordering {
fn compare_paths(
path_a: &[(&dyn Plugin, &str, &str)],
path_b: &[(&dyn Plugin, &str, &str)],
priority: &str,
) -> std::cmp::Ordering {
for ch in priority.chars() {
match ch {
'f' | 'F' => {
let score_a: u32 = path_a.iter().map(|(p, _, _)| p.familiarity() as u32).sum();
let score_b: u32 = path_b.iter().map(|(p, _, _)| p.familiarity() as u32).sum();
let score_a: u32 = path_a
.iter()
.map(|(p, from, to)| p.familiarity(*from, *to) as u32)
.sum();
let score_b: u32 = path_b
.iter()
.map(|(p, from, to)| p.familiarity(*from, *to) as u32)
.sum();
if score_a != score_b {
return score_a.cmp(&score_b);
}
},
}
'q' | 'Q' => {
let score_a: u32 = path_a.iter().map(|(p, _, _)| p.quality() as u32).sum();
let score_b: u32 = path_b.iter().map(|(p, _, _)| p.quality() as u32).sum();
let score_a: u32 = path_a
.iter()
.map(|(p, from, to)| p.quality(*from, *to) as u32)
.sum();
let score_b: u32 = path_b
.iter()
.map(|(p, from, to)| p.quality(*from, *to) as u32)
.sum();
if score_a != score_b {
return score_a.cmp(&score_b);
}
},
}
's' | 'S' => {
let score_a: u32 = path_a.iter().map(|(p, _, _)| p.speed() as u32).sum();
let score_b: u32 = path_b.iter().map(|(p, _, _)| p.speed() as u32).sum();
let score_a: u32 = path_a
.iter()
.map(|(p, from, to)| p.speed(*from, *to) as u32)
.sum();
let score_b: u32 = path_b
.iter()
.map(|(p, from, to)| p.speed(*from, *to) as u32)
.sum();
if score_a != score_b {
return score_a.cmp(&score_b);
}
},
}
_ => {}
}
}
@@ -146,21 +185,62 @@ mod tests {
}
impl Plugin for MockPlugin {
fn name(&self) -> &'static str { self.name }
fn from_formats(&self) -> Vec<&'static str> { self.from.clone() }
fn to_formats(&self) -> Vec<&'static str> { self.to.clone() }
fn familiarity(&self) -> u8 { self.f }
fn quality(&self) -> u8 { self.q }
fn speed(&self) -> u8 { self.s }
fn convert(&self, _input: &[u8], _from: &str, _to: &str) -> Result<Vec<u8>, String> { Ok(vec![]) }
fn name(&self) -> &'static str {
self.name
}
fn from_formats(&self) -> Vec<&'static str> {
self.from.clone()
}
fn to_formats(&self) -> Vec<&'static str> {
self.to.clone()
}
fn familiarity(&self, _from: &str, _to: &str) -> u8 {
self.f
}
fn quality(&self, _from: &str, _to: &str) -> u8 {
self.q
}
fn speed(&self, _from: &str, _to: &str) -> u8 {
self.s
}
fn convert(
&self,
_input: &[u8],
_from: &str,
_to: &str,
_temp_dir: &std::path::Path,
) -> Result<Vec<u8>, String> {
Ok(vec![])
}
}
#[test]
fn test_shortest_path() {
let plugins: Vec<Box<dyn Plugin>> = vec![
Box::new(MockPlugin { name: "A", from: vec!["jpeg"], to: vec!["png"], f: 10, q: 10, s: 10 }),
Box::new(MockPlugin { name: "B", from: vec!["jpeg"], to: vec!["bmp"], f: 10, q: 10, s: 10 }),
Box::new(MockPlugin { name: "C", from: vec!["bmp"], to: vec!["png"], f: 10, q: 10, s: 10 }),
Box::new(MockPlugin {
name: "A",
from: vec!["jpeg"],
to: vec!["png"],
f: 10,
q: 10,
s: 10,
}),
Box::new(MockPlugin {
name: "B",
from: vec!["jpeg"],
to: vec!["bmp"],
f: 10,
q: 10,
s: 10,
}),
Box::new(MockPlugin {
name: "C",
from: vec!["bmp"],
to: vec!["png"],
f: 10,
q: 10,
s: 10,
}),
];
let path = find_best_path(&plugins, "jpeg", "png", "fqs").unwrap();
@@ -171,8 +251,22 @@ mod tests {
#[test]
fn test_priority_tie_break() {
let plugins: Vec<Box<dyn Plugin>> = vec![
Box::new(MockPlugin { name: "A", from: vec!["jpeg"], to: vec!["png"], f: 10, q: 50, s: 10 }),
Box::new(MockPlugin { name: "B", from: vec!["jpeg"], to: vec!["png"], f: 50, q: 10, s: 10 }),
Box::new(MockPlugin {
name: "A",
from: vec!["jpeg"],
to: vec!["png"],
f: 10,
q: 50,
s: 10,
}),
Box::new(MockPlugin {
name: "B",
from: vec!["jpeg"],
to: vec!["png"],
f: 50,
q: 10,
s: 10,
}),
];
let path = find_best_path(&plugins, "jpeg", "png", "fqs").unwrap();
@@ -187,13 +281,62 @@ mod tests {
#[test]
fn $name() {
let plugins: Vec<Box<dyn Plugin>> = vec![
Box::new(MockPlugin { name: "A", from: vec!["a"], to: vec!["b"], f: 10, q: 10, s: 10 }),
Box::new(MockPlugin { name: "B", from: vec!["b"], to: vec!["c"], f: 20, q: 10, s: 10 }),
Box::new(MockPlugin { name: "C", from: vec!["a"], to: vec!["c"], f: 5, q: 10, s: 10 }),
Box::new(MockPlugin { name: "D", from: vec!["a"], to: vec!["d"], f: 10, q: 20, s: 10 }),
Box::new(MockPlugin { name: "E", from: vec!["d"], to: vec!["c"], f: 10, q: 20, s: 10 }),
Box::new(MockPlugin { name: "F", from: vec!["a"], to: vec!["b"], f: 50, q: 5, s: 5 }), // High familiarity, low q/s
Box::new(MockPlugin { name: "G", from: vec!["c"], to: vec!["e"], f: 10, q: 10, s: 50 }),
Box::new(MockPlugin {
name: "A",
from: vec!["a"],
to: vec!["b"],
f: 10,
q: 10,
s: 10,
}),
Box::new(MockPlugin {
name: "B",
from: vec!["b"],
to: vec!["c"],
f: 20,
q: 10,
s: 10,
}),
Box::new(MockPlugin {
name: "C",
from: vec!["a"],
to: vec!["c"],
f: 5,
q: 10,
s: 10,
}),
Box::new(MockPlugin {
name: "D",
from: vec!["a"],
to: vec!["d"],
f: 10,
q: 20,
s: 10,
}),
Box::new(MockPlugin {
name: "E",
from: vec!["d"],
to: vec!["c"],
f: 10,
q: 20,
s: 10,
}),
Box::new(MockPlugin {
name: "F",
from: vec!["a"],
to: vec!["b"],
f: 50,
q: 5,
s: 5,
}), // High familiarity, low q/s
Box::new(MockPlugin {
name: "G",
from: vec!["c"],
to: vec!["e"],
f: 10,
q: 10,
s: 50,
}),
];
let path = find_best_path(&plugins, $from, $to, $priority);
@@ -224,11 +367,46 @@ mod tests {
#[test]
fn $name() {
let plugins: Vec<Box<dyn Plugin>> = vec![
Box::new(MockPlugin { name: "1", from: vec!["1"], to: vec!["2"], f: 10, q: 10, s: 10 }),
Box::new(MockPlugin { name: "2", from: vec!["2"], to: vec!["3"], f: 10, q: 10, s: 10 }),
Box::new(MockPlugin { name: "3", from: vec!["3"], to: vec!["4"], f: 10, q: 10, s: 10 }),
Box::new(MockPlugin { name: "4", from: vec!["4"], to: vec!["5"], f: 10, q: 10, s: 10 }),
Box::new(MockPlugin { name: "5", from: vec!["5"], to: vec!["6"], f: 10, q: 10, s: 10 }),
Box::new(MockPlugin {
name: "1",
from: vec!["1"],
to: vec!["2"],
f: 10,
q: 10,
s: 10,
}),
Box::new(MockPlugin {
name: "2",
from: vec!["2"],
to: vec!["3"],
f: 10,
q: 10,
s: 10,
}),
Box::new(MockPlugin {
name: "3",
from: vec!["3"],
to: vec!["4"],
f: 10,
q: 10,
s: 10,
}),
Box::new(MockPlugin {
name: "4",
from: vec!["4"],
to: vec!["5"],
f: 10,
q: 10,
s: 10,
}),
Box::new(MockPlugin {
name: "5",
from: vec!["5"],
to: vec!["6"],
f: 10,
q: 10,
s: 10,
}),
];
let path = find_best_path(&plugins, $from, $to, $priority);
if $expected_len == 0 {
+27 -4
View File
@@ -1,13 +1,36 @@
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 exclusively.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use std::path::Path;
pub trait Plugin: Send + Sync {
fn name(&self) -> &'static str;
fn from_formats(&self) -> Vec<&'static str>;
fn to_formats(&self) -> Vec<&'static str>;
/// Score from 1 to 255. Higher is better.
fn familiarity(&self) -> u8;
fn quality(&self) -> u8;
fn speed(&self) -> u8;
fn familiarity(&self, from_format: &str, to_format: &str) -> u8;
fn quality(&self, from_format: &str, to_format: &str) -> u8;
fn speed(&self, from_format: &str, to_format: &str) -> u8;
/// Converts the given input bytes from `from_format` to `to_format`.
fn convert(&self, input: &[u8], from_format: &str, to_format: &str) -> Result<Vec<u8>, String>;
/// `temp_dir` is the directory to use for any intermediate scratch files.
fn convert(
&self,
input: &[u8],
from_format: &str,
to_format: &str,
temp_dir: &Path,
) -> Result<Vec<u8>, String>;
}
+67 -13
View File
@@ -1,10 +1,26 @@
// Copyright (C) 2026 Elias Wendland <eliaswendland@pm.me>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 exclusively.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::plugin::Plugin;
use std::path::Path;
use std::time::Instant;
pub fn run_conversion(
path: &[(&dyn Plugin, &str, &str)],
input: &[u8],
verbosity: u8,
temp_dir: &Path,
) -> Result<Vec<u8>, String> {
if verbosity >= 1 {
let path_str: Vec<_> = path.iter().map(|(p, _, _)| p.name()).collect();
@@ -15,22 +31,35 @@ pub fn run_conversion(
for (plugin, from_format, to_format) in path {
if verbosity >= 2 {
eprintln!("Converting {} to {} using plugin {}...", from_format, to_format, plugin.name());
eprintln!(
"Converting {} to {} using plugin {}...",
from_format,
to_format,
plugin.name()
);
}
if verbosity >= 4 {
eprintln!("[Plugin Log] Running plugin: {}", plugin.name());
eprintln!("[Plugin Log] Source format: {}", from_format);
eprintln!("[Plugin Log] Target format: {}", to_format);
eprintln!("[Plugin Log] Metrics - Familiarity: {}, Quality: {}, Speed: {}", plugin.familiarity(), plugin.quality(), plugin.speed());
eprintln!(
"[Plugin Log] Metrics - Familiarity: {}, Quality: {}, Speed: {}",
plugin.familiarity(*from_format, *to_format),
plugin.quality(*from_format, *to_format),
plugin.speed(*from_format, *to_format)
);
}
let start_time = Instant::now();
current_data = plugin.convert(&current_data, from_format, to_format)?;
current_data = plugin.convert(&current_data, from_format, to_format, temp_dir)?;
let elapsed = start_time.elapsed();
if verbosity >= 3 {
eprintln!("Step '{} -> {}' completed in {:.2?}", from_format, to_format, elapsed);
eprintln!(
"Step '{} -> {}' completed in {:.2?}",
from_format, to_format, elapsed
);
}
}
@@ -50,13 +79,31 @@ mod tests {
}
impl Plugin for MockPlugin {
fn name(&self) -> &'static str { self.name }
fn from_formats(&self) -> Vec<&'static str> { vec!["a"] }
fn to_formats(&self) -> Vec<&'static str> { vec!["b"] }
fn familiarity(&self) -> u8 { self.f }
fn quality(&self) -> u8 { self.q }
fn speed(&self) -> u8 { self.s }
fn convert(&self, input: &[u8], _from: &str, _to: &str) -> Result<Vec<u8>, String> {
fn name(&self) -> &'static str {
self.name
}
fn from_formats(&self) -> Vec<&'static str> {
vec!["a"]
}
fn to_formats(&self) -> Vec<&'static str> {
vec!["b"]
}
fn familiarity(&self, _from: &str, _to: &str) -> u8 {
self.f
}
fn quality(&self, _from: &str, _to: &str) -> u8 {
self.q
}
fn speed(&self, _from: &str, _to: &str) -> u8 {
self.s
}
fn convert(
&self,
input: &[u8],
_from: &str,
_to: &str,
_temp_dir: &Path,
) -> Result<Vec<u8>, String> {
if self.fail {
return Err("Failed".to_string());
}
@@ -70,11 +117,18 @@ mod tests {
($name:ident, $verbosity:expr, $fail:expr, $expected_res:expr, $expected_len:expr) => {
#[test]
fn $name() {
let plugin = MockPlugin { name: "mock", f: 10, q: 10, s: 10, fail: $fail };
let plugin = MockPlugin {
name: "mock",
f: 10,
q: 10,
s: 10,
fail: $fail,
};
let path: Vec<(&dyn Plugin, &str, &str)> = vec![(&plugin, "a", "b")];
let input = vec![0];
let result = run_conversion(&path, &input, $verbosity);
let result =
run_conversion(&path, &input, $verbosity, std::path::Path::new("/dev/shm"));
assert_eq!(result.is_ok(), $expected_res);
if let Ok(res) = result {
assert_eq!(res.len(), $expected_len);