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