use ::bson::{Bson, Document};
use core::str::FromStr;
use mediaframe::{
capture::{Device, GeoLocation},
container::Format,
};
use crate::domain::{aggregates::media::Media, enums::MediaKind, MediaErrorFlags, Uuid7};
use super::{error::MongoError, util::*};
fn media_kind_to_i32(k: MediaKind) -> i32 {
match k {
MediaKind::Video => 0,
MediaKind::Audio => 1,
}
}
fn media_kind_from_i64(v: i64, field: &'static str) -> Result<MediaKind, MongoError> {
match v {
0 => Ok(MediaKind::Video),
1 => Ok(MediaKind::Audio),
_ => Err(MongoError::IntOutOfRange {
field: smol_str::SmolStr::from(field),
value: v,
}),
}
}
fn device_to_bson(d: &Device) -> Bson {
let mut doc = Document::new();
doc.insert("make", Bson::String(d.make().to_owned()));
doc.insert("model", Bson::String(d.model().to_owned()));
Bson::Document(doc)
}
fn device_from_bson(b: Bson, field: &'static str) -> Result<Device, MongoError> {
let mut d = as_doc(b, field)?;
let make = as_smol(take(&mut d, "make")?, "make")?;
let model = as_smol(take(&mut d, "model")?, "model")?;
Ok(Device::new().with_make(make).with_model(model))
}
fn geo_to_bson(g: &GeoLocation) -> Bson {
let mut doc = Document::new();
doc.insert("lat", Bson::Double(g.lat()));
doc.insert("lon", Bson::Double(g.lon()));
doc.insert(
"altitude",
g.altitude()
.map(|a| Bson::Double(a as f64))
.unwrap_or(Bson::Null),
);
Bson::Document(doc)
}
fn geo_from_bson(b: Bson, field: &'static str) -> Result<GeoLocation, MongoError> {
let mut d = as_doc(b, field)?;
let lat = as_f64(take(&mut d, "lat")?, "lat")?;
let lon = as_f64(take(&mut d, "lon")?, "lon")?;
let altitude = opt(take_opt(&mut d, "altitude"), |bb| as_f32(bb, "altitude"))?;
Ok(GeoLocation::try_new(lat, lon, altitude)?)
}
impl From<&Media<Uuid7>> for Document {
fn from(m: &Media<Uuid7>) -> Self {
let mut d = Document::new();
d.insert("_id", uuid7_to_bson(*m.id_ref()));
d.insert("checksum", checksum_to_bson(m.checksum_ref()));
d.insert("format", Bson::String(m.format_ref().as_str().to_owned()));
d.insert("size", Bson::Int64(m.size() as i64));
d.insert(
"duration",
m.duration_ref()
.map(|t| media_ts_to_bson(*t))
.unwrap_or(Bson::Null),
);
d.insert("kind", Bson::Int32(media_kind_to_i32(m.kind())));
d.insert("nb_streams", Bson::Int64(i64::from(m.nb_streams())));
d.insert("nb_chapters", Bson::Int64(i64::from(m.nb_chapters())));
d.insert(
"video_id",
m.video_id_ref()
.map(|i| uuid7_to_bson(*i))
.unwrap_or(Bson::Null),
);
d.insert(
"audio_id",
m.audio_id_ref()
.map(|i| uuid7_to_bson(*i))
.unwrap_or(Bson::Null),
);
d.insert(
"subtitle_id",
m.subtitle_id_ref()
.map(|i| uuid7_to_bson(*i))
.unwrap_or(Bson::Null),
);
d.insert("error_flags", Bson::Int64(m.error_flags().bits() as i64));
d.insert(
"probe_error",
m.probe_error_ref()
.map(error_info_to_bson)
.unwrap_or(Bson::Null),
);
d.insert(
"capture_date",
m.capture_date_ref()
.map(|t| jiff_to_bson(*t))
.unwrap_or(Bson::Null),
);
d.insert(
"device",
m.device_ref().map(device_to_bson).unwrap_or(Bson::Null),
);
d.insert("gps", m.gps_ref().map(geo_to_bson).unwrap_or(Bson::Null));
d
}
}
impl TryFrom<Document> for Media<Uuid7> {
type Error = MongoError;
fn try_from(mut d: Document) -> Result<Self, Self::Error> {
let id = uuid7_from_bson(take(&mut d, "_id")?, "_id")?;
let checksum = checksum_from_bson(take(&mut d, "checksum")?, "checksum")?;
let Ok(format) = Format::from_str(&as_str(take(&mut d, "format")?, "format")?);
let size = as_u64(take(&mut d, "size")?, "size")?;
let kind = media_kind_from_i64(as_i64(take(&mut d, "kind")?, "kind")?, "kind")?;
let mut m = Media::try_new(id, checksum, format, size, kind)?;
if let Some(b) = take_opt(&mut d, "nb_streams") {
let n = as_u64(b, "nb_streams")?;
let n32 = u32::try_from(n).map_err(|_| MongoError::IntOutOfRange {
field: smol_str::SmolStr::from("nb_streams"),
value: n as i64,
})?;
m.set_nb_streams(n32);
}
if let Some(b) = take_opt(&mut d, "nb_chapters") {
let n = as_u64(b, "nb_chapters")?;
let n32 = u32::try_from(n).map_err(|_| MongoError::IntOutOfRange {
field: smol_str::SmolStr::from("nb_chapters"),
value: n as i64,
})?;
m.set_nb_chapters(n32);
}
if let Some(b) = take_opt(&mut d, "duration") {
m.try_set_duration(Some(media_ts_from_bson(b, "duration")?))?;
}
if let Some(b) = take_opt(&mut d, "video_id") {
m.set_video_id(Some(uuid7_from_bson(b, "video_id")?));
}
if let Some(b) = take_opt(&mut d, "audio_id") {
m.set_audio_id(Some(uuid7_from_bson(b, "audio_id")?));
}
if let Some(b) = take_opt(&mut d, "subtitle_id") {
m.set_subtitle_id(Some(uuid7_from_bson(b, "subtitle_id")?));
}
if let Some(b) = take_opt(&mut d, "error_flags") {
let bits = as_u64(b, "error_flags")?;
let bits16 = u16::try_from(bits).map_err(|_| MongoError::IntOutOfRange {
field: smol_str::SmolStr::from("error_flags"),
value: bits as i64,
})?;
m.set_error_flags(MediaErrorFlags::from_bits_truncate(bits16));
}
if let Some(b) = take_opt(&mut d, "probe_error") {
m.set_probe_error(Some(error_info_from_bson(b, "probe_error")?));
}
if let Some(b) = take_opt(&mut d, "capture_date") {
m.set_capture_date(Some(jiff_from_bson(b, "capture_date")?));
}
if let Some(b) = take_opt(&mut d, "device") {
m.set_device(Some(device_from_bson(b, "device")?));
}
if let Some(b) = take_opt(&mut d, "gps") {
m.set_gps(Some(geo_from_bson(b, "gps")?));
}
Ok(m)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::primitives::{ErrorCode, ErrorInfo, FileChecksum};
use core::num::NonZeroU32;
use jiff::Timestamp as JiffTimestamp;
use mediatime::{Timebase, Timestamp as MediaTimestamp};
fn mp4() -> Format {
Format::Mp4
}
fn cs() -> FileChecksum {
let mut b = [0u8; 32];
b[0] = 1;
FileChecksum::from_bytes(b)
}
fn tb() -> Timebase {
Timebase::new(1, NonZeroU32::new(1000).unwrap())
}
#[test]
fn media_minimal_roundtrip() {
let m = Media::try_new(Uuid7::new(), cs(), mp4(), 12_345, MediaKind::Video).unwrap();
let doc: Document = (&m).into();
let m2: Media<Uuid7> = doc.try_into().unwrap();
assert_eq!(m, m2);
}
#[test]
fn media_full_roundtrip() {
let m = Media::try_new(Uuid7::new(), cs(), mp4(), 999_999, MediaKind::Audio)
.unwrap()
.with_video_id(Some(Uuid7::new()))
.with_audio_id(Some(Uuid7::new()))
.with_subtitle_id(Some(Uuid7::new()))
.try_with_duration(Some(MediaTimestamp::new(60_000, tb())))
.unwrap()
.with_error_flags(MediaErrorFlags::AUDIO_ERROR | MediaErrorFlags::SUBTITLE_ERROR)
.with_probe_error(Some(ErrorInfo::new(ErrorCode::ProbeCorrupt, "bad header")))
.with_capture_date(Some(JiffTimestamp::default()))
.with_device(Some(
Device::new().with_make("Apple").with_model("iPhone 15 Pro"),
))
.with_gps(Some(
GeoLocation::try_new(37.7749, -122.4194, Some(20.0)).unwrap(),
));
let doc: Document = (&m).into();
let m2: Media<Uuid7> = doc.try_into().unwrap();
assert_eq!(m, m2);
}
#[test]
fn media_missing_id_errors() {
let mut d = Document::new();
d.insert("checksum", checksum_to_bson(&cs()));
let err = Media::<Uuid7>::try_from(d).unwrap_err();
assert!(err.is_missing_field());
}
#[test]
fn media_zero_checksum_rejected() {
let mut d = Document::new();
d.insert("_id", uuid7_to_bson(Uuid7::new()));
d.insert("checksum", checksum_to_bson(&FileChecksum::new()));
d.insert("format", "mp4");
d.insert("size", Bson::Int64(0));
d.insert("kind", Bson::Int32(0));
let err = Media::<Uuid7>::try_from(d).unwrap_err();
assert!(err.is_media());
}
}