diff --git a/Cargo.lock b/Cargo.lock index 2783b28..edcc75e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1245,6 +1245,7 @@ dependencies = [ "async-nats", "async-trait", "bytes", + "chrono", "futures-util", "libertas_core", "libertas_infra", diff --git a/libertas_api/migrations/20251104042053_add_media_date_taken.sql b/libertas_api/migrations/20251104042053_add_media_date_taken.sql new file mode 100644 index 0000000..a71fb6a --- /dev/null +++ b/libertas_api/migrations/20251104042053_add_media_date_taken.sql @@ -0,0 +1,4 @@ +ALTER TABLE media +ADD COLUMN date_taken TIMESTAMPTZ; + +CREATE INDEX idx_media_date_taken ON media (date_taken); \ No newline at end of file diff --git a/libertas_api/src/services/media_service.rs b/libertas_api/src/services/media_service.rs index ea30f05..646297d 100644 --- a/libertas_api/src/services/media_service.rs +++ b/libertas_api/src/services/media_service.rs @@ -259,6 +259,7 @@ impl MediaServiceImpl { extracted_location: None, width: None, height: None, + date_taken: None, }; self.repo.create(&media_model).await?; diff --git a/libertas_core/src/models.rs b/libertas_core/src/models.rs index 50093ad..3fa1b7b 100644 --- a/libertas_core/src/models.rs +++ b/libertas_core/src/models.rs @@ -36,6 +36,7 @@ pub struct Media { pub extracted_location: Option, pub width: Option, pub height: Option, + pub date_taken: Option>, } #[derive(Clone)] diff --git a/libertas_core/src/repositories.rs b/libertas_core/src/repositories.rs index b7b9cf5..f027e5c 100644 --- a/libertas_core/src/repositories.rs +++ b/libertas_core/src/repositories.rs @@ -18,6 +18,7 @@ pub trait MediaRepository: Send + Sync { width: Option, height: Option, location: Option, + date_taken: Option>, ) -> CoreResult<()>; async fn delete(&self, id: Uuid) -> CoreResult<()>; } diff --git a/libertas_infra/src/db_models.rs b/libertas_infra/src/db_models.rs index a24dfdd..5b679de 100644 --- a/libertas_infra/src/db_models.rs +++ b/libertas_infra/src/db_models.rs @@ -47,6 +47,7 @@ pub struct PostgresMedia { pub extracted_location: Option, pub width: Option, pub height: Option, + pub date_taken: Option>, } #[derive(Debug, Clone, Copy, sqlx::Type, PartialEq, Eq, Deserialize)] diff --git a/libertas_infra/src/mappers.rs b/libertas_infra/src/mappers.rs index 39d72d6..b22a900 100644 --- a/libertas_infra/src/mappers.rs +++ b/libertas_infra/src/mappers.rs @@ -63,6 +63,7 @@ impl From for Media { extracted_location: pg_media.extracted_location, width: pg_media.width, height: pg_media.height, + date_taken: pg_media.date_taken, } } } diff --git a/libertas_infra/src/repositories/media_repository.rs b/libertas_infra/src/repositories/media_repository.rs index a9d3e05..01dbb2b 100644 --- a/libertas_infra/src/repositories/media_repository.rs +++ b/libertas_infra/src/repositories/media_repository.rs @@ -50,7 +50,7 @@ impl MediaRepository for PostgresMediaRepository { PostgresMedia, r#" SELECT id, owner_id, storage_path, original_filename, mime_type, hash, created_at, - extracted_location, width, height + extracted_location, width, height, date_taken FROM media WHERE hash = $1 "#, @@ -68,7 +68,7 @@ impl MediaRepository for PostgresMediaRepository { PostgresMedia, r#" SELECT id, owner_id, storage_path, original_filename, mime_type, hash, created_at, - extracted_location, width, height + extracted_location, width, height, date_taken FROM media WHERE id = $1 "#, @@ -86,7 +86,7 @@ impl MediaRepository for PostgresMediaRepository { PostgresMedia, r#" SELECT id, owner_id, storage_path, original_filename, mime_type, hash, created_at, - extracted_location, width, height + extracted_location, width, height, date_taken FROM media WHERE owner_id = $1 "#, @@ -105,17 +105,19 @@ impl MediaRepository for PostgresMediaRepository { width: Option, height: Option, location: Option, + date_taken: Option>, ) -> CoreResult<()> { sqlx::query!( r#" UPDATE media - SET width = $2, height = $3, extracted_location = $4 + SET width = $2, height = $3, extracted_location = $4, date_taken = $5 WHERE id = $1 "#, id, width, height, - location + location, + date_taken ) .execute(&self.pool) .await diff --git a/libertas_worker/Cargo.toml b/libertas_worker/Cargo.toml index ced273a..69dbdb6 100644 --- a/libertas_worker/Cargo.toml +++ b/libertas_worker/Cargo.toml @@ -25,3 +25,4 @@ uuid = { version = "1.18.1", features = ["v4", "serde"] } nom-exif = { version = "2.5.4", features = ["serde", "tokio", "async"] } async-trait = "0.1.89" xmp_toolkit = "1.11.0" +chrono = "0.4.42" diff --git a/libertas_worker/src/plugins/exif_reader.rs b/libertas_worker/src/plugins/exif_reader.rs index 78d7bba..16e89a9 100644 --- a/libertas_worker/src/plugins/exif_reader.rs +++ b/libertas_worker/src/plugins/exif_reader.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use chrono::{DateTime, NaiveDateTime, Utc}; use std::path::PathBuf; use libertas_core::{ @@ -59,15 +60,20 @@ impl MediaProcessorPlugin for ExifReaderPlugin { .and_then(|f| f.as_u32()) .map(|v| v as i32); - if width.is_some() || height.is_some() || location.is_some() { + let date_taken = exif + .get(ExifTag::DateTimeOriginal) + .and_then(|f| f.as_str()) + .and_then(parse_exif_datetime); + + if width.is_some() || height.is_some() || location.is_some() || date_taken.is_some() { context .media_repo - .update_metadata(media.id, width, height, location.clone()) + .update_metadata(media.id, width, height, location.clone(), date_taken) .await?; let message = format!( - "Extracted EXIF: width={:?}, height={:?}, location={:?}", - width, height, location + "Extracted EXIF: width={:?}, height={:?}, location={:?}, date_taken={:?}", + width, height, location, date_taken ); Ok(PluginData { message }) } else { @@ -77,3 +83,10 @@ impl MediaProcessorPlugin for ExifReaderPlugin { } } } + +fn parse_exif_datetime(s: &str) -> Option> { + // EXIF datetime format is 'YYYY:MM:DD HH:MM:SS' + NaiveDateTime::parse_from_str(s, "%Y:%m:%d %H:%M:%S") + .ok() + .map(|ndt| ndt.and_local_timezone(Utc).unwrap()) +} \ No newline at end of file diff --git a/libertas_worker/src/plugins/xmp_writer.rs b/libertas_worker/src/plugins/xmp_writer.rs index c90a8aa..c21cea0 100644 --- a/libertas_worker/src/plugins/xmp_writer.rs +++ b/libertas_worker/src/plugins/xmp_writer.rs @@ -7,7 +7,7 @@ use libertas_core::{ plugins::{MediaProcessorPlugin, PluginContext, PluginData}, }; use tokio::fs; -use xmp_toolkit::XmpMeta; +use xmp_toolkit::{XmpMeta, XmpValue}; pub struct XmpWriterPlugin; @@ -39,6 +39,18 @@ impl MediaProcessorPlugin for XmpWriterPlugin { CoreError::Unknown(format!("Failed to set description property in XMP: {}", e)) })?; + if let Some(date_taken) = &fresh_media.date_taken { + let date_str = date_taken.to_rfc3339(); + xmp.set_property( + "http://ns.adobe.com/exif/1.0/", + "DateTimeOriginal", + &XmpValue::from(date_str), + ) + .map_err(|e| { + CoreError::Unknown(format!("Failed to set DateTimeOriginal in XMP: {}", e)) + })?; + } + if let Some(_location) = &fresh_media.extracted_location { // TODO: Set location properties in XMP }