1use std::path::PathBuf;
2
3use crate::color::{ColorSpace, TransferFunction};
4use crate::export::{
5 Av1Profile, CodecFamily, DnxhrProfile, H264Profile, HevcProfile, ProResProfile,
6 RateControl, Vp9Profile,
7};
8
9#[derive(Debug, Clone)]
15pub struct ExportPreset {
16 pub name: String,
17
18 pub color_space: ColorSpace,
19 pub transfer_function: TransferFunction,
20 pub codec_family: CodecFamily,
21
22 pub prores_profile: ProResProfile,
23 pub dnxhr_profile: DnxhrProfile,
24 pub hevc_profile: HevcProfile,
25 pub h264_profile: H264Profile,
26 pub av1_profile: Av1Profile,
27 pub vp9_profile: Vp9Profile,
28
29 pub rate_control: RateControl,
30 pub export_folder: Option<PathBuf>,
33}
34
35#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
38struct PresetFile {
39 #[serde(default)]
40 schema_version: u32,
41 #[serde(default)]
42 presets: Vec<StoredPreset>,
43}
44
45#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
50struct StoredPreset {
51 name: String,
52
53 color_space: String,
54 transfer_function: String,
55 codec_family: String,
56
57 #[serde(default)]
58 prores_profile: Option<String>,
59 #[serde(default)]
60 dnxhr_profile: Option<String>,
61 #[serde(default)]
62 hevc_profile: Option<String>,
63 #[serde(default)]
64 h264_profile: Option<String>,
65 #[serde(default)]
66 av1_profile: Option<String>,
67 #[serde(default)]
68 vp9_profile: Option<String>,
69
70 rate_control: String,
73 #[serde(default)]
74 export_folder: Option<PathBuf>,
75}
76
77const PRESETS_SCHEMA_VERSION: u32 = 1;
78
79impl ExportPreset {
80 pub fn snapshot(
82 name: String,
83 color_space: ColorSpace,
84 transfer_function: TransferFunction,
85 codec_family: CodecFamily,
86 prores_profile: ProResProfile,
87 dnxhr_profile: DnxhrProfile,
88 hevc_profile: HevcProfile,
89 h264_profile: H264Profile,
90 av1_profile: Av1Profile,
91 vp9_profile: Vp9Profile,
92 rate_control: RateControl,
93 export_folder: Option<PathBuf>,
94 ) -> Self {
95 Self {
96 name,
97 color_space,
98 transfer_function,
99 codec_family,
100 prores_profile,
101 dnxhr_profile,
102 hevc_profile,
103 h264_profile,
104 av1_profile,
105 vp9_profile,
106 rate_control,
107 export_folder,
108 }
109 }
110
111 pub fn presets_file() -> Option<PathBuf> {
113 let mut dir = dirs::config_dir()?;
114 dir.push("mcraw-tui");
115 std::fs::create_dir_all(&dir).ok()?;
116 dir.push("presets.json");
117 Some(dir)
118 }
119
120 pub fn load_all() -> Vec<ExportPreset> {
122 let path = match Self::presets_file() {
123 Some(p) => p,
124 None => return Vec::new(),
125 };
126 let data = match std::fs::read_to_string(&path) {
127 Ok(d) => d,
128 Err(_) => return Vec::new(),
129 };
130 let parsed: PresetFile = match serde_json::from_str(&data) {
131 Ok(p) => p,
132 Err(e) => {
133 tracing::warn!("presets.json parse failed, ignoring: {}", e);
134 return Vec::new();
135 }
136 };
137 parsed.presets.into_iter().filter_map(Self::from_stored).collect()
138 }
139
140 pub fn save_all(presets: &[ExportPreset]) {
142 let path = match Self::presets_file() {
143 Some(p) => p,
144 None => return,
145 };
146 let stored: Vec<StoredPreset> = presets.iter().map(Self::to_stored).collect();
147 let file = PresetFile { schema_version: PRESETS_SCHEMA_VERSION, presets: stored };
148 match serde_json::to_string_pretty(&file) {
149 Ok(data) => {
150 if let Err(e) = std::fs::write(&path, data) {
151 tracing::warn!("failed to write presets.json: {}", e);
152 }
153 }
154 Err(e) => tracing::warn!("failed to serialize presets: {}", e),
155 }
156 }
157
158 pub fn upsert(list: &mut Vec<ExportPreset>, preset: ExportPreset) {
161 if let Some(pos) = list.iter().position(|p| p.name == preset.name) {
162 list[pos] = preset;
163 } else {
164 list.push(preset);
165 }
166 }
167
168 pub fn remove_by_name(list: &mut Vec<ExportPreset>, name: &str) -> bool {
170 let before = list.len();
171 list.retain(|p| p.name != name);
172 list.len() != before
173 }
174
175 fn to_stored(p: &ExportPreset) -> StoredPreset {
176 StoredPreset {
177 name: p.name.clone(),
178 color_space: cs_to_str(p.color_space).to_string(),
179 transfer_function: tf_to_str(p.transfer_function).to_string(),
180 codec_family: p.codec_family.name().to_string(),
181 prores_profile: Some(p.prores_profile.name().to_string()),
182 dnxhr_profile: Some(p.dnxhr_profile.name().to_string()),
183 hevc_profile: Some(p.hevc_profile.name().to_string()),
184 h264_profile: Some(p.h264_profile.name().to_string()),
185 av1_profile: Some(p.av1_profile.name().to_string()),
186 vp9_profile: Some(p.vp9_profile.name().to_string()),
187 rate_control: rate_to_str(&p.rate_control),
188 export_folder: p.export_folder.clone(),
189 }
190 }
191
192 fn from_stored(s: StoredPreset) -> Option<ExportPreset> {
193 let color_space = cs_from_str(&s.color_space)?;
194 let transfer_function = tf_from_str(&s.transfer_function)?;
195 let codec_family = codec_from_str(&s.codec_family)?;
196 let prores_profile = s.prores_profile.as_deref()
197 .and_then(prores_from_str).unwrap_or_default();
198 let dnxhr_profile = s.dnxhr_profile.as_deref()
199 .and_then(dnxhr_from_str).unwrap_or_default();
200 let hevc_profile = s.hevc_profile.as_deref()
201 .and_then(hevc_from_str).unwrap_or_default();
202 let h264_profile = s.h264_profile.as_deref()
203 .and_then(h264_from_str).unwrap_or_default();
204 let av1_profile = s.av1_profile.as_deref()
205 .and_then(av1_from_str).unwrap_or_default();
206 let vp9_profile = s.vp9_profile.as_deref()
207 .and_then(vp9_from_str).unwrap_or_default();
208 let rate_control = rate_from_str(&s.rate_control);
209
210 Some(ExportPreset {
211 name: s.name,
212 color_space,
213 transfer_function,
214 codec_family,
215 prores_profile,
216 dnxhr_profile,
217 hevc_profile,
218 h264_profile,
219 av1_profile,
220 vp9_profile,
221 rate_control,
222 export_folder: s.export_folder,
223 })
224 }
225}
226
227fn cs_to_str(cs: ColorSpace) -> &'static str {
235 cs.name()
237}
238
239fn cs_from_str(s: &str) -> Option<ColorSpace> {
240 ColorSpace::all().iter().copied().find(|c| c.name() == s)
241}
242
243fn tf_to_str(tf: TransferFunction) -> &'static str {
244 tf.name()
245}
246
247fn tf_from_str(s: &str) -> Option<TransferFunction> {
248 TransferFunction::all().iter().copied().find(|t| t.name() == s)
249}
250
251fn codec_from_str(s: &str) -> Option<CodecFamily> {
252 CodecFamily::all().iter().copied().find(|c| c.name() == s)
253}
254
255fn prores_from_str(s: &str) -> Option<ProResProfile> {
256 ProResProfile::all().iter().copied().find(|p| p.name() == s)
257}
258
259fn dnxhr_from_str(s: &str) -> Option<DnxhrProfile> {
260 DnxhrProfile::all().iter().copied().find(|p| p.name() == s)
261}
262
263fn hevc_from_str(s: &str) -> Option<HevcProfile> {
264 HevcProfile::all().iter().copied().find(|p| p.name() == s)
265}
266
267fn h264_from_str(s: &str) -> Option<H264Profile> {
268 H264Profile::all().iter().copied().find(|p| p.name() == s)
269}
270
271fn av1_from_str(s: &str) -> Option<Av1Profile> {
272 Av1Profile::all().iter().copied().find(|p| p.name() == s)
273}
274
275fn vp9_from_str(s: &str) -> Option<Vp9Profile> {
276 Vp9Profile::all().iter().copied().find(|p| p.name() == s)
277}
278
279fn rate_to_str(r: &RateControl) -> String {
280 r.name()
281}
282
283fn rate_from_str(s: &str) -> RateControl {
284 if let Some(inner) = s.strip_prefix("Custom: [").and_then(|x| x.strip_suffix(']')) {
289 return RateControl::Custom(inner.to_string());
290 }
291 match s {
292 "Lossless" => RateControl::Lossless,
293 "High Quality" => RateControl::High,
294 "Standard" => RateControl::Standard,
295 "Master 400M" => RateControl::Master400M,
296 "Standard 150M" => RateControl::Standard150M,
297 _ => RateControl::Lossless, }
299}
300
301impl Default for ProResProfile { fn default() -> Self { ProResProfile::HQ } }
310impl Default for DnxhrProfile { fn default() -> Self { DnxhrProfile::HQX } }
311impl Default for HevcProfile { fn default() -> Self { HevcProfile::Main10_420 } }
312impl Default for H264Profile { fn default() -> Self { H264Profile::Main8bit } }
313impl Default for Av1Profile { fn default() -> Self { Av1Profile::Profile0_420_10bit } }
314impl Default for Vp9Profile { fn default() -> Self { Vp9Profile::Profile2_420_10bit } }
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 fn sample() -> ExportPreset {
321 ExportPreset::snapshot(
322 "ARRIRAW-bake".to_string(),
323 ColorSpace::ARRIWideGamut4,
324 TransferFunction::ARRIlog4,
325 CodecFamily::ProRes,
326 ProResProfile::HQ,
327 DnxhrProfile::HQX,
328 HevcProfile::Main10_420,
329 H264Profile::Main8bit,
330 Av1Profile::Profile0_420_10bit,
331 Vp9Profile::Profile2_420_10bit,
332 RateControl::Lossless,
333 Some(PathBuf::from("/tmp/out")),
334 )
335 }
336
337 #[test]
338 fn roundtrip_through_stored() {
339 let original = sample();
340 let stored = ExportPreset::to_stored(&original);
341 let recovered = ExportPreset::from_stored(stored).expect("from_stored");
342 assert_eq!(recovered.name, original.name);
343 assert_eq!(recovered.color_space, original.color_space);
344 assert_eq!(recovered.transfer_function, original.transfer_function);
345 assert_eq!(recovered.codec_family, original.codec_family);
346 assert_eq!(recovered.prores_profile, original.prores_profile);
347 assert_eq!(recovered.export_folder, original.export_folder);
348 }
349
350 #[test]
351 fn upsert_replaces_same_name() {
352 let mut list = vec![sample()];
353 let mut updated = sample();
354 updated.color_space = ColorSpace::Rec709;
355 ExportPreset::upsert(&mut list, updated);
356 assert_eq!(list.len(), 1);
357 assert_eq!(list[0].color_space, ColorSpace::Rec709);
358 }
359
360 #[test]
361 fn upsert_appends_new_name() {
362 let mut list = vec![sample()];
363 let mut new_one = sample();
364 new_one.name = "ProRes-4444".to_string();
365 ExportPreset::upsert(&mut list, new_one);
366 assert_eq!(list.len(), 2);
367 }
368
369 #[test]
370 fn remove_by_name_drops_matching() {
371 let mut list = vec![sample()];
372 let mut second = sample();
373 second.name = "second".to_string();
374 list.push(second);
375 assert!(ExportPreset::remove_by_name(&mut list, "second"));
376 assert_eq!(list.len(), 1);
377 assert!(!ExportPreset::remove_by_name(&mut list, "missing"));
378 }
379
380 #[test]
381 fn custom_rate_roundtrip() {
382 let mut p = sample();
383 p.rate_control = RateControl::Custom("80M".to_string());
384 let stored = ExportPreset::to_stored(&p);
385 let recovered = ExportPreset::from_stored(stored).expect("from_stored");
386 if let RateControl::Custom(s) = recovered.rate_control {
387 assert_eq!(s, "80M");
388 } else {
389 panic!("expected Custom, got {:?}", recovered.rate_control);
390 }
391 }
392}