Skip to main content

cloudillo_file/
preset.rs

1//! File processing presets for different use cases.
2//!
3//! Presets define which variants to generate for different media types
4//! and use cases (e.g., "default", "podcast", "archive").
5
6use serde::{Deserialize, Serialize};
7
8use crate::image::ImageFormat;
9use crate::variant::VariantClass;
10
11/// File processing preset configuration
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct FilePreset {
14	/// Preset name (e.g., "default", "podcast", "archive")
15	pub name: String,
16	/// Allowed media classes for upload (e.g., [Visual, Video, Audio])
17	pub allowed_media_classes: Vec<VariantClass>,
18	/// Image/visual variants to generate (e.g., ["vis.tn", "vis.sd", "vis.md", "vis.hd"])
19	pub image_variants: Vec<String>,
20	/// Video variants to generate (e.g., ["vid.sd", "vid.md", "vid.hd"])
21	pub video_variants: Vec<String>,
22	/// Audio variants to generate (e.g., ["aud.md"])
23	pub audio_variants: Vec<String>,
24	/// Extract audio track from video files
25	pub extract_audio: bool,
26	/// Generate thumbnail for video/audio/document files
27	pub generate_thumbnail: bool,
28	/// Maximum variant to generate (caps generation at this level)
29	pub max_variant: Option<String>,
30	/// Variant to create synchronously for immediate thumbnail response (e.g., "vis.pf" or "vis.tn")
31	pub thumbnail_variant: Option<String>,
32	/// Store original blob (file_variant record is always created, but blob storage is optional)
33	pub store_original: bool,
34}
35
36impl Default for FilePreset {
37	fn default() -> Self {
38		Self {
39			name: "default".to_string(),
40			allowed_media_classes: vec![
41				VariantClass::Visual,
42				VariantClass::Video,
43				VariantClass::Audio,
44				VariantClass::Document,
45			],
46			image_variants: vec![
47				"vis.tn".into(),
48				"vis.sd".into(),
49				"vis.md".into(),
50				"vis.hd".into(),
51			],
52			video_variants: vec!["vid.sd".into(), "vid.md".into(), "vid.hd".into()],
53			audio_variants: vec!["aud.md".into()],
54			extract_audio: false,
55			generate_thumbnail: true,
56			max_variant: Some("vid.hd".into()),
57			thumbnail_variant: Some("vis.tn".into()),
58			store_original: true,
59		}
60	}
61}
62
63/// Built-in presets
64pub mod presets {
65	use super::{FilePreset, VariantClass};
66
67	/// Default preset - balanced quality and storage
68	pub fn default() -> FilePreset {
69		FilePreset::default()
70	}
71
72	/// Podcast preset - prioritizes audio extraction and quality
73	pub fn podcast() -> FilePreset {
74		FilePreset {
75			name: "podcast".to_string(),
76			allowed_media_classes: vec![VariantClass::Audio, VariantClass::Video],
77			image_variants: vec!["vis.tn".into()], // Just thumbnail for audio-focused
78			video_variants: vec!["vid.sd".into()],
79			audio_variants: vec!["aud.sd".into(), "aud.md".into(), "aud.hd".into()],
80			extract_audio: true,
81			generate_thumbnail: true,
82			max_variant: Some("vid.sd".into()),
83			thumbnail_variant: Some("vis.tn".into()),
84			store_original: true,
85		}
86	}
87
88	/// Archive preset - keep original only, minimal processing (allows all types including raw)
89	pub fn archive() -> FilePreset {
90		FilePreset {
91			name: "archive".to_string(),
92			allowed_media_classes: vec![
93				VariantClass::Visual,
94				VariantClass::Video,
95				VariantClass::Audio,
96				VariantClass::Document,
97				VariantClass::Raw,
98			],
99			image_variants: vec!["vis.tn".into()], // Minimal processing
100			video_variants: vec![],
101			audio_variants: vec![],
102			extract_audio: false,
103			generate_thumbnail: true,
104			max_variant: None, // Keep original only
105			thumbnail_variant: Some("vis.tn".into()),
106			store_original: true, // Archive always stores original
107		}
108	}
109
110	/// High quality preset - maximum quality variants
111	pub fn high_quality() -> FilePreset {
112		FilePreset {
113			name: "high_quality".to_string(),
114			allowed_media_classes: vec![
115				VariantClass::Visual,
116				VariantClass::Video,
117				VariantClass::Audio,
118			],
119			image_variants: vec![
120				"vis.tn".into(),
121				"vis.sd".into(),
122				"vis.md".into(),
123				"vis.hd".into(),
124				"vis.xd".into(),
125			],
126			video_variants: vec![
127				"vid.sd".into(),
128				"vid.md".into(),
129				"vid.hd".into(),
130				"vid.xd".into(),
131			],
132			audio_variants: vec!["aud.md".into(), "aud.hd".into()],
133			extract_audio: false,
134			generate_thumbnail: true,
135			max_variant: Some("vid.xd".into()),
136			thumbnail_variant: Some("vis.tn".into()),
137			store_original: true,
138		}
139	}
140
141	/// Mobile preset - optimized for mobile devices and bandwidth
142	pub fn mobile() -> FilePreset {
143		FilePreset {
144			name: "mobile".to_string(),
145			allowed_media_classes: vec![
146				VariantClass::Visual,
147				VariantClass::Video,
148				VariantClass::Audio,
149			],
150			image_variants: vec!["vis.tn".into(), "vis.sd".into(), "vis.md".into()],
151			video_variants: vec!["vid.sd".into(), "vid.md".into()],
152			audio_variants: vec!["aud.sd".into()],
153			extract_audio: false,
154			generate_thumbnail: true,
155			max_variant: Some("vid.md".into()),
156			thumbnail_variant: Some("vis.tn".into()),
157			store_original: false, // Mobile optimization - minimize storage
158		}
159	}
160
161	/// Video preset - video-focused, no separate audio track extraction
162	pub fn video() -> FilePreset {
163		FilePreset {
164			name: "video".to_string(),
165			allowed_media_classes: vec![VariantClass::Video],
166			image_variants: vec![
167				"vis.tn".into(),
168				"vis.sd".into(),
169				"vis.md".into(),
170				"vis.hd".into(),
171			],
172			video_variants: vec!["vid.sd".into(), "vid.md".into(), "vid.hd".into()],
173			audio_variants: vec![], // No separate audio variants
174			extract_audio: false,   // Don't extract audio track
175			generate_thumbnail: true,
176			max_variant: Some("vid.hd".into()),
177			thumbnail_variant: Some("vis.tn".into()),
178			store_original: true,
179		}
180	}
181
182	/// Profile picture preset - includes vis.pf variant for small profile thumbnails
183	pub fn profile_picture() -> FilePreset {
184		FilePreset {
185			name: "profile-picture".to_string(),
186			allowed_media_classes: vec![VariantClass::Visual],
187			image_variants: vec![
188				"vis.pf".into(),
189				"vis.tn".into(),
190				"vis.sd".into(),
191				"vis.md".into(),
192				"vis.hd".into(),
193			],
194			video_variants: vec![],
195			audio_variants: vec![],
196			extract_audio: false,
197			generate_thumbnail: false, // vis.pf serves as thumbnail
198			max_variant: Some("vis.hd".into()),
199			thumbnail_variant: Some("vis.pf".into()),
200			store_original: false, // Generated variants are sufficient
201		}
202	}
203
204	/// Cover image preset - standard image variants without vis.pf
205	pub fn cover() -> FilePreset {
206		FilePreset {
207			name: "cover".to_string(),
208			allowed_media_classes: vec![VariantClass::Visual],
209			image_variants: vec![
210				"vis.tn".into(),
211				"vis.sd".into(),
212				"vis.md".into(),
213				"vis.hd".into(),
214			],
215			video_variants: vec![],
216			audio_variants: vec![],
217			extract_audio: false,
218			generate_thumbnail: false, // vis.tn serves as thumbnail
219			max_variant: Some("vis.hd".into()),
220			thumbnail_variant: Some("vis.tn".into()),
221			store_original: false, // Generated variants are sufficient
222		}
223	}
224
225	/// Get preset by name
226	pub fn get(name: &str) -> Option<FilePreset> {
227		match name {
228			"default" => Some(default()),
229			"podcast" => Some(podcast()),
230			"archive" => Some(archive()),
231			"high_quality" => Some(high_quality()),
232			"mobile" => Some(mobile()),
233			"video" => Some(video()),
234			"profile-picture" => Some(profile_picture()),
235			"cover" => Some(cover()),
236			_ => None,
237		}
238	}
239
240	/// List all available preset names
241	pub fn list() -> Vec<&'static str> {
242		vec![
243			"default",
244			"podcast",
245			"archive",
246			"high_quality",
247			"mobile",
248			"video",
249			"profile-picture",
250			"cover",
251		]
252	}
253}
254
255/// Video quality tier with associated settings
256#[derive(Debug, Clone, Copy)]
257pub struct VideoQualityTier {
258	pub name: &'static str,
259	pub max_dim: u32,
260	pub bitrate: u32,
261}
262
263/// Audio quality tier with associated settings
264#[derive(Debug, Clone, Copy)]
265pub struct AudioQualityTier {
266	pub name: &'static str,
267	pub bitrate: u32,
268}
269
270/// Image quality tier with associated settings
271#[derive(Debug, Clone, Copy)]
272pub struct ImageQualityTier {
273	pub name: &'static str,
274	pub max_dim: u32,
275	/// Optional format override (None = use setting, Some = override)
276	pub format: Option<ImageFormat>,
277}
278
279/// Video quality tiers
280pub const VIDEO_TIERS: &[VideoQualityTier] = &[
281	VideoQualityTier { name: "vid.sd", max_dim: 720, bitrate: 1500 },
282	VideoQualityTier { name: "vid.md", max_dim: 1280, bitrate: 3000 },
283	VideoQualityTier { name: "vid.hd", max_dim: 1920, bitrate: 5000 },
284	VideoQualityTier { name: "vid.xd", max_dim: 3840, bitrate: 15000 },
285];
286
287/// Audio quality tiers
288pub const AUDIO_TIERS: &[AudioQualityTier] = &[
289	AudioQualityTier { name: "aud.sd", bitrate: 64 },
290	AudioQualityTier { name: "aud.md", bitrate: 128 },
291	AudioQualityTier { name: "aud.hd", bitrate: 256 },
292];
293
294/// Image quality tiers
295pub const IMAGE_TIERS: &[ImageQualityTier] = &[
296	ImageQualityTier { name: "vis.pf", max_dim: 80, format: Some(ImageFormat::Avif) },
297	ImageQualityTier { name: "vis.tn", max_dim: 256, format: None },
298	ImageQualityTier { name: "vis.sd", max_dim: 720, format: None },
299	ImageQualityTier { name: "vis.md", max_dim: 1280, format: None },
300	ImageQualityTier { name: "vis.hd", max_dim: 1920, format: None },
301	ImageQualityTier { name: "vis.xd", max_dim: 3840, format: None },
302];
303
304/// Get video tier by variant name
305pub fn get_video_tier(variant: &str) -> Option<&'static VideoQualityTier> {
306	VIDEO_TIERS.iter().find(|t| t.name == variant)
307}
308
309/// Get audio tier by variant name
310pub fn get_audio_tier(variant: &str) -> Option<&'static AudioQualityTier> {
311	AUDIO_TIERS.iter().find(|t| t.name == variant)
312}
313
314/// Get image tier by variant name
315pub fn get_image_tier(variant: &str) -> Option<&'static ImageQualityTier> {
316	IMAGE_TIERS.iter().find(|t| t.name == variant)
317}
318
319#[cfg(test)]
320mod tests {
321	use super::*;
322
323	#[test]
324	fn test_default_preset() {
325		let preset = presets::default();
326		assert_eq!(preset.name, "default");
327		assert!(preset.video_variants.contains(&"vid.hd".to_string()));
328		assert!(preset.generate_thumbnail);
329	}
330
331	#[test]
332	fn test_podcast_preset() {
333		let preset = presets::podcast();
334		assert_eq!(preset.name, "podcast");
335		assert!(preset.extract_audio);
336		assert!(preset.audio_variants.contains(&"aud.hd".to_string()));
337	}
338
339	#[test]
340	fn test_get_preset() {
341		assert!(presets::get("default").is_some());
342		assert!(presets::get("podcast").is_some());
343		assert!(presets::get("nonexistent").is_none());
344	}
345
346	#[test]
347	fn test_video_tiers() {
348		let tier = get_video_tier("vid.hd");
349		assert!(tier.is_some());
350		let tier = tier.unwrap();
351		assert_eq!(tier.max_dim, 1920);
352		assert_eq!(tier.bitrate, 5000);
353	}
354
355	#[test]
356	fn test_audio_tiers() {
357		let tier = get_audio_tier("aud.md");
358		assert!(tier.is_some());
359		let tier = tier.unwrap();
360		assert_eq!(tier.bitrate, 128);
361	}
362
363	#[test]
364	fn test_image_tiers() {
365		let tier = get_image_tier("vis.hd");
366		assert!(tier.is_some());
367		let tier = tier.unwrap();
368		assert_eq!(tier.max_dim, 1920);
369	}
370}
371
372// vim: ts=4