1pub mod builtin;
20pub mod custom;
21pub mod device;
22pub mod streaming;
23pub mod validate;
24pub mod web;
25
26use anyhow::{anyhow, Context, Result};
27use serde::{Deserialize, Serialize};
28use std::collections::HashMap;
29use std::path::{Path, PathBuf};
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
33pub struct VideoConfig {
34 pub codec: String,
36
37 #[serde(skip_serializing_if = "Option::is_none")]
39 pub bitrate: Option<String>,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub crf: Option<u32>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub width: Option<u32>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub height: Option<u32>,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub fps: Option<f64>,
56
57 #[serde(skip_serializing_if = "Option::is_none")]
59 pub preset: Option<String>,
60
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub pixel_format: Option<String>,
64
65 #[serde(default)]
67 pub two_pass: bool,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub max_bitrate: Option<String>,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub min_bitrate: Option<String>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub buffer_size: Option<String>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub keyframe_interval: Option<u32>,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub min_keyframe_interval: Option<u32>,
88
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub aspect_ratio: Option<String>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
96pub struct AudioConfig {
97 pub codec: String,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
102 pub bitrate: Option<String>,
103
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub sample_rate: Option<u32>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
110 pub channels: Option<u32>,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub quality: Option<f64>,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub compression_level: Option<u32>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
123pub struct FilterConfig {
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub video_filters: Option<Vec<String>>,
127
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub audio_filters: Option<Vec<String>>,
131
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub deinterlace: Option<String>,
135
136 #[serde(skip_serializing_if = "Option::is_none")]
138 pub denoise: Option<String>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
143pub struct Preset {
144 pub name: String,
146
147 pub description: String,
149
150 pub category: PresetCategory,
152
153 pub video: VideoConfig,
155
156 pub audio: AudioConfig,
158
159 pub container: String,
161
162 #[serde(skip_serializing_if = "Option::is_none")]
164 pub filters: Option<FilterConfig>,
165
166 #[serde(default)]
168 pub builtin: bool,
169
170 #[serde(default)]
172 pub tags: Vec<String>,
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
177pub enum PresetCategory {
178 Web,
180
181 Device,
183
184 Quality,
186
187 Archival,
189
190 Streaming,
192
193 Custom,
195}
196
197impl PresetCategory {
198 pub fn name(&self) -> &'static str {
200 match self {
201 Self::Web => "Web",
202 Self::Device => "Device",
203 Self::Quality => "Quality",
204 Self::Archival => "Archival",
205 Self::Streaming => "Streaming",
206 Self::Custom => "Custom",
207 }
208 }
209
210 pub fn description(&self) -> &'static str {
212 match self {
213 Self::Web => "Presets optimized for web platforms (YouTube, Vimeo, social media)",
214 Self::Device => "Presets optimized for specific devices (iPhone, Android, TV)",
215 Self::Quality => "Quality tier presets (4K, 1080p, 720p, 480p)",
216 Self::Archival => "Archival presets (lossless, high quality preservation)",
217 Self::Streaming => "Streaming presets (HLS/DASH adaptive bitrate variants)",
218 Self::Custom => "User-defined custom presets",
219 }
220 }
221
222 pub fn from_str(s: &str) -> Result<Self> {
224 match s.to_lowercase().as_str() {
225 "web" => Ok(Self::Web),
226 "device" => Ok(Self::Device),
227 "quality" => Ok(Self::Quality),
228 "archival" => Ok(Self::Archival),
229 "streaming" => Ok(Self::Streaming),
230 "custom" => Ok(Self::Custom),
231 _ => Err(anyhow!("Unknown preset category: {}", s)),
232 }
233 }
234}
235
236pub struct PresetManager {
238 presets: HashMap<String, Preset>,
240
241 custom_dir: Option<PathBuf>,
243}
244
245impl PresetManager {
246 pub fn new() -> Self {
248 let mut manager = Self {
249 presets: HashMap::new(),
250 custom_dir: None,
251 };
252
253 manager.load_builtin_presets();
255
256 manager
257 }
258
259 pub fn with_custom_dir<P: AsRef<Path>>(custom_dir: P) -> Result<Self> {
261 let mut manager = Self::new();
262 manager.custom_dir = Some(custom_dir.as_ref().to_path_buf());
263 manager.load_custom_presets()?;
264 Ok(manager)
265 }
266
267 fn load_builtin_presets(&mut self) {
269 for preset in web::get_web_presets() {
271 self.presets.insert(preset.name.clone(), preset);
272 }
273
274 for preset in device::get_device_presets() {
276 self.presets.insert(preset.name.clone(), preset);
277 }
278
279 for preset in streaming::get_streaming_presets() {
281 self.presets.insert(preset.name.clone(), preset);
282 }
283
284 for preset in builtin::get_quality_presets() {
286 self.presets.insert(preset.name.clone(), preset);
287 }
288
289 for preset in builtin::get_archival_presets() {
290 self.presets.insert(preset.name.clone(), preset);
291 }
292 }
293
294 fn load_custom_presets(&mut self) -> Result<()> {
296 if let Some(ref dir) = self.custom_dir {
297 if dir.exists() && dir.is_dir() {
298 for entry in std::fs::read_dir(dir)? {
299 let entry = entry?;
300 let path = entry.path();
301
302 if path.extension().and_then(|s| s.to_str()) == Some("toml") {
303 match custom::load_preset_from_file(&path) {
304 Ok(preset) => {
305 if let Some(existing) = self.presets.get(&preset.name) {
307 if existing.builtin {
308 eprintln!(
309 "Warning: Cannot override built-in preset '{}' with custom preset",
310 preset.name
311 );
312 continue;
313 }
314 }
315 self.presets.insert(preset.name.clone(), preset);
316 }
317 Err(e) => {
318 eprintln!(
319 "Warning: Failed to load preset from {}: {}",
320 path.display(),
321 e
322 );
323 }
324 }
325 }
326 }
327 }
328 }
329 Ok(())
330 }
331
332 pub fn get_preset(&self, name: &str) -> Result<&Preset> {
334 self.presets
335 .get(name)
336 .ok_or_else(|| anyhow!("Preset '{}' not found", name))
337 }
338
339 pub fn list_presets(&self) -> Vec<&Preset> {
341 let mut presets: Vec<_> = self.presets.values().collect();
342 presets.sort_by(|a, b| a.name.cmp(&b.name));
343 presets
344 }
345
346 pub fn list_presets_by_category(&self, category: PresetCategory) -> Vec<&Preset> {
348 let mut presets: Vec<_> = self
349 .presets
350 .values()
351 .filter(|p| p.category == category)
352 .collect();
353 presets.sort_by(|a, b| a.name.cmp(&b.name));
354 presets
355 }
356
357 pub fn preset_names(&self) -> Vec<String> {
359 let mut names: Vec<_> = self.presets.keys().cloned().collect();
360 names.sort();
361 names
362 }
363
364 #[allow(dead_code)]
366 pub fn has_preset(&self, name: &str) -> bool {
367 self.presets.contains_key(name)
368 }
369
370 #[allow(dead_code)]
372 pub fn add_preset(&mut self, preset: Preset) -> Result<()> {
373 validate::validate_preset(&preset)?;
375
376 if let Some(existing) = self.presets.get(&preset.name) {
378 if existing.builtin {
379 return Err(anyhow!("Cannot override built-in preset '{}'", preset.name));
380 }
381 }
382
383 self.presets.insert(preset.name.clone(), preset);
384 Ok(())
385 }
386
387 #[allow(dead_code)]
389 pub fn save_preset(&self, name: &str) -> Result<()> {
390 let preset = self.get_preset(name)?;
391
392 if preset.builtin {
393 return Err(anyhow!("Cannot save built-in preset '{}'", name));
394 }
395
396 let dir = self
397 .custom_dir
398 .as_ref()
399 .ok_or_else(|| anyhow!("No custom preset directory configured"))?;
400
401 if !dir.exists() {
402 std::fs::create_dir_all(dir).context("Failed to create custom preset directory")?;
403 }
404
405 custom::save_preset_to_file(preset, dir)?;
406 Ok(())
407 }
408
409 #[allow(dead_code)]
411 pub fn remove_preset(&mut self, name: &str) -> Result<()> {
412 let preset = self.get_preset(name)?;
413
414 if preset.builtin {
415 return Err(anyhow!("Cannot remove built-in preset '{}'", name));
416 }
417
418 self.presets.remove(name);
419
420 if let Some(ref dir) = self.custom_dir {
422 let path = dir.join(format!("{}.toml", name));
423 if path.exists() {
424 std::fs::remove_file(&path).context("Failed to remove preset file")?;
425 }
426 }
427
428 Ok(())
429 }
430
431 pub fn default_custom_dir() -> Result<PathBuf> {
433 let config_dir =
434 dirs::config_dir().ok_or_else(|| anyhow!("Could not determine config directory"))?;
435 Ok(config_dir.join("oximedia").join("presets"))
436 }
437}
438
439impl Default for PresetManager {
440 fn default() -> Self {
441 Self::new()
442 }
443}
444
445#[allow(dead_code)]
447pub fn parse_bitrate(bitrate: &str) -> Result<u64> {
448 let bitrate = bitrate.trim();
449 let multiplier = if bitrate.ends_with('M') || bitrate.ends_with('m') {
450 1_000_000
451 } else if bitrate.ends_with('K') || bitrate.ends_with('k') {
452 1_000
453 } else {
454 1
455 };
456
457 let numeric = bitrate.trim_end_matches(|c: char| c.is_alphabetic()).trim();
458
459 let value: f64 = numeric
460 .parse()
461 .context(format!("Invalid bitrate format: {}", bitrate))?;
462
463 Ok((value * multiplier as f64) as u64)
464}
465
466#[allow(dead_code)]
468pub fn format_bitrate(bits_per_second: u64) -> String {
469 if bits_per_second >= 1_000_000 {
470 format!("{:.1}M", bits_per_second as f64 / 1_000_000.0)
471 } else if bits_per_second >= 1_000 {
472 format!("{}k", bits_per_second / 1_000)
473 } else {
474 format!("{}", bits_per_second)
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn test_parse_bitrate() {
484 assert_eq!(
485 parse_bitrate("5M").expect("parse should succeed"),
486 5_000_000
487 );
488 assert_eq!(
489 parse_bitrate("2.5M").expect("parse should succeed"),
490 2_500_000
491 );
492 assert_eq!(
493 parse_bitrate("128k").expect("parse should succeed"),
494 128_000
495 );
496 assert_eq!(parse_bitrate("1000").expect("parse should succeed"), 1_000);
497 }
498
499 #[test]
500 fn test_format_bitrate() {
501 assert_eq!(format_bitrate(5_000_000), "5.0M");
502 assert_eq!(format_bitrate(128_000), "128k");
503 assert_eq!(format_bitrate(500), "500");
504 }
505}