use alux_sdk::IterTraversableExt;
use derive_more::From;
use derive_new::new;
use rsynko_media::*;
use std::collections::BTreeMap;
#[derive(Clone, Debug, Default, PartialEq, From)]
pub enum InfoValue {
#[default]
Null,
Bool(bool),
Integer(i64),
Float(f64),
String(String),
List(Vec<InfoValue>),
Record(InfoRecord),
}
impl InfoValue {
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::String(value) => Some(value),
_ => None,
}
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(value) => Some(*value),
_ => None,
}
}
#[must_use]
#[expect(clippy::cast_precision_loss, reason = "format observations are compared, not accumulated")]
pub fn as_number(&self) -> Option<f64> {
match self {
Self::Integer(value) => Some(*value as f64),
Self::Float(value) => Some(*value),
_ => None,
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct InfoRecord(BTreeMap<String, InfoValue>);
impl InfoRecord {
pub fn insert(&mut self, key: impl Into<String>, value: impl Into<InfoValue>) -> Option<InfoValue> {
self.0.insert(key.into(), value.into())
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&InfoValue> {
self.0.get(key)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
}
impl From<&str> for InfoValue {
fn from(value: &str) -> Self {
Self::String(value.to_owned())
}
}
#[derive(Clone, Debug, PartialEq, new)]
pub struct Format {
pub id: String,
pub metadata: InfoRecord,
}
impl Format {
#[must_use]
pub fn observe(&self, key: &str) -> Option<&InfoValue> {
self.metadata.get(key)
}
#[must_use]
pub fn text(&self, key: &str) -> Option<&str> {
self.observe(key).and_then(InfoValue::as_text)
}
#[must_use]
pub fn number(&self, key: &str) -> Option<f64> {
self.observe(key).and_then(InfoValue::as_number)
}
#[must_use]
pub fn flag(&self, key: &str) -> bool {
self.observe(key).and_then(InfoValue::as_bool).unwrap_or(false)
}
#[must_use]
pub fn extension(&self) -> Option<&str> {
self.text(FORMAT_EXTENSION)
}
#[must_use]
pub fn has_audio(&self) -> bool {
self.flag(FORMAT_HAS_AUDIO)
}
#[must_use]
pub fn has_video(&self) -> bool {
self.flag(FORMAT_HAS_VIDEO)
}
#[must_use]
pub fn source(&self) -> Option<&str> {
self.text(FORMAT_SOURCE)
}
}
#[derive(Clone, Debug, PartialEq, new)]
pub struct Artifact {
pub id: String,
pub kind: ArtifactKind,
pub metadata: InfoRecord,
}
impl Artifact {
#[must_use]
pub fn location(&self) -> Option<&str> {
self.metadata.get(ARTIFACT_LOCATION).and_then(InfoValue::as_text)
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ExtractorKey(pub String);
impl ExtractorKey {
#[must_use]
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
}
#[derive(Clone, Debug, PartialEq, new)]
pub struct Media {
pub id: String,
pub metadata: InfoRecord,
pub formats: Vec<Format>,
}
#[derive(Clone, Debug, PartialEq, Eq, new)]
pub struct UrlReference {
pub url: String,
pub extractor: Option<ExtractorKey>,
pub transparent: bool,
}
#[derive(Clone, Debug, PartialEq, new)]
pub struct Collection {
pub id: Option<String>,
pub kind: CollectionKind,
pub metadata: InfoRecord,
pub entries: Vec<Extraction>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Extraction {
Media(Media),
Url(UrlReference),
Collection(Collection),
}
#[derive(Clone, Debug, PartialEq)]
pub enum FormatPredicate {
Any,
Id(String),
Observed(String),
Text {
key: String,
value: String,
},
Flag {
key: String,
value: bool,
},
Number {
key: String,
comparison: FormatComparison,
value: f64,
},
All(Box<FormatPredicate>, Box<FormatPredicate>),
Not(Box<FormatPredicate>),
}
#[derive(Clone, Debug, PartialEq)]
pub enum FormatSelection {
Best(FormatPredicate),
Worst(FormatPredicate),
Merge(Vec<FormatSelection>),
Fallback(Vec<FormatSelection>),
}
#[must_use]
pub fn predicate_accepts(predicate: &FormatPredicate, format: &Format) -> bool {
match predicate {
FormatPredicate::Any => true,
FormatPredicate::Id(id) => format.id == *id,
FormatPredicate::Observed(key) => format.observe(key).is_some(),
FormatPredicate::Text { key, value } => format.text(key) == Some(value.as_str()),
FormatPredicate::Flag { key, value } => format.flag(key) == *value,
FormatPredicate::Number { key, comparison, value } => {
format.number(key).is_some_and(|observed| match comparison {
FormatComparison::AtMost => observed <= *value,
FormatComparison::Exactly => (observed - *value).abs() < f64::EPSILON,
FormatComparison::AtLeast => observed >= *value,
})
}
FormatPredicate::All(left, right) => predicate_accepts(left, format) && predicate_accepts(right, format),
FormatPredicate::Not(child) => !predicate_accepts(child, format),
}
}
#[must_use]
pub fn interpret_selection<'a, This>(
alg: &This,
formats: &'a [Format],
selection: &FormatSelection,
) -> Option<Vec<&'a Format>>
where
This: FormatPredicateMatchAlg<Predicate = FormatPredicate, Format = Format>,
{
match selection {
FormatSelection::Best(predicate) => {
formats.iter().rev().find(|format| alg.format_matches(predicate, format)).map(|format| vec![format])
}
FormatSelection::Worst(predicate) => {
formats.iter().find(|format| alg.format_matches(predicate, format)).map(|format| vec![format])
}
FormatSelection::Merge(selections) => {
selections.iter().traverse_iter(|child| interpret_selection(alg, formats, child).ok_or(())).ok()
}
FormatSelection::Fallback(selections) => {
selections.iter().find_map(|child| interpret_selection(alg, formats, child))
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct ProcessorId(pub String);
impl ProcessorId {
#[must_use]
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProcessingStep {
pub stage: ProcessingStage,
pub processor: ProcessorId,
}
impl ProcessingStep {
#[must_use]
pub fn new(stage: ProcessingStage, processor: impl Into<String>) -> Self {
Self { stage, processor: ProcessorId::new(processor) }
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ProcessingProgram(Vec<ProcessingStep>);
impl ProcessingProgram {
#[must_use]
pub fn from_steps(steps: impl IntoIterator<Item = ProcessingStep>) -> Self {
Self(steps.into_iter().collect())
}
pub fn push(&mut self, step: ProcessingStep) {
self.0.push(step);
}
pub fn steps(&self) -> impl Iterator<Item = &ProcessingStep> {
self.0.iter()
}
#[must_use]
pub fn then(mut self, next: Self) -> Self {
self.0.extend(next.0);
self
}
}