feat: implement media metadata management with EXIF and TrackInfo support
This commit is contained in:
@@ -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 })
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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>>;
|
||||
}
|
||||
Reference in New Issue
Block a user