1use async_trait::async_trait;
12use itertools::Itertools;
13use serde::{Deserialize, Serialize};
14use std::{fmt::Debug, sync::Arc};
15
16use crate::handler::GetFileVariantSelector;
17use crate::prelude::*;
18use crate::variant::{Variant, VariantClass};
19use cloudillo_core::scheduler::{Task, TaskId};
20use cloudillo_types::hasher::Hasher;
21use cloudillo_types::meta_adapter;
22use cloudillo_types::types::TnId;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum DescriptorVersion {
27 V1,
29 V2,
31}
32
33impl DescriptorVersion {
34 pub fn prefix(&self) -> &'static str {
36 match self {
37 Self::V1 => "d1~",
38 Self::V2 => "d2,",
39 }
40 }
41
42 pub fn variant_separator(&self) -> char {
44 match self {
45 Self::V1 => ',',
46 Self::V2 => ';',
47 }
48 }
49
50 pub fn id_separator(&self) -> char {
52 match self {
53 Self::V1 => '~',
54 Self::V2 => ',',
55 }
56 }
57}
58
59pub fn get_file_descriptor<S: AsRef<str> + Debug + Eq>(
61 variants: &[meta_adapter::FileVariant<S>],
62) -> String {
63 get_file_descriptor_versioned(variants, DescriptorVersion::V2)
64}
65
66pub fn get_file_descriptor_versioned<S: AsRef<str> + Debug + Eq>(
68 variants: &[meta_adapter::FileVariant<S>],
69 version: DescriptorVersion,
70) -> String {
71 let sep = version.variant_separator();
72 let id_sep = version.id_separator();
73
74 let _ = id_sep; version.prefix().to_owned()
79 + &variants
80 .iter()
81 .map(|v| {
82 let mut parts = format!(
83 "{}:{}:f={}:s={}:r={}x{}",
84 v.variant.as_ref(),
85 v.variant_id.as_ref(),
86 v.format.as_ref(),
87 v.size,
88 v.resolution.0,
89 v.resolution.1
90 );
91
92 if let Some(dur) = v.duration.filter(|&d| d != 0.0) {
94 parts.push_str(&format!(":dur={}", dur));
95 }
96 if let Some(br) = v.bitrate.filter(|&b| b != 0) {
97 parts.push_str(&format!(":br={}", br));
98 }
99 if let Some(pg) = v.page_count.filter(|&p| p != 0) {
100 parts.push_str(&format!(":pg={}", pg));
101 }
102
103 parts
104 })
105 .join(&sep.to_string())
106}
107
108fn parse_variant_entry(
110 entry: &str,
111 _id_separator: char,
112) -> ClResult<meta_adapter::FileVariant<&str>> {
113 let v_vec: Vec<&str> = entry.split(':').collect();
114 if v_vec.len() < 2 {
115 return Err(Error::Parse);
116 }
117
118 let variant = v_vec[0];
119 let variant_id = v_vec[1];
120
121 let mut resolution: Option<(u32, u32)> = None;
122 let mut format: Option<&str> = Some("avif");
123 let mut size: Option<u64> = None;
124 let mut duration: Option<f64> = None;
125 let mut bitrate: Option<u32> = None;
126 let mut page_count: Option<u32> = None;
127
128 for prop in v_vec[2..].iter() {
129 if let Some(val) = prop.strip_prefix("f=") {
130 format = Some(val);
131 } else if let Some(val) = prop.strip_prefix("s=") {
132 size = Some(val.parse().map_err(|_| Error::Parse)?);
133 } else if let Some(val) = prop.strip_prefix("r=") {
134 let res_str: (&str, &str) = val.split('x').collect_tuple().ok_or(Error::Parse)?;
135 resolution = Some((res_str.0.parse()?, res_str.1.parse()?));
136 } else if let Some(val) = prop.strip_prefix("dur=") {
137 duration = Some(val.parse().map_err(|_| Error::Parse)?);
138 } else if let Some(val) = prop.strip_prefix("br=") {
139 bitrate = Some(val.parse().map_err(|_| Error::Parse)?);
140 } else if let Some(val) = prop.strip_prefix("pg=") {
141 page_count = Some(val.parse().map_err(|_| Error::Parse)?);
142 }
143 }
145
146 if let (Some(resolution), Some(format), Some(size)) = (resolution, format, size) {
147 Ok(meta_adapter::FileVariant {
148 variant,
149 variant_id,
150 resolution,
151 format,
152 size,
153 available: false,
154 duration,
155 bitrate,
156 page_count,
157 })
158 } else {
159 error!(
160 "Invalid variant entry - resolution: {:?}, format: {:?}, size: {:?}",
161 resolution, format, size
162 );
163 Err(Error::Parse)
164 }
165}
166
167pub fn parse_file_descriptor(descriptor: &str) -> ClResult<Vec<meta_adapter::FileVariant<&str>>> {
169 if let Some(body) = descriptor.strip_prefix("d2,") {
170 body.split(';')
172 .filter(|s| !s.is_empty())
173 .map(|entry| parse_variant_entry(entry, ','))
174 .collect()
175 } else if let Some(body) = descriptor.strip_prefix("d1~") {
176 body.split(',')
178 .filter(|s| !s.is_empty())
179 .map(|entry| parse_variant_entry(entry, '~'))
180 .collect()
181 } else {
182 Err(Error::Parse)
183 }
184}
185
186fn normalize_variant_name(name: &str) -> &str {
189 if let Some((_class, quality)) = name.split_once('.') {
191 quality
192 } else {
193 name
194 }
195}
196
197fn variant_matches(variant: &str, requested: &str) -> bool {
199 if variant == requested {
201 return true;
202 }
203
204 if let Some(parsed) = Variant::parse(variant) {
206 if parsed.quality.as_str() == requested {
207 return true;
208 }
209 }
210
211 if normalize_variant_name(variant) == requested {
213 return true;
214 }
215
216 false
217}
218
219fn find_variant<'a, S: AsRef<str> + Debug>(
221 variants: &[&'a meta_adapter::FileVariant<S>],
222 name: &str,
223) -> Option<&'a meta_adapter::FileVariant<S>> {
224 variants.iter().find(|v| variant_matches(v.variant.as_ref(), name)).copied()
225}
226
227pub fn get_best_file_variant<'a, S: AsRef<str> + Debug + Eq>(
229 variants: &'a [meta_adapter::FileVariant<S>],
230 selector: &'_ GetFileVariantSelector,
231) -> ClResult<&'a meta_adapter::FileVariant<S>> {
232 debug!("get_best_file_variant: {:?}", selector);
233
234 let (requested_class, requested_quality) = if let Some(ref variant_str) = selector.variant {
236 if let Some(parsed) = Variant::parse(variant_str) {
237 (Some(parsed.class), parsed.quality.as_str())
238 } else {
239 (None, variant_str.as_str())
240 }
241 } else {
242 (None, "tn") };
244
245 let class_filtered: Vec<_> = if let Some(class) = requested_class {
247 variants
248 .iter()
249 .filter(|v| {
250 if let Some(parsed) = Variant::parse(v.variant.as_ref()) {
251 parsed.class == class
252 } else {
253 class == VariantClass::Visual
255 }
256 })
257 .collect()
258 } else {
259 variants.iter().collect()
260 };
261
262 let best = match requested_quality {
263 "tn" => find_variant(&class_filtered, "tn")
264 .or_else(|| find_variant(&class_filtered, "pf"))
265 .ok_or(Error::NotFound),
266 "sd" => find_variant(&class_filtered, "sd")
267 .or_else(|| find_variant(&class_filtered, "md"))
268 .or_else(|| find_variant(&class_filtered, "tn"))
269 .or_else(|| find_variant(&class_filtered, "pf"))
270 .ok_or(Error::NotFound),
271 "md" => find_variant(&class_filtered, "md")
272 .or_else(|| find_variant(&class_filtered, "sd"))
273 .or_else(|| find_variant(&class_filtered, "tn"))
274 .ok_or(Error::NotFound),
275 "hd" => find_variant(&class_filtered, "hd")
276 .or_else(|| find_variant(&class_filtered, "md"))
277 .or_else(|| find_variant(&class_filtered, "sd"))
278 .or_else(|| find_variant(&class_filtered, "tn"))
279 .ok_or(Error::NotFound),
280 "xd" => find_variant(&class_filtered, "xd")
281 .or_else(|| find_variant(&class_filtered, "hd"))
282 .or_else(|| find_variant(&class_filtered, "md"))
283 .or_else(|| find_variant(&class_filtered, "sd"))
284 .or_else(|| find_variant(&class_filtered, "tn"))
285 .ok_or(Error::NotFound),
286 "pf" => find_variant(&class_filtered, "pf")
287 .or_else(|| find_variant(&class_filtered, "tn"))
288 .ok_or(Error::NotFound),
289 "orig" => find_variant(&class_filtered, "orig")
290 .or_else(|| find_variant(&class_filtered, "xd"))
291 .or_else(|| find_variant(&class_filtered, "hd"))
292 .ok_or(Error::NotFound),
293 _ => Err(Error::NotFound),
294 };
295
296 debug!("best variant: {:?}", best);
297 best
298}
299
300#[derive(Debug, Serialize, Deserialize)]
302pub struct FileIdGeneratorTask {
303 tn_id: TnId,
304 f_id: u64,
305}
306
307impl FileIdGeneratorTask {
308 pub fn new(tn_id: TnId, f_id: u64) -> Arc<Self> {
309 Arc::new(Self { tn_id, f_id })
310 }
311}
312
313#[async_trait]
314impl Task<App> for FileIdGeneratorTask {
315 fn kind() -> &'static str {
316 "file.id-generate"
317 }
318 fn kind_of(&self) -> &'static str {
319 Self::kind()
320 }
321
322 fn build(_id: TaskId, ctx: &str) -> ClResult<Arc<dyn Task<App>>> {
323 let (tn_id, f_id) = ctx
324 .split(',')
325 .collect_tuple()
326 .ok_or(Error::Internal("invalid FileIdGenerator context format".into()))?;
327 let task = FileIdGeneratorTask::new(TnId(tn_id.parse()?), f_id.parse()?);
328 Ok(task)
329 }
330
331 fn serialize(&self) -> String {
332 format!("{},{}", self.tn_id, self.f_id)
333 }
334
335 async fn run(&self, app: &App) -> ClResult<()> {
336 info!("Running task file.id-generate {}", self.f_id);
337 let mut variants = app
338 .meta_adapter
339 .list_file_variants(self.tn_id, meta_adapter::FileId::FId(self.f_id))
340 .await?;
341 variants.sort();
342 let descriptor = get_file_descriptor(&variants);
343
344 let mut hasher = Hasher::new();
345 hasher.update(descriptor.as_bytes());
346 let file_id = hasher.finalize("f");
347
348 app.meta_adapter.finalize_file(self.tn_id, self.f_id, &file_id).await?;
350
351 let msg = cloudillo_core::ws_broadcast::BroadcastMessage::new(
354 "FILE_ID_GENERATED",
355 serde_json::json!({
356 "tempId": format!("@{}", self.f_id),
357 "fileId": file_id
358 }),
359 "system",
360 );
361 let delivered = app.broadcast.send_to_tenant(self.tn_id, msg).await;
362 debug!("FILE_ID_GENERATED broadcast delivered to {} connections", delivered);
363
364 info!("Finished task file.id-generate {} → {}", descriptor, file_id);
365 Ok(())
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn test_parse_d1_descriptor() {
375 let desc = "d1~tn:b1~abc123:f=webp:s=2048:r=128x128,sd:b1~def456:f=webp:s=10240:r=720x720";
376 let variants = parse_file_descriptor(desc).unwrap();
377
378 assert_eq!(variants.len(), 2);
379 assert_eq!(variants[0].variant, "tn");
380 assert_eq!(variants[0].variant_id, "b1~abc123");
381 assert_eq!(variants[0].format, "webp");
382 assert_eq!(variants[0].size, 2048);
383 assert_eq!(variants[0].resolution, (128, 128));
384
385 assert_eq!(variants[1].variant, "sd");
386 assert_eq!(variants[1].variant_id, "b1~def456");
387 }
388
389 #[test]
390 fn test_parse_d2_descriptor() {
391 let desc = "d2,vis.tn:b1~abc123:f=webp:s=2048:r=128x128;vis.sd:b1~def456:f=webp:s=10240:r=720x720:dur=120.5:br=5000";
393 let variants = parse_file_descriptor(desc).unwrap();
394
395 assert_eq!(variants.len(), 2);
396 assert_eq!(variants[0].variant, "vis.tn");
397 assert_eq!(variants[0].variant_id, "b1~abc123");
398 assert_eq!(variants[0].format, "webp");
399 assert_eq!(variants[0].size, 2048);
400 assert_eq!(variants[0].resolution, (128, 128));
401 assert_eq!(variants[0].duration, None);
402
403 assert_eq!(variants[1].variant, "vis.sd");
404 assert_eq!(variants[1].variant_id, "b1~def456");
405 assert_eq!(variants[1].duration, Some(120.5));
406 assert_eq!(variants[1].bitrate, Some(5000));
407 }
408
409 #[test]
410 fn test_generate_d2_descriptor() {
411 let variants = vec![
412 meta_adapter::FileVariant {
413 variant: "vis.tn",
414 variant_id: "b1~abc123",
415 format: "webp",
416 size: 2048,
417 resolution: (128, 128),
418 available: true,
419 duration: None,
420 bitrate: None,
421 page_count: None,
422 },
423 meta_adapter::FileVariant {
424 variant: "vid.hd",
425 variant_id: "b1~def456",
426 format: "mp4",
427 size: 51200,
428 resolution: (1920, 1080),
429 available: true,
430 duration: Some(120.5),
431 bitrate: Some(5000),
432 page_count: None,
433 },
434 ];
435
436 let desc = get_file_descriptor(&variants);
437 assert!(desc.starts_with("d2,"));
438 assert!(desc.contains("vis.tn:b1~abc123"));
440 assert!(desc.contains("vid.hd:b1~def456"));
441 assert!(desc.contains(":dur=120.5"));
442 assert!(desc.contains(":br=5000"));
443 assert!(desc.contains(";vid.hd"));
445 }
446
447 #[test]
448 fn test_variant_matches() {
449 assert!(variant_matches("sd", "sd"));
451 assert!(variant_matches("vis.sd", "vis.sd"));
452
453 assert!(variant_matches("vis.sd", "sd"));
455 assert!(variant_matches("vid.hd", "hd"));
456
457 assert!(!variant_matches("vis.sd", "hd"));
459 assert!(!variant_matches("sd", "hd"));
460 }
461}
462
463