feat: implement media metadata management with EXIF and TrackInfo support

This commit is contained in:
2025-11-14 07:41:54 +01:00
parent ea95c2255f
commit 55cf4db2de
18 changed files with 343 additions and 195 deletions

View File

@@ -3,7 +3,7 @@ use std::{path::PathBuf, sync::Arc};
use futures_util::StreamExt;
use libertas_core::plugins::PluginContext;
use libertas_infra::factory::{
build_album_repository, build_database_pool, build_media_repository, build_user_repository,
build_album_repository, build_database_pool, build_media_metadata_repository, build_media_repository, build_user_repository
};
use serde::Deserialize;
use tokio::fs;
@@ -37,11 +37,14 @@ async fn main() -> anyhow::Result<()> {
let media_repo = build_media_repository(&config, db_pool.clone()).await?;
let album_repo = build_album_repository(&config.database, db_pool.clone()).await?;
let user_repo = build_user_repository(&config.database, db_pool.clone()).await?;
let metadata_repo =
build_media_metadata_repository(&config.database, db_pool.clone()).await?;
let context = Arc::new(PluginContext {
media_repo,
album_repo,
user_repo,
metadata_repo,
media_library_path: config.media_library_path.clone(),
config: Arc::new(config.clone()),
});

View File

@@ -1,8 +1,9 @@
use async_trait::async_trait;
use uuid::Uuid;
use std::path::PathBuf;
use libertas_core::{
error::CoreResult, media_utils::extract_exif_data, models::Media, plugins::{MediaProcessorPlugin, PluginContext, PluginData}
error::CoreResult, media_utils::extract_exif_data, models::{Media, MediaMetadata}, plugins::{MediaProcessorPlugin, PluginContext, PluginData}
};
pub struct ExifReaderPlugin;
@@ -16,8 +17,8 @@ impl MediaProcessorPlugin for ExifReaderPlugin {
async fn process(&self, media: &Media, context: &PluginContext) -> CoreResult<PluginData> {
let file_path = PathBuf::from(&context.media_library_path).join(&media.storage_path);
let (width, height, location, date_taken) = match extract_exif_data(&file_path).await {
Ok(data) => (data.width, data.height, data.location, data.date_taken),
let extracted_data = match extract_exif_data(&file_path).await {
Ok(data) => data,
Err(e) => {
return Ok(PluginData {
message: format!("Could not parse EXIF: {}", e),
@@ -25,21 +26,28 @@ impl MediaProcessorPlugin for ExifReaderPlugin {
}
};
if width.is_some() || height.is_some() || location.is_some() || date_taken.is_some() {
context
.media_repo
.update_exif_data(media.id, width, height, location.clone(), date_taken)
.await?;
let message = format!(
"Extracted EXIF: width={:?}, height={:?}, location={:?}, date_taken={:?}",
width, height, location, date_taken
);
Ok(PluginData { message })
} else {
Ok(PluginData {
message: "No EXIF width/height or GPS location found.".to_string(),
})
let mut metadata_models = Vec::new();
for (source, tag_name, tag_value) in extracted_data.all_tags {
metadata_models.push(MediaMetadata {
id: Uuid::new_v4(),
media_id: media.id,
source,
tag_name,
tag_value,
});
}
let message;
if !metadata_models.is_empty() {
context
.metadata_repo
.create_batch(&metadata_models)
.await?;
message = format!("Saved {} metadata tags.", metadata_models.len());
} else {
message = "No metadata tags found.".to_string();
}
Ok(PluginData { message })
}
}

View File

@@ -18,13 +18,8 @@ impl MediaProcessorPlugin for XmpWriterPlugin {
}
async fn process(&self, media: &Media, context: &PluginContext) -> CoreResult<PluginData> {
let fresh_media = context
.media_repo
.find_by_id(media.id)
.await?
.ok_or(CoreError::NotFound("Media".to_string(), media.id))?;
let file_path = PathBuf::from(&context.media_library_path).join(&fresh_media.storage_path);
let metadata = context.metadata_repo.find_by_media_id(media.id).await?;
let file_path = PathBuf::from(&context.media_library_path).join(&media.storage_path);
let xmp_path = format!("{}.xmp", file_path.to_string_lossy());
let mut xmp = XmpMeta::new()
@@ -33,28 +28,24 @@ impl MediaProcessorPlugin for XmpWriterPlugin {
xmp.set_property(
"http://purl.org/dc/elements/1.1/",
"description",
&fresh_media.original_filename.into(),
&XmpValue::from(media.original_filename.as_str()),
)
.map_err(|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();
if let Some(date_tag) = metadata.iter().find(|m| m.tag_name == "DateTimeOriginal") {
let date_str = &date_tag.tag_value;
xmp.set_property(
"http://ns.adobe.com/exif/1.0/",
"DateTimeOriginal",
&XmpValue::from(date_str),
&XmpValue::from(date_str.as_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
}
let xmp_str = xmp.to_string();
fs::write(&xmp_path, xmp_str).await?;