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

@@ -1,71 +1,77 @@
use std::path::Path;
use chrono::{DateTime, NaiveDateTime, Utc};
use nom_exif::{AsyncMediaParser, AsyncMediaSource, Exif, ExifIter, ExifTag};
use nom_exif::{AsyncMediaParser, AsyncMediaSource, ExifIter, TrackInfo};
use crate::error::{CoreError, CoreResult};
use crate::{error::{CoreError, CoreResult}, models::MediaMetadataSource};
#[derive(Default, Debug)]
pub struct ExtractedExif {
pub width: Option<i32>,
pub height: Option<i32>,
pub location: Option<String>,
pub date_taken: Option<DateTime<Utc>>,
pub all_tags: Vec<(MediaMetadataSource, String, String)>,
}
fn parse_exif_datetime(s: &str) -> Option<DateTime<Utc>> {
pub fn parse_exif_datetime(s: &str) -> Option<DateTime<Utc>> {
NaiveDateTime::parse_from_str(s, "%Y:%m:%d %H:%M:%S")
.ok()
.map(|ndt| ndt.and_local_timezone(Utc).unwrap())
}
pub async fn extract_exif_data(file_path: &Path) -> CoreResult<ExtractedExif> {
let ms = AsyncMediaSource::file_path(file_path)
let ms = AsyncMediaSource::file_path(file_path)
.await
.map_err(|e| CoreError::Unknown(format!("Failed to open file for EXIF: {}", e)))?;
if !ms.has_exif() {
return Ok(ExtractedExif::default());
}
let mut parser = AsyncMediaParser::new();
let iter: ExifIter = match parser.parse(ms).await {
Ok(iter) => iter,
Err(e) => {
println!("Could not parse EXIF: {}", e);
return Ok(ExtractedExif::default());
let all_tags = if ms.has_exif() {
let iter: ExifIter = match parser.parse(ms).await {
Ok(iter) => iter,
Err(e) => {
println!("Could not parse EXIF: {}", e);
return Ok(ExtractedExif::default());
}
};
iter.into_iter()
.filter_map(|mut x| {
let res = x.take_result();
match res {
Ok(v) => Some((
MediaMetadataSource::Exif,
x.tag()
.map(|t| t.to_string())
.unwrap_or_else(|| format!("Unknown(0x{:04x})", x.tag_code())),
v.to_string(),
)),
Err(e) => {
println!(
" !! EXIF parsing error for tag 0x{:04x}: {}",
x.tag_code(),
e
);
None
}
}
})
.collect::<Vec<_>>()
} else {
match parser.parse::<_, _, TrackInfo>(ms).await {
Ok(info) => info
.into_iter()
.map(|x| {
(
MediaMetadataSource::TrackInfo,
x.0.to_string(),
x.1.to_string(),
)
})
.collect::<Vec<_>>(),
Err(e) => {
println!("Could not parse TrackInfo: {}", e);
return Ok(ExtractedExif::default());
}
}
};
let location = iter.parse_gps_info().ok().flatten().map(|g| g.format_iso6709());
let exif: Exif = iter.into();
let width = exif
.get(ExifTag::ExifImageWidth)
.and_then(|f| f.as_u32())
.map(|v| v as i32);
let height = exif
.get(ExifTag::ExifImageHeight)
.and_then(|f| f.as_u32())
.map(|v| v as i32);
let dt_original = exif
.get(ExifTag::DateTimeOriginal)
.and_then(|f| f.as_str())
.and_then(parse_exif_datetime);
let dt_modify = exif
.get(ExifTag::ModifyDate)
.and_then(|f| f.as_str())
.and_then(parse_exif_datetime);
let date_taken = dt_original.or(dt_modify);
Ok(ExtractedExif {
width,
height,
location,
date_taken,
})
Ok(ExtractedExif { all_tags })
}

View File

@@ -25,6 +25,30 @@ impl From<&str> for Role {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MediaMetadataSource {
Exif,
TrackInfo,
}
impl MediaMetadataSource {
pub fn as_str(&self) -> &'static str {
match self {
MediaMetadataSource::Exif => "exif",
MediaMetadataSource::TrackInfo => "track_info",
}
}
}
impl From<&str> for MediaMetadataSource {
fn from(s: &str) -> Self {
match s {
"track_info" => MediaMetadataSource::TrackInfo,
_ => MediaMetadataSource::Exif,
}
}
}
pub struct Media {
pub id: uuid::Uuid,
pub owner_id: uuid::Uuid,
@@ -33,13 +57,17 @@ pub struct Media {
pub mime_type: String,
pub hash: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub extracted_location: Option<String>,
pub width: Option<i32>,
pub height: Option<i32>,
pub date_taken: Option<chrono::DateTime<chrono::Utc>>,
pub thumbnail_path: Option<String>,
}
pub struct MediaMetadata {
pub id: uuid::Uuid,
pub media_id: uuid::Uuid,
pub source: MediaMetadataSource,
pub tag_name: String,
pub tag_value: String,
}
#[derive(Clone)]
pub struct User {
pub id: uuid::Uuid,

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use async_trait::async_trait;
use crate::{
config::Config, error::CoreResult, models::Media, repositories::{AlbumRepository, MediaRepository, UserRepository}
config::Config, error::CoreResult, models::Media, repositories::{AlbumRepository, MediaMetadataRepository, MediaRepository, UserRepository}
};
pub struct PluginData {
@@ -14,6 +14,7 @@ pub struct PluginContext {
pub media_repo: Arc<dyn MediaRepository>,
pub album_repo: Arc<dyn AlbumRepository>,
pub user_repo: Arc<dyn UserRepository>,
pub metadata_repo: Arc<dyn MediaMetadataRepository>,
pub media_library_path: String,
pub config: Arc<Config>,
}

View File

@@ -3,7 +3,7 @@ use uuid::Uuid;
use crate::{
error::CoreResult,
models::{Album, AlbumPermission, Media, User}, schema::ListMediaOptions,
models::{Album, AlbumPermission, Media, MediaMetadata, User}, schema::ListMediaOptions,
};
#[async_trait]
@@ -12,14 +12,6 @@ pub trait MediaRepository: Send + Sync {
async fn create(&self, media: &Media) -> CoreResult<()>;
async fn find_by_id(&self, id: Uuid) -> CoreResult<Option<Media>>;
async fn list_by_user(&self, user_id: Uuid, options: &ListMediaOptions) -> CoreResult<Vec<Media>>;
async fn update_exif_data(
&self,
id: Uuid,
width: Option<i32>,
height: Option<i32>,
location: Option<String>,
date_taken: Option<chrono::DateTime<chrono::Utc>>,
) -> CoreResult<()>;
async fn update_thumbnail_path(&self, id: Uuid, thumbnail_path: String) -> CoreResult<()>;
async fn delete(&self, id: Uuid) -> CoreResult<()>;
}
@@ -60,3 +52,9 @@ pub trait AlbumShareRepository: Send + Sync {
async fn is_media_in_shared_album(&self, media_id: Uuid, user_id: Uuid) -> CoreResult<bool>;
}
#[async_trait]
pub trait MediaMetadataRepository: Send + Sync {
async fn create_batch(&self, metadata: &[MediaMetadata]) -> CoreResult<()>;
async fn find_by_media_id(&self, media_id: Uuid) -> CoreResult<Vec<MediaMetadata>>;
}