feat: Add thumbnail generation feature and update media model

- Updated workspace members to include `libertas_importer`.
- Added a new migration to add `thumbnail_path` column to `media` table.
- Enhanced configuration structures to include `thumbnail_config`.
- Modified `Media` model to include `thumbnail_path`.
- Updated `MediaRepository` trait and its implementation to handle thumbnail path updates.
- Created `ThumbnailPlugin` for generating thumbnails based on media configurations.
- Integrated thumbnail generation into the media processing workflow.
- Updated dependencies in `libertas_worker` and `libertas_importer` for image processing.
This commit is contained in:
2025-11-12 00:28:12 +01:00
parent 4f7b93a8b0
commit 60860cf508
22 changed files with 1259 additions and 22 deletions

View File

@@ -20,5 +20,6 @@ pub fn load_config() -> CoreResult<Config> {
"created_at".to_string(),
"original_filename".to_string(),
]),
thumbnail_config: None,
})
}

View File

@@ -43,6 +43,7 @@ async fn main() -> anyhow::Result<()> {
album_repo,
user_repo,
media_library_path: config.media_library_path.clone(),
config: Arc::new(config.clone()),
});
println!("Plugin context created.");

View File

@@ -5,7 +5,7 @@ use libertas_core::{
plugins::{MediaProcessorPlugin, PluginContext},
};
use crate::plugins::{exif_reader::ExifReaderPlugin, xmp_writer::XmpWriterPlugin};
use crate::plugins::{exif_reader::ExifReaderPlugin, thumbnail::ThumbnailPlugin, xmp_writer::XmpWriterPlugin};
pub struct PluginManager {
plugins: Vec<Arc<dyn MediaProcessorPlugin>>,
@@ -16,7 +16,7 @@ impl PluginManager {
let mut plugins: Vec<Arc<dyn MediaProcessorPlugin>> = Vec::new();
plugins.push(Arc::new(ExifReaderPlugin));
plugins.push(Arc::new(ThumbnailPlugin));
plugins.push(Arc::new(XmpWriterPlugin));
println!("PluginManager loaded {} plugins", plugins.len());

View File

@@ -68,7 +68,7 @@ impl MediaProcessorPlugin for ExifReaderPlugin {
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(), date_taken)
.update_exif_data(media.id, width, height, location.clone(), date_taken)
.await?;
let message = format!(

View File

@@ -1,2 +1,3 @@
pub mod exif_reader;
pub mod xmp_writer;
pub mod thumbnail;

View File

@@ -0,0 +1,71 @@
use std::path::PathBuf;
use async_trait::async_trait;
use libertas_core::{config::ThumbnailFormat, error::{CoreError, CoreResult}, models::Media, plugins::{MediaProcessorPlugin, PluginContext, PluginData}};
use tokio::fs;
pub struct ThumbnailPlugin;
#[async_trait]
impl MediaProcessorPlugin for ThumbnailPlugin {
fn name(&self) -> &'static str {
"thumbnail_generator"
}
async fn process(&self, media: &Media, context: &PluginContext) -> CoreResult<PluginData> {
let config = &context.config.thumbnail_config;
if config.is_none() {
return Ok(PluginData {
message: "Thumbnail generation is disabled in config.".to_string(),
});
}
let config = config.as_ref().unwrap();
let original_path = PathBuf::from(&context.media_library_path).join(&media.storage_path);
let extension = match config.format {
ThumbnailFormat::Jpeg => "jpg",
ThumbnailFormat::Webp => "webp",
};
let thumbnail_filename = format!("{}_thumb.{}", media.id, extension);
let thumbnail_path = PathBuf::from(&config.library_path).join(thumbnail_filename);
if let Some(parent) = thumbnail_path.parent() {
if !parent.exists() {
fs::create_dir_all(parent).await?;
}
}
if !media.mime_type.starts_with("image/") {
return Ok(PluginData {
message: "Media is not an image; skipping thumbnail generation.".to_string(),
});
}
let img = image::open(&original_path)
.map_err(|e| CoreError::Unknown(format!("Failed to open image: {}", e)))?;
let thumb = img.thumbnail(config.width, config.height);
match config.format {
ThumbnailFormat::Jpeg => {
thumb
.save_with_format(&thumbnail_path, image::ImageFormat::Jpeg)
},
ThumbnailFormat::Webp => {
thumb
.save_with_format(&thumbnail_path, image::ImageFormat::WebP)
},
}.map_err(|e| CoreError::Unknown(format!("Failed to save thumbnail: {}", e)))?;
let thumb_path_str = thumbnail_path.to_string_lossy().to_string();
context
.media_repo
.update_thumbnail_path(media.id, thumb_path_str)
.await?;
Ok(PluginData {
message: format!("Thumbnail generated at {:?}", thumbnail_path.display()),
})
}
}