1use serde::{Deserialize, Serialize};
4use std::fmt;
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
8pub struct QualityProfile {
9 pub bitrate: u32,
11 pub sample_rate: u32,
13 pub channels: u8,
15 pub codec: String,
17 pub duration: f64,
19}
20
21impl QualityProfile {
22 pub fn new(bitrate: u32, sample_rate: u32, channels: u8, codec: String, duration: f64) -> anyhow::Result<Self> {
24 if bitrate == 0 {
25 anyhow::bail!("Bitrate must be positive, got {}", bitrate);
26 }
27 if sample_rate == 0 {
28 anyhow::bail!("Sample rate must be positive, got {}", sample_rate);
29 }
30 if channels != 1 && channels != 2 {
31 anyhow::bail!("Channels must be 1 or 2, got {}", channels);
32 }
33
34 Ok(Self {
35 bitrate,
36 sample_rate,
37 channels,
38 codec,
39 duration,
40 })
41 }
42
43 pub fn is_better_than(&self, other: &QualityProfile, prefer_stereo: bool) -> bool {
45 if self.bitrate != other.bitrate {
48 return self.bitrate > other.bitrate;
49 }
50
51 if self.sample_rate != other.sample_rate {
53 return self.sample_rate > other.sample_rate;
54 }
55
56 if self.channels != other.channels {
58 if prefer_stereo {
59 return self.channels > other.channels;
60 } else {
61 return self.channels < other.channels;
62 }
63 }
64
65 let codec_priority = |codec: &str| match codec.to_lowercase().as_str() {
67 "aac" => 2,
68 "mp3" => 1,
69 _ => 0,
70 };
71
72 codec_priority(&self.codec) > codec_priority(&other.codec)
73 }
74
75 pub fn is_compatible_for_concat(&self, other: &QualityProfile) -> bool {
77 self.bitrate == other.bitrate
78 && self.sample_rate == other.sample_rate
79 && self.channels == other.channels
80 && self.codec.to_lowercase() == other.codec.to_lowercase()
81 }
82
83 pub fn to_aac_equivalent(&self) -> QualityProfile {
85 let aac_bitrate = self.bitrate.max(128); QualityProfile {
89 bitrate: aac_bitrate,
90 sample_rate: self.sample_rate,
91 channels: self.channels,
92 codec: "aac".to_string(),
93 duration: self.duration,
94 }
95 }
96
97 pub fn from_preset(preset: &str, source: &QualityProfile) -> Option<QualityProfile> {
100 match preset.to_lowercase().as_str() {
101 "low" => Some(QualityProfile {
102 bitrate: 64,
103 sample_rate: 22050,
104 channels: 1, codec: "aac".to_string(),
106 duration: source.duration,
107 }),
108 "medium" => Some(QualityProfile {
109 bitrate: 96,
110 sample_rate: 44100,
111 channels: 2, codec: "aac".to_string(),
113 duration: source.duration,
114 }),
115 "high" => Some(QualityProfile {
116 bitrate: 128,
117 sample_rate: 48000,
118 channels: 2, codec: "aac".to_string(),
120 duration: source.duration,
121 }),
122 "source" | _ => None, }
124 }
125
126 pub fn apply_preset(&self, preset: Option<&str>) -> QualityProfile {
128 preset
129 .and_then(|p| Self::from_preset(p, self))
130 .unwrap_or_else(|| self.clone())
131 }
132}
133
134impl fmt::Display for QualityProfile {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 write!(
137 f,
138 "{}kbps, {}Hz, {}ch, {}, {:.1}s",
139 self.bitrate, self.sample_rate, self.channels, self.codec, self.duration
140 )
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_quality_creation() {
150 let profile = QualityProfile::new(128, 44100, 2, "aac".to_string(), 3600.0).unwrap();
151 assert_eq!(profile.bitrate, 128);
152 assert_eq!(profile.sample_rate, 44100);
153 assert_eq!(profile.channels, 2);
154 assert_eq!(profile.codec, "aac");
155 }
156
157 #[test]
158 fn test_quality_validation() {
159 assert!(QualityProfile::new(0, 44100, 2, "aac".to_string(), 3600.0).is_err());
160 assert!(QualityProfile::new(128, 0, 2, "aac".to_string(), 3600.0).is_err());
161 assert!(QualityProfile::new(128, 44100, 3, "aac".to_string(), 3600.0).is_err());
162 }
163
164 #[test]
165 fn test_is_better_than() {
166 let high = QualityProfile::new(256, 44100, 2, "aac".to_string(), 3600.0).unwrap();
167 let low = QualityProfile::new(128, 44100, 2, "aac".to_string(), 3600.0).unwrap();
168
169 assert!(high.is_better_than(&low, true));
170 assert!(!low.is_better_than(&high, true));
171 }
172
173 #[test]
174 fn test_compatibility() {
175 let profile1 = QualityProfile::new(128, 44100, 2, "aac".to_string(), 3600.0).unwrap();
176 let profile2 = QualityProfile::new(128, 44100, 2, "aac".to_string(), 1800.0).unwrap();
177 let profile3 = QualityProfile::new(256, 44100, 2, "aac".to_string(), 3600.0).unwrap();
178
179 assert!(profile1.is_compatible_for_concat(&profile2));
180 assert!(!profile1.is_compatible_for_concat(&profile3));
181 }
182}