use crate::object_model::MobType;
use crate::{AafError, CompositionMob, ContentStorage, Result};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MobRef {
pub mob_id: Uuid,
pub name: String,
pub mob_type: MobTypeKind,
pub duration: Option<i64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MobTypeKind {
Composition,
Master,
Source,
}
impl From<MobType> for MobTypeKind {
fn from(t: MobType) -> Self {
match t {
MobType::Composition => MobTypeKind::Composition,
MobType::Master => MobTypeKind::Master,
MobType::Source => MobTypeKind::Source,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct AafQuery {
pub name_contains: Option<String>,
pub name_contains_ci: Option<String>,
pub mob_type: Option<MobTypeKind>,
pub timecode_range: Option<(i64, i64)>,
pub limit: Option<usize>,
}
impl AafQuery {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_name_contains(mut self, pattern: impl Into<String>) -> Self {
self.name_contains = Some(pattern.into());
self
}
#[must_use]
pub fn with_name_contains_ci(mut self, pattern: impl Into<String>) -> Self {
self.name_contains_ci = Some(pattern.into());
self
}
#[must_use]
pub fn with_mob_type(mut self, mob_type: MobTypeKind) -> Self {
self.mob_type = Some(mob_type);
self
}
#[must_use]
pub fn with_timecode_range(mut self, start: i64, end: i64) -> Self {
self.timecode_range = Some((start, end));
self
}
#[must_use]
pub fn with_limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
fn matches(&self, mob_ref: &MobRef) -> bool {
if let Some(ref pattern) = self.name_contains {
if !mob_ref.name.contains(pattern.as_str()) {
return false;
}
}
if let Some(ref pattern) = self.name_contains_ci {
let name_lower = mob_ref.name.to_lowercase();
let pat_lower = pattern.to_lowercase();
if !name_lower.contains(&pat_lower) {
return false;
}
}
if let Some(ref mob_type) = self.mob_type {
if mob_ref.mob_type != *mob_type {
return false;
}
}
if let Some((range_start, range_end)) = self.timecode_range {
match mob_ref.duration {
Some(dur) => {
if dur <= range_start || 0 >= range_end {
return false;
}
}
None => {
return false;
}
}
}
true
}
}
pub struct AafSearcher;
impl AafSearcher {
pub fn search(storage: &ContentStorage, query: &AafQuery) -> Result<Vec<MobRef>> {
if let Some((start, end)) = query.timecode_range {
if start >= end {
return Err(AafError::InvalidFile(format!(
"Timecode range start ({start}) must be less than end ({end})"
)));
}
}
let mut results: Vec<MobRef> = Vec::new();
for comp_mob in storage.composition_mobs() {
let mob_ref = mob_ref_from_composition(comp_mob);
if query.matches(&mob_ref) {
results.push(mob_ref);
}
}
for mob in storage.master_mobs() {
let mob_ref = MobRef {
mob_id: mob.mob_id(),
name: mob.name().to_string(),
mob_type: MobTypeKind::Master,
duration: None,
};
if query.matches(&mob_ref) {
results.push(mob_ref);
}
}
for mob in storage.source_mobs() {
let mob_ref = MobRef {
mob_id: mob.mob_id(),
name: mob.name().to_string(),
mob_type: MobTypeKind::Source,
duration: None,
};
if query.matches(&mob_ref) {
results.push(mob_ref);
}
}
if let Some(limit) = query.limit {
results.truncate(limit);
}
Ok(results)
}
pub fn find_compositions_by_name(storage: &ContentStorage, name: &str) -> Result<Vec<MobRef>> {
let query = AafQuery::new()
.with_name_contains(name)
.with_mob_type(MobTypeKind::Composition);
Self::search(storage, &query)
}
pub fn find_compositions_by_name_ci(
storage: &ContentStorage,
name: &str,
) -> Result<Vec<MobRef>> {
let query = AafQuery::new()
.with_name_contains_ci(name)
.with_mob_type(MobTypeKind::Composition);
Self::search(storage, &query)
}
}
fn mob_ref_from_composition(comp_mob: &CompositionMob) -> MobRef {
MobRef {
mob_id: comp_mob.mob_id(),
name: comp_mob.name().to_string(),
mob_type: MobTypeKind::Composition,
duration: comp_mob.duration(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::composition::{
CompositionMob, Sequence, SequenceComponent, SourceClip, Track, TrackType,
};
use crate::dictionary::Auid;
use crate::timeline::{EditRate, Position};
use crate::ContentStorage;
use uuid::Uuid;
fn make_storage_with_compositions() -> ContentStorage {
let mut storage = ContentStorage::new();
let mut comp1 = CompositionMob::new(Uuid::new_v4(), "Main Edit");
let mut seq = Sequence::new(Auid::PICTURE);
seq.add_component(SequenceComponent::SourceClip(SourceClip::new(
100,
Position::zero(),
Uuid::new_v4(),
1,
)));
let mut track = Track::new(1, "Video", EditRate::PAL_25, TrackType::Picture);
track.set_sequence(seq);
comp1.add_track(track);
storage.add_composition_mob(comp1);
let comp2 = CompositionMob::new(Uuid::new_v4(), "Rough Cut");
storage.add_composition_mob(comp2);
let comp3 = CompositionMob::new(Uuid::new_v4(), "main_audio");
storage.add_composition_mob(comp3);
storage
}
#[test]
fn test_search_all_compositions() {
let storage = make_storage_with_compositions();
let query = AafQuery::new().with_mob_type(MobTypeKind::Composition);
let results = AafSearcher::search(&storage, &query).expect("search should succeed");
assert_eq!(results.len(), 3);
}
#[test]
fn test_search_by_name_case_sensitive() {
let storage = make_storage_with_compositions();
let query = AafQuery::new()
.with_name_contains("Main")
.with_mob_type(MobTypeKind::Composition);
let results = AafSearcher::search(&storage, &query).expect("search should succeed");
assert_eq!(results.len(), 1);
assert_eq!(results[0].name, "Main Edit");
}
#[test]
fn test_search_by_name_case_insensitive() {
let storage = make_storage_with_compositions();
let query = AafQuery::new()
.with_name_contains_ci("main")
.with_mob_type(MobTypeKind::Composition);
let results = AafSearcher::search(&storage, &query).expect("search should succeed");
assert_eq!(results.len(), 2);
}
#[test]
fn test_search_no_results() {
let storage = make_storage_with_compositions();
let query = AafQuery::new().with_name_contains("DoesNotExist");
let results = AafSearcher::search(&storage, &query).expect("search should succeed");
assert!(results.is_empty());
}
#[test]
fn test_search_timecode_range() {
let storage = make_storage_with_compositions();
let query = AafQuery::new()
.with_mob_type(MobTypeKind::Composition)
.with_timecode_range(0, 200);
let results = AafSearcher::search(&storage, &query).expect("search should succeed");
assert_eq!(results.len(), 1);
assert_eq!(results[0].name, "Main Edit");
}
#[test]
fn test_search_timecode_range_no_overlap() {
let storage = make_storage_with_compositions();
let query = AafQuery::new()
.with_mob_type(MobTypeKind::Composition)
.with_timecode_range(200, 400);
let results = AafSearcher::search(&storage, &query).expect("search should succeed");
assert!(results.is_empty());
}
#[test]
fn test_search_invalid_timecode_range() {
let storage = make_storage_with_compositions();
let query = AafQuery::new().with_timecode_range(100, 50);
let result = AafSearcher::search(&storage, &query);
assert!(result.is_err());
}
#[test]
fn test_search_with_limit() {
let storage = make_storage_with_compositions();
let query = AafQuery::new()
.with_mob_type(MobTypeKind::Composition)
.with_limit(1);
let results = AafSearcher::search(&storage, &query).expect("search should succeed");
assert_eq!(results.len(), 1);
}
#[test]
fn test_find_compositions_by_name_ci() {
let storage = make_storage_with_compositions();
let results =
AafSearcher::find_compositions_by_name_ci(&storage, "cut").expect("should succeed");
assert_eq!(results.len(), 1);
assert_eq!(results[0].name, "Rough Cut");
}
}