feat: add date_taken field to media model and update related functionalities

This commit is contained in:
2025-11-04 05:28:27 +01:00
parent 39ee8d52a4
commit 828d8e4a2b
11 changed files with 48 additions and 10 deletions

1
Cargo.lock generated
View File

@@ -1245,6 +1245,7 @@ dependencies = [
"async-nats", "async-nats",
"async-trait", "async-trait",
"bytes", "bytes",
"chrono",
"futures-util", "futures-util",
"libertas_core", "libertas_core",
"libertas_infra", "libertas_infra",

View File

@@ -0,0 +1,4 @@
ALTER TABLE media
ADD COLUMN date_taken TIMESTAMPTZ;
CREATE INDEX idx_media_date_taken ON media (date_taken);

View File

@@ -259,6 +259,7 @@ impl MediaServiceImpl {
extracted_location: None, extracted_location: None,
width: None, width: None,
height: None, height: None,
date_taken: None,
}; };
self.repo.create(&media_model).await?; self.repo.create(&media_model).await?;

View File

@@ -36,6 +36,7 @@ pub struct Media {
pub extracted_location: Option<String>, pub extracted_location: Option<String>,
pub width: Option<i32>, pub width: Option<i32>,
pub height: Option<i32>, pub height: Option<i32>,
pub date_taken: Option<chrono::DateTime<chrono::Utc>>,
} }
#[derive(Clone)] #[derive(Clone)]

View File

@@ -18,6 +18,7 @@ pub trait MediaRepository: Send + Sync {
width: Option<i32>, width: Option<i32>,
height: Option<i32>, height: Option<i32>,
location: Option<String>, location: Option<String>,
date_taken: Option<chrono::DateTime<chrono::Utc>>,
) -> CoreResult<()>; ) -> CoreResult<()>;
async fn delete(&self, id: Uuid) -> CoreResult<()>; async fn delete(&self, id: Uuid) -> CoreResult<()>;
} }

View File

@@ -47,6 +47,7 @@ pub struct PostgresMedia {
pub extracted_location: Option<String>, pub extracted_location: Option<String>,
pub width: Option<i32>, pub width: Option<i32>,
pub height: Option<i32>, pub height: Option<i32>,
pub date_taken: Option<chrono::DateTime<chrono::Utc>>,
} }
#[derive(Debug, Clone, Copy, sqlx::Type, PartialEq, Eq, Deserialize)] #[derive(Debug, Clone, Copy, sqlx::Type, PartialEq, Eq, Deserialize)]

View File

@@ -63,6 +63,7 @@ impl From<PostgresMedia> for Media {
extracted_location: pg_media.extracted_location, extracted_location: pg_media.extracted_location,
width: pg_media.width, width: pg_media.width,
height: pg_media.height, height: pg_media.height,
date_taken: pg_media.date_taken,
} }
} }
} }

View File

@@ -50,7 +50,7 @@ impl MediaRepository for PostgresMediaRepository {
PostgresMedia, PostgresMedia,
r#" r#"
SELECT id, owner_id, storage_path, original_filename, mime_type, hash, created_at, 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 FROM media
WHERE hash = $1 WHERE hash = $1
"#, "#,
@@ -68,7 +68,7 @@ impl MediaRepository for PostgresMediaRepository {
PostgresMedia, PostgresMedia,
r#" r#"
SELECT id, owner_id, storage_path, original_filename, mime_type, hash, created_at, 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 FROM media
WHERE id = $1 WHERE id = $1
"#, "#,
@@ -86,7 +86,7 @@ impl MediaRepository for PostgresMediaRepository {
PostgresMedia, PostgresMedia,
r#" r#"
SELECT id, owner_id, storage_path, original_filename, mime_type, hash, created_at, 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 FROM media
WHERE owner_id = $1 WHERE owner_id = $1
"#, "#,
@@ -105,17 +105,19 @@ impl MediaRepository for PostgresMediaRepository {
width: Option<i32>, width: Option<i32>,
height: Option<i32>, height: Option<i32>,
location: Option<String>, location: Option<String>,
date_taken: Option<chrono::DateTime<chrono::Utc>>,
) -> CoreResult<()> { ) -> CoreResult<()> {
sqlx::query!( sqlx::query!(
r#" r#"
UPDATE media UPDATE media
SET width = $2, height = $3, extracted_location = $4 SET width = $2, height = $3, extracted_location = $4, date_taken = $5
WHERE id = $1 WHERE id = $1
"#, "#,
id, id,
width, width,
height, height,
location location,
date_taken
) )
.execute(&self.pool) .execute(&self.pool)
.await .await

View File

@@ -25,3 +25,4 @@ uuid = { version = "1.18.1", features = ["v4", "serde"] }
nom-exif = { version = "2.5.4", features = ["serde", "tokio", "async"] } nom-exif = { version = "2.5.4", features = ["serde", "tokio", "async"] }
async-trait = "0.1.89" async-trait = "0.1.89"
xmp_toolkit = "1.11.0" xmp_toolkit = "1.11.0"
chrono = "0.4.42"

View File

@@ -1,4 +1,5 @@
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{DateTime, NaiveDateTime, Utc};
use std::path::PathBuf; use std::path::PathBuf;
use libertas_core::{ use libertas_core::{
@@ -59,15 +60,20 @@ impl MediaProcessorPlugin for ExifReaderPlugin {
.and_then(|f| f.as_u32()) .and_then(|f| f.as_u32())
.map(|v| v as i32); .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 context
.media_repo .media_repo
.update_metadata(media.id, width, height, location.clone()) .update_metadata(media.id, width, height, location.clone(), date_taken)
.await?; .await?;
let message = format!( let message = format!(
"Extracted EXIF: width={:?}, height={:?}, location={:?}", "Extracted EXIF: width={:?}, height={:?}, location={:?}, date_taken={:?}",
width, height, location width, height, location, date_taken
); );
Ok(PluginData { message }) Ok(PluginData { message })
} else { } else {
@@ -77,3 +83,10 @@ impl MediaProcessorPlugin for ExifReaderPlugin {
} }
} }
} }
fn parse_exif_datetime(s: &str) -> Option<DateTime<Utc>> {
// 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())
}

View File

@@ -7,7 +7,7 @@ use libertas_core::{
plugins::{MediaProcessorPlugin, PluginContext, PluginData}, plugins::{MediaProcessorPlugin, PluginContext, PluginData},
}; };
use tokio::fs; use tokio::fs;
use xmp_toolkit::XmpMeta; use xmp_toolkit::{XmpMeta, XmpValue};
pub struct XmpWriterPlugin; pub struct XmpWriterPlugin;
@@ -39,6 +39,18 @@ impl MediaProcessorPlugin for XmpWriterPlugin {
CoreError::Unknown(format!("Failed to set description property in XMP: {}", e)) 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 { if let Some(_location) = &fresh_media.extracted_location {
// TODO: Set location properties in XMP // TODO: Set location properties in XMP
} }