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(clippy::cast_precision_loss, reason = "format observations are compared, not accumulated")]
54 pub fn as_number(&self) -> Option<f64> {
55 match self {
56 Self::Integer(value) => Some(*value as f64),
57 Self::Float(value) => Some(*value),
58 _ => None,
59 }
60 }
61}
62
63#[derive(Clone, Debug, Default, PartialEq)]
68pub struct InfoRecord(BTreeMap<String, InfoValue>);
69
70impl InfoRecord {
71 pub fn insert(&mut self, key: impl Into<String>, value: impl Into<InfoValue>) -> Option<InfoValue> {
73 self.0.insert(key.into(), value.into())
74 }
75
76 #[must_use]
78 pub fn get(&self, key: &str) -> Option<&InfoValue> {
79 self.0.get(key)
80 }
81
82 #[must_use]
84 pub fn is_empty(&self) -> bool {
85 self.0.is_empty()
86 }
87
88 #[must_use]
90 pub fn len(&self) -> usize {
91 self.0.len()
92 }
93}
94
95impl From<&str> for InfoValue {
96 fn from(value: &str) -> Self {
97 Self::String(value.to_owned())
98 }
99}
100
101#[derive(Clone, Debug, PartialEq, new)]
108pub struct Format {
109 pub id: String,
111 pub metadata: InfoRecord,
113}
114
115impl Format {
116 #[must_use]
118 pub fn observe(&self, key: &str) -> Option<&InfoValue> {
119 self.metadata.get(key)
120 }
121
122 #[must_use]
124 pub fn text(&self, key: &str) -> Option<&str> {
125 self.observe(key).and_then(InfoValue::as_text)
126 }
127
128 #[must_use]
130 pub fn number(&self, key: &str) -> Option<f64> {
131 self.observe(key).and_then(InfoValue::as_number)
132 }
133
134 #[must_use]
136 pub fn flag(&self, key: &str) -> bool {
137 self.observe(key).and_then(InfoValue::as_bool).unwrap_or(false)
138 }
139
140 #[must_use]
142 pub fn extension(&self) -> Option<&str> {
143 self.text(FORMAT_EXTENSION)
144 }
145
146 #[must_use]
148 pub fn has_audio(&self) -> bool {
149 self.flag(FORMAT_HAS_AUDIO)
150 }
151
152 #[must_use]
154 pub fn has_video(&self) -> bool {
155 self.flag(FORMAT_HAS_VIDEO)
156 }
157
158 #[must_use]
160 pub fn source(&self) -> Option<&str> {
161 self.text(FORMAT_SOURCE)
162 }
163}
164
165#[derive(Clone, Debug, PartialEq, new)]
167pub struct Artifact {
168 pub id: String,
170 pub kind: ArtifactKind,
172 pub metadata: InfoRecord,
174}
175
176impl Artifact {
177 #[must_use]
179 pub fn location(&self) -> Option<&str> {
180 self.metadata.get(ARTIFACT_LOCATION).and_then(InfoValue::as_text)
181 }
182}
183
184#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
186pub struct ExtractorKey(pub String);
187
188impl ExtractorKey {
189 #[must_use]
191 pub fn new(value: impl Into<String>) -> Self {
192 Self(value.into())
193 }
194}
195
196#[derive(Clone, Debug, PartialEq, new)]
198pub struct Media {
199 pub id: String,
201 pub metadata: InfoRecord,
203 pub formats: Vec<Format>,
205}
206
207#[derive(Clone, Debug, PartialEq, Eq, new)]
209pub struct UrlReference {
210 pub url: String,
212 pub extractor: Option<ExtractorKey>,
214 pub transparent: bool,
216}
217
218#[derive(Clone, Debug, PartialEq, new)]
220pub struct Collection {
221 pub id: Option<String>,
223 pub kind: CollectionKind,
225 pub metadata: InfoRecord,
227 pub entries: Vec<Extraction>,
229}
230
231#[derive(Clone, Debug, PartialEq)]
233pub enum Extraction {
234 Media(Media),
236 Url(UrlReference),
238 Collection(Collection),
240}
241
242#[derive(Clone, Debug, PartialEq)]
247pub enum FormatPredicate {
248 Any,
250 Id(String),
252 Observed(String),
254 Text {
256 key: String,
258 value: String,
260 },
261 Flag {
263 key: String,
265 value: bool,
267 },
268 Number {
270 key: String,
272 comparison: FormatComparison,
274 value: f64,
276 },
277 All(Box<FormatPredicate>, Box<FormatPredicate>),
279 Not(Box<FormatPredicate>),
281}
282
283#[derive(Clone, Debug, PartialEq)]
285pub enum FormatSelection {
286 Best(FormatPredicate),
288 Worst(FormatPredicate),
290 Merge(Vec<FormatSelection>),
292 Fallback(Vec<FormatSelection>),
294}
295
296#[must_use]
301pub fn predicate_accepts(predicate: &FormatPredicate, format: &Format) -> bool {
302 match predicate {
303 FormatPredicate::Any => true,
304 FormatPredicate::Id(id) => format.id == *id,
305 FormatPredicate::Observed(key) => format.observe(key).is_some(),
306 FormatPredicate::Text { key, value } => format.text(key) == Some(value.as_str()),
307 FormatPredicate::Flag { key, value } => format.flag(key) == *value,
308 FormatPredicate::Number { key, comparison, value } => {
309 format.number(key).is_some_and(|observed| match comparison {
310 FormatComparison::AtMost => observed <= *value,
311 FormatComparison::Exactly => (observed - *value).abs() < f64::EPSILON,
312 FormatComparison::AtLeast => observed >= *value,
313 })
314 }
315 FormatPredicate::All(left, right) => predicate_accepts(left, format) && predicate_accepts(right, format),
316 FormatPredicate::Not(child) => !predicate_accepts(child, format),
317 }
318}
319
320#[must_use]
325pub fn interpret_selection<'a, This>(
326 alg: &This,
327 formats: &'a [Format],
328 selection: &FormatSelection,
329) -> Option<Vec<&'a Format>>
330where
331 This: FormatPredicateMatchAlg<Predicate = FormatPredicate, Format = Format>,
332{
333 match selection {
334 FormatSelection::Best(predicate) => {
335 formats.iter().rev().find(|format| alg.format_matches(predicate, format)).map(|format| vec![format])
336 }
337 FormatSelection::Worst(predicate) => {
338 formats.iter().find(|format| alg.format_matches(predicate, format)).map(|format| vec![format])
339 }
340 FormatSelection::Merge(selections) => {
341 selections.iter().traverse_iter(|child| interpret_selection(alg, formats, child).ok_or(())).ok()
342 }
343 FormatSelection::Fallback(selections) => {
344 selections.iter().find_map(|child| interpret_selection(alg, formats, child))
345 }
346 }
347}
348
349#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
351pub struct ProcessorId(pub String);
352
353impl ProcessorId {
354 #[must_use]
356 pub fn new(value: impl Into<String>) -> Self {
357 Self(value.into())
358 }
359}
360
361#[derive(Clone, Debug, PartialEq, Eq)]
363pub struct ProcessingStep {
364 pub stage: ProcessingStage,
366 pub processor: ProcessorId,
368}
369
370impl ProcessingStep {
371 #[must_use]
373 pub fn new(stage: ProcessingStage, processor: impl Into<String>) -> Self {
374 Self { stage, processor: ProcessorId::new(processor) }
375 }
376}
377
378#[derive(Clone, Debug, Default, PartialEq, Eq)]
380pub struct ProcessingProgram(Vec<ProcessingStep>);
381
382impl ProcessingProgram {
383 #[must_use]
385 pub fn from_steps(steps: impl IntoIterator<Item = ProcessingStep>) -> Self {
386 Self(steps.into_iter().collect())
387 }
388
389 pub fn push(&mut self, step: ProcessingStep) {
391 self.0.push(step);
392 }
393
394 pub fn steps(&self) -> impl Iterator<Item = &ProcessingStep> {
396 self.0.iter()
397 }
398
399 #[must_use]
401 pub fn then(mut self, next: Self) -> Self {
402 self.0.extend(next.0);
403 self
404 }
405}