1use alux_sdk::IterTraversableExt;
4use derive_more::From;
5use derive_new::new;
6use rsynko_media::*;
7use std::collections::BTreeMap;
8
9#[derive(Clone, Debug, Default, PartialEq, From)]
11pub enum InfoValue {
12 #[default]
14 Null,
15 Bool(bool),
17 Integer(i64),
19 Float(f64),
21 String(String),
23 List(Vec<InfoValue>),
25 Record(InfoRecord),
27}
28
29impl InfoValue {
30 #[must_use]
32 pub fn as_text(&self) -> Option<&str> {
33 match self {
34 Self::String(value) => Some(value),
35 _ => None,
36 }
37 }
38
39 #[must_use]
41 pub fn as_bool(&self) -> Option<bool> {
42 match self {
43 Self::Bool(value) => Some(*value),
44 _ => None,
45 }
46 }
47
48 #[must_use]
53 #[expect(
54 clippy::cast_precision_loss,
55 reason = "format observations are compared, not accumulated"
56 )]
57 pub fn as_number(&self) -> Option<f64> {
58 match self {
59 Self::Integer(value) => Some(*value as f64),
60 Self::Float(value) => Some(*value),
61 _ => None,
62 }
63 }
64}
65
66#[derive(Clone, Debug, Default, PartialEq)]
71pub struct InfoRecord(BTreeMap<String, InfoValue>);
72
73impl InfoRecord {
74 pub fn insert(
76 &mut self,
77 key: impl Into<String>,
78 value: impl Into<InfoValue>,
79 ) -> Option<InfoValue> {
80 self.0.insert(key.into(), value.into())
81 }
82
83 #[must_use]
85 pub fn get(&self, key: &str) -> Option<&InfoValue> {
86 self.0.get(key)
87 }
88
89 #[must_use]
91 pub fn is_empty(&self) -> bool {
92 self.0.is_empty()
93 }
94
95 #[must_use]
97 pub fn len(&self) -> usize {
98 self.0.len()
99 }
100}
101
102impl From<&str> for InfoValue {
103 fn from(value: &str) -> Self {
104 Self::String(value.to_owned())
105 }
106}
107
108#[derive(Clone, Debug, PartialEq, new)]
115pub struct Format {
116 pub id: String,
118 pub metadata: InfoRecord,
120}
121
122impl Format {
123 #[must_use]
125 pub fn observe(&self, key: &str) -> Option<&InfoValue> {
126 self.metadata.get(key)
127 }
128
129 #[must_use]
131 pub fn text(&self, key: &str) -> Option<&str> {
132 self.observe(key).and_then(InfoValue::as_text)
133 }
134
135 #[must_use]
137 pub fn number(&self, key: &str) -> Option<f64> {
138 self.observe(key).and_then(InfoValue::as_number)
139 }
140
141 #[must_use]
143 pub fn flag(&self, key: &str) -> bool {
144 self.observe(key)
145 .and_then(InfoValue::as_bool)
146 .unwrap_or(false)
147 }
148
149 #[must_use]
151 pub fn extension(&self) -> Option<&str> {
152 self.text(FORMAT_EXTENSION)
153 }
154
155 #[must_use]
157 pub fn has_audio(&self) -> bool {
158 self.flag(FORMAT_HAS_AUDIO)
159 }
160
161 #[must_use]
163 pub fn has_video(&self) -> bool {
164 self.flag(FORMAT_HAS_VIDEO)
165 }
166
167 #[must_use]
169 pub fn source(&self) -> Option<&str> {
170 self.text(FORMAT_SOURCE)
171 }
172}
173
174#[derive(Clone, Debug, PartialEq, new)]
176pub struct Artifact {
177 pub id: String,
179 pub kind: ArtifactKind,
181 pub metadata: InfoRecord,
183}
184
185impl Artifact {
186 #[must_use]
188 pub fn location(&self) -> Option<&str> {
189 self.metadata
190 .get(ARTIFACT_LOCATION)
191 .and_then(InfoValue::as_text)
192 }
193}
194
195#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
197pub struct ExtractorKey(pub String);
198
199impl ExtractorKey {
200 #[must_use]
202 pub fn new(value: impl Into<String>) -> Self {
203 Self(value.into())
204 }
205}
206
207#[derive(Clone, Debug, PartialEq, new)]
209pub struct Media {
210 pub id: String,
212 pub metadata: InfoRecord,
214 pub formats: Vec<Format>,
216}
217
218#[derive(Clone, Debug, PartialEq, Eq, new)]
220pub struct UrlReference {
221 pub url: String,
223 pub extractor: Option<ExtractorKey>,
225 pub transparent: bool,
227}
228
229#[derive(Clone, Debug, PartialEq, new)]
231pub struct Collection {
232 pub id: Option<String>,
234 pub kind: CollectionKind,
236 pub metadata: InfoRecord,
238 pub entries: Vec<Extraction>,
240}
241
242#[derive(Clone, Debug, PartialEq)]
244pub enum Extraction {
245 Media(Media),
247 Url(UrlReference),
249 Collection(Collection),
251}
252
253#[derive(Clone, Debug, PartialEq)]
258pub enum FormatPredicate {
259 Any,
261 Id(String),
263 Observed(String),
265 Text {
267 key: String,
269 value: String,
271 },
272 Flag {
274 key: String,
276 value: bool,
278 },
279 Number {
281 key: String,
283 comparison: FormatComparison,
285 value: f64,
287 },
288 All(Box<FormatPredicate>, Box<FormatPredicate>),
290 Not(Box<FormatPredicate>),
292}
293
294#[derive(Clone, Debug, PartialEq)]
296pub enum FormatSelection {
297 Best(FormatPredicate),
299 Worst(FormatPredicate),
301 Merge(Vec<FormatSelection>),
303 Fallback(Vec<FormatSelection>),
305}
306
307#[must_use]
312pub fn predicate_accepts(predicate: &FormatPredicate, format: &Format) -> bool {
313 match predicate {
314 FormatPredicate::Any => true,
315 FormatPredicate::Id(id) => format.id == *id,
316 FormatPredicate::Observed(key) => format.observe(key).is_some(),
317 FormatPredicate::Text { key, value } => format.text(key) == Some(value.as_str()),
318 FormatPredicate::Flag { key, value } => format.flag(key) == *value,
319 FormatPredicate::Number {
320 key,
321 comparison,
322 value,
323 } => format.number(key).is_some_and(|observed| match comparison {
324 FormatComparison::AtMost => observed <= *value,
325 FormatComparison::Exactly => (observed - *value).abs() < f64::EPSILON,
326 FormatComparison::AtLeast => observed >= *value,
327 }),
328 FormatPredicate::All(left, right) => {
329 predicate_accepts(left, format) && predicate_accepts(right, format)
330 }
331 FormatPredicate::Not(child) => !predicate_accepts(child, format),
332 }
333}
334
335#[must_use]
340pub fn interpret_selection<'a, This>(
341 alg: &This,
342 formats: &'a [Format],
343 selection: &FormatSelection,
344) -> Option<Vec<&'a Format>>
345where
346 This: FormatPredicateMatchAlg<Predicate = FormatPredicate, Format = Format>,
347{
348 match selection {
349 FormatSelection::Best(predicate) => formats
350 .iter()
351 .rev()
352 .find(|format| alg.format_matches(predicate, format))
353 .map(|format| vec![format]),
354 FormatSelection::Worst(predicate) => formats
355 .iter()
356 .find(|format| alg.format_matches(predicate, format))
357 .map(|format| vec![format]),
358 FormatSelection::Merge(selections) => selections
359 .iter()
360 .traverse_iter(|child| interpret_selection(alg, formats, child).ok_or(()))
361 .ok(),
362 FormatSelection::Fallback(selections) => selections
363 .iter()
364 .find_map(|child| interpret_selection(alg, formats, child)),
365 }
366}
367
368#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
370pub struct ProcessorId(pub String);
371
372impl ProcessorId {
373 #[must_use]
375 pub fn new(value: impl Into<String>) -> Self {
376 Self(value.into())
377 }
378}
379
380#[derive(Clone, Debug, PartialEq, Eq)]
382pub struct ProcessingStep {
383 pub stage: ProcessingStage,
385 pub processor: ProcessorId,
387}
388
389impl ProcessingStep {
390 #[must_use]
392 pub fn new(stage: ProcessingStage, processor: impl Into<String>) -> Self {
393 Self {
394 stage,
395 processor: ProcessorId::new(processor),
396 }
397 }
398}
399
400#[derive(Clone, Debug, Default, PartialEq, Eq)]
402pub struct ProcessingProgram(Vec<ProcessingStep>);
403
404impl ProcessingProgram {
405 #[must_use]
407 pub fn from_steps(steps: impl IntoIterator<Item = ProcessingStep>) -> Self {
408 Self(steps.into_iter().collect())
409 }
410
411 pub fn push(&mut self, step: ProcessingStep) {
413 self.0.push(step);
414 }
415
416 pub fn steps(&self) -> impl Iterator<Item = &ProcessingStep> {
418 self.0.iter()
419 }
420
421 #[must_use]
423 pub fn then(mut self, next: Self) -> Self {
424 self.0.extend(next.0);
425 self
426 }
427}