pub mod builtin;
pub mod custom;
pub mod device;
pub mod streaming;
pub mod validate;
pub mod web;
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VideoConfig {
pub codec: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub bitrate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub crf: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub height: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fps: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preset: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pixel_format: Option<String>,
#[serde(default)]
pub two_pass: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_bitrate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_bitrate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub buffer_size: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keyframe_interval: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub min_keyframe_interval: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aspect_ratio: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AudioConfig {
pub codec: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub bitrate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sample_rate: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub channels: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compression_level: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FilterConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub video_filters: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub audio_filters: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deinterlace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub denoise: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Preset {
pub name: String,
pub description: String,
pub category: PresetCategory,
pub video: VideoConfig,
pub audio: AudioConfig,
pub container: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub filters: Option<FilterConfig>,
#[serde(default)]
pub builtin: bool,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum PresetCategory {
Web,
Device,
Quality,
Archival,
Streaming,
Custom,
}
impl PresetCategory {
pub fn name(&self) -> &'static str {
match self {
Self::Web => "Web",
Self::Device => "Device",
Self::Quality => "Quality",
Self::Archival => "Archival",
Self::Streaming => "Streaming",
Self::Custom => "Custom",
}
}
pub fn description(&self) -> &'static str {
match self {
Self::Web => "Presets optimized for web platforms (YouTube, Vimeo, social media)",
Self::Device => "Presets optimized for specific devices (iPhone, Android, TV)",
Self::Quality => "Quality tier presets (4K, 1080p, 720p, 480p)",
Self::Archival => "Archival presets (lossless, high quality preservation)",
Self::Streaming => "Streaming presets (HLS/DASH adaptive bitrate variants)",
Self::Custom => "User-defined custom presets",
}
}
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_str() {
"web" => Ok(Self::Web),
"device" => Ok(Self::Device),
"quality" => Ok(Self::Quality),
"archival" => Ok(Self::Archival),
"streaming" => Ok(Self::Streaming),
"custom" => Ok(Self::Custom),
_ => Err(anyhow!("Unknown preset category: {}", s)),
}
}
}
pub struct PresetManager {
presets: HashMap<String, Preset>,
custom_dir: Option<PathBuf>,
}
impl PresetManager {
pub fn new() -> Self {
let mut manager = Self {
presets: HashMap::new(),
custom_dir: None,
};
manager.load_builtin_presets();
manager
}
pub fn with_custom_dir<P: AsRef<Path>>(custom_dir: P) -> Result<Self> {
let mut manager = Self::new();
manager.custom_dir = Some(custom_dir.as_ref().to_path_buf());
manager.load_custom_presets()?;
Ok(manager)
}
fn load_builtin_presets(&mut self) {
for preset in web::get_web_presets() {
self.presets.insert(preset.name.clone(), preset);
}
for preset in device::get_device_presets() {
self.presets.insert(preset.name.clone(), preset);
}
for preset in streaming::get_streaming_presets() {
self.presets.insert(preset.name.clone(), preset);
}
for preset in builtin::get_quality_presets() {
self.presets.insert(preset.name.clone(), preset);
}
for preset in builtin::get_archival_presets() {
self.presets.insert(preset.name.clone(), preset);
}
}
fn load_custom_presets(&mut self) -> Result<()> {
if let Some(ref dir) = self.custom_dir {
if dir.exists() && dir.is_dir() {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("toml") {
match custom::load_preset_from_file(&path) {
Ok(preset) => {
if let Some(existing) = self.presets.get(&preset.name) {
if existing.builtin {
eprintln!(
"Warning: Cannot override built-in preset '{}' with custom preset",
preset.name
);
continue;
}
}
self.presets.insert(preset.name.clone(), preset);
}
Err(e) => {
eprintln!(
"Warning: Failed to load preset from {}: {}",
path.display(),
e
);
}
}
}
}
}
}
Ok(())
}
pub fn get_preset(&self, name: &str) -> Result<&Preset> {
self.presets
.get(name)
.ok_or_else(|| anyhow!("Preset '{}' not found", name))
}
pub fn list_presets(&self) -> Vec<&Preset> {
let mut presets: Vec<_> = self.presets.values().collect();
presets.sort_by(|a, b| a.name.cmp(&b.name));
presets
}
pub fn list_presets_by_category(&self, category: PresetCategory) -> Vec<&Preset> {
let mut presets: Vec<_> = self
.presets
.values()
.filter(|p| p.category == category)
.collect();
presets.sort_by(|a, b| a.name.cmp(&b.name));
presets
}
pub fn preset_names(&self) -> Vec<String> {
let mut names: Vec<_> = self.presets.keys().cloned().collect();
names.sort();
names
}
#[allow(dead_code)]
pub fn has_preset(&self, name: &str) -> bool {
self.presets.contains_key(name)
}
#[allow(dead_code)]
pub fn add_preset(&mut self, preset: Preset) -> Result<()> {
validate::validate_preset(&preset)?;
if let Some(existing) = self.presets.get(&preset.name) {
if existing.builtin {
return Err(anyhow!("Cannot override built-in preset '{}'", preset.name));
}
}
self.presets.insert(preset.name.clone(), preset);
Ok(())
}
#[allow(dead_code)]
pub fn save_preset(&self, name: &str) -> Result<()> {
let preset = self.get_preset(name)?;
if preset.builtin {
return Err(anyhow!("Cannot save built-in preset '{}'", name));
}
let dir = self
.custom_dir
.as_ref()
.ok_or_else(|| anyhow!("No custom preset directory configured"))?;
if !dir.exists() {
std::fs::create_dir_all(dir).context("Failed to create custom preset directory")?;
}
custom::save_preset_to_file(preset, dir)?;
Ok(())
}
#[allow(dead_code)]
pub fn remove_preset(&mut self, name: &str) -> Result<()> {
let preset = self.get_preset(name)?;
if preset.builtin {
return Err(anyhow!("Cannot remove built-in preset '{}'", name));
}
self.presets.remove(name);
if let Some(ref dir) = self.custom_dir {
let path = dir.join(format!("{}.toml", name));
if path.exists() {
std::fs::remove_file(&path).context("Failed to remove preset file")?;
}
}
Ok(())
}
pub fn default_custom_dir() -> Result<PathBuf> {
let config_dir =
dirs::config_dir().ok_or_else(|| anyhow!("Could not determine config directory"))?;
Ok(config_dir.join("oximedia").join("presets"))
}
}
impl Default for PresetManager {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
pub fn parse_bitrate(bitrate: &str) -> Result<u64> {
let bitrate = bitrate.trim();
let multiplier = if bitrate.ends_with('M') || bitrate.ends_with('m') {
1_000_000
} else if bitrate.ends_with('K') || bitrate.ends_with('k') {
1_000
} else {
1
};
let numeric = bitrate.trim_end_matches(|c: char| c.is_alphabetic()).trim();
let value: f64 = numeric
.parse()
.context(format!("Invalid bitrate format: {}", bitrate))?;
Ok((value * multiplier as f64) as u64)
}
#[allow(dead_code)]
pub fn format_bitrate(bits_per_second: u64) -> String {
if bits_per_second >= 1_000_000 {
format!("{:.1}M", bits_per_second as f64 / 1_000_000.0)
} else if bits_per_second >= 1_000 {
format!("{}k", bits_per_second / 1_000)
} else {
format!("{}", bits_per_second)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_bitrate() {
assert_eq!(
parse_bitrate("5M").expect("parse should succeed"),
5_000_000
);
assert_eq!(
parse_bitrate("2.5M").expect("parse should succeed"),
2_500_000
);
assert_eq!(
parse_bitrate("128k").expect("parse should succeed"),
128_000
);
assert_eq!(parse_bitrate("1000").expect("parse should succeed"), 1_000);
}
#[test]
fn test_format_bitrate() {
assert_eq!(format_bitrate(5_000_000), "5.0M");
assert_eq!(format_bitrate(128_000), "128k");
assert_eq!(format_bitrate(500), "500");
}
}