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
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:
1 parent
e46696c521
commit
b97a108d1e
10 files changed
+705
-134
No files matched your search
+233
-55
@@ -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;
|
||||
@@ -14,13 +28,13 @@ impl<'a> PathNode<'a> {
|
||||
pub fn get_path(&self) -> Vec<(&'a dyn Plugin, &'a str, &'a str)> {
|
||||
let mut path = Vec::new();
|
||||
path.push((self.plugin, self.from_format, self.to_format));
|
||||
|
||||
|
||||
let mut curr = self.prev.clone();
|
||||
while let Some(node) = curr {
|
||||
path.push((node.plugin, node.from_format, node.to_format));
|
||||
curr = node.prev.clone();
|
||||
}
|
||||
|
||||
|
||||
path.reverse();
|
||||
path
|
||||
}
|
||||
@@ -42,22 +56,22 @@ pub fn find_best_path<'a>(
|
||||
// BFS to find paths with least amount of conversions
|
||||
let mut queue = VecDeque::new();
|
||||
queue.push_back((from_format, None::<Rc<PathNode<'a>>>));
|
||||
|
||||
|
||||
let mut min_length = None;
|
||||
let mut best_paths: Vec<Vec<(&'a dyn Plugin, &'a str, &'a str)>> = Vec::new();
|
||||
|
||||
|
||||
let mut visited_depth = HashMap::new();
|
||||
visited_depth.insert(from_format, 0);
|
||||
|
||||
while let Some((curr_format, prev_node)) = queue.pop_front() {
|
||||
let current_depth = visited_depth.get(curr_format).copied().unwrap_or(0);
|
||||
|
||||
|
||||
if let Some(min_len) = min_length {
|
||||
if current_depth > min_len {
|
||||
break; // We've moved beyond the shortest paths
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if curr_format == to_format && prev_node.is_some() {
|
||||
if min_length.is_none() {
|
||||
min_length = Some(current_depth);
|
||||
@@ -72,60 +86,85 @@ pub fn find_best_path<'a>(
|
||||
for plugin in neighbors {
|
||||
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);
|
||||
|
||||
|
||||
let new_node = Rc::new(PathNode {
|
||||
plugin: *plugin,
|
||||
from_format: curr_format,
|
||||
to_format: next_format,
|
||||
prev: prev_node.clone(),
|
||||
});
|
||||
|
||||
|
||||
queue.push_back((next_format, Some(new_node)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if best_paths.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
// 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,23 +185,64 @@ 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();
|
||||
assert_eq!(path.len(), 1);
|
||||
assert_eq!(path[0].0.name(), "A");
|
||||
@@ -171,13 +251,27 @@ 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();
|
||||
assert_eq!(path[0].0.name(), "B");
|
||||
|
||||
|
||||
let path = find_best_path(&plugins, "jpeg", "png", "qfs").unwrap();
|
||||
assert_eq!(path[0].0.name(), "A");
|
||||
}
|
||||
@@ -187,15 +281,64 @@ 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);
|
||||
if $expected_len == 0 {
|
||||
assert!(path.is_none());
|
||||
@@ -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 {
|
||||
@@ -239,7 +417,7 @@ mod tests {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
test_pathfinder_2!(test_p2_1, "1", "2", "f", 1);
|
||||
test_pathfinder_2!(test_p2_2, "1", "3", "f", 2);
|
||||
test_pathfinder_2!(test_p2_3, "1", "4", "f", 3);
|
||||
|
||||
Reference in new issue
Block a user