feat: Add media thumbnail retrieval functionality and update MediaResponse structure

This commit is contained in:
2025-11-15 17:37:39 +01:00
parent 3f96de117b
commit 8a735c7c26
4 changed files with 72 additions and 26 deletions

View File

@@ -17,7 +17,7 @@ use crate::{
error::ApiError, error::ApiError,
extractors::query_options::ApiListMediaOptions, extractors::query_options::ApiListMediaOptions,
middleware::auth::{OptionalUserId, UserId}, middleware::auth::{OptionalUserId, UserId},
schema::{MediaDetailsResponse, MediaMetadataResponse, MediaResponse}, schema::{MediaDetailsResponse, MediaResponse},
state::AppState, state::AppState,
}; };
@@ -26,6 +26,7 @@ pub fn media_routes(max_upload_size: usize) -> Router<AppState> {
.route("/", post(upload_media).get(list_user_media)) .route("/", post(upload_media).get(list_user_media))
.route("/{id}", get(get_media_details).delete(delete_media)) .route("/{id}", get(get_media_details).delete(delete_media))
.route("/{id}/file", get(get_media_file)) .route("/{id}/file", get(get_media_file))
.route("/{id}/thumbnail", get(get_media_thumbnail))
.layer(DefaultBodyLimit::max(max_upload_size)) .layer(DefaultBodyLimit::max(max_upload_size))
} }
@@ -90,26 +91,37 @@ async fn get_media_file(
}) })
} }
async fn get_media_thumbnail(
State(state): State<AppState>,
OptionalUserId(user_id): OptionalUserId,
Path(media_id): Path<Uuid>,
request: Request,
) -> Result<impl IntoResponse, ApiError> {
let thumbnail_path = state
.media_service
.get_media_thumbnail_path(media_id, user_id)
.await?;
let full_path = PathBuf::from(&thumbnail_path);
ServeFile::new(full_path)
.oneshot(request)
.await
.map_err(|e| {
ApiError::from(CoreError::Io(io::Error::new(
io::ErrorKind::NotFound,
format!("File not found: {}", e),
)))
})
}
async fn get_media_details( async fn get_media_details(
State(state): State<AppState>, State(state): State<AppState>,
OptionalUserId(user_id): OptionalUserId, OptionalUserId(user_id): OptionalUserId,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<Json<MediaDetailsResponse>, ApiError> { ) -> Result<Json<MediaDetailsResponse>, ApiError> {
let bundle = state.media_service.get_media_details(id, user_id).await?; let bundle = state.media_service.get_media_details(id, user_id).await?;
let response = MediaDetailsResponse { let response = MediaDetailsResponse::from(bundle);
id: bundle.media.id,
storage_path: bundle.media.storage_path,
original_filename: bundle.media.original_filename,
mime_type: bundle.media.mime_type,
hash: bundle.media.hash,
thumbnail_path: bundle.media.thumbnail_path,
metadata: bundle
.metadata
.into_iter()
.map(MediaMetadataResponse::from)
.collect(),
};
Ok(Json(response)) Ok(Json(response))
} }

View File

@@ -1,5 +1,6 @@
use libertas_core::models::{ use libertas_core::models::{
Album, AlbumPermission, FaceRegion, Media, MediaMetadata, Person, PersonPermission, Tag, Album, AlbumPermission, FaceRegion, Media, MediaBundle, MediaMetadata, Person,
PersonPermission, Tag,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
@@ -7,22 +8,24 @@ use uuid::Uuid;
#[derive(Serialize)] #[derive(Serialize)]
pub struct MediaResponse { pub struct MediaResponse {
pub id: uuid::Uuid, pub id: uuid::Uuid,
pub storage_path: String,
pub original_filename: String, pub original_filename: String,
pub mime_type: String, pub mime_type: String,
pub hash: String, pub hash: String,
pub thumbnail_path: Option<String>, pub file_url: String,
pub thumbnail_url: Option<String>,
} }
impl From<Media> for MediaResponse { impl From<Media> for MediaResponse {
fn from(media: Media) -> Self { fn from(media: Media) -> Self {
Self { Self {
id: media.id, id: media.id,
storage_path: media.storage_path,
original_filename: media.original_filename, original_filename: media.original_filename,
mime_type: media.mime_type, mime_type: media.mime_type,
hash: media.hash, hash: media.hash,
thumbnail_path: media.thumbnail_path, file_url: format!("/api/v1/media/{}/file", media.id),
thumbnail_url: media
.thumbnail_path
.map(|_| format!("/api/v1/media/{}/thumbnail", media.id)),
} }
} }
} }
@@ -130,15 +133,24 @@ impl From<MediaMetadata> for MediaMetadataResponse {
#[derive(Serialize)] #[derive(Serialize)]
pub struct MediaDetailsResponse { pub struct MediaDetailsResponse {
pub id: uuid::Uuid, #[serde(flatten)]
pub storage_path: String, pub media: MediaResponse,
pub original_filename: String,
pub mime_type: String,
pub hash: String,
pub thumbnail_path: Option<String>,
pub metadata: Vec<MediaMetadataResponse>, pub metadata: Vec<MediaMetadataResponse>,
} }
impl From<MediaBundle> for MediaDetailsResponse {
fn from(bundle: MediaBundle) -> Self {
Self {
media: MediaResponse::from(bundle.media),
metadata: bundle
.metadata
.into_iter()
.map(MediaMetadataResponse::from)
.collect(),
}
}
}
#[derive(Serialize)] #[derive(Serialize)]
pub struct TagResponse { pub struct TagResponse {
pub id: Uuid, pub id: Uuid,

View File

@@ -127,6 +127,26 @@ impl MediaService for MediaServiceImpl {
Ok(media.storage_path) Ok(media.storage_path)
} }
async fn get_media_thumbnail_path(
&self,
id: Uuid,
user_id: Option<Uuid>,
) -> CoreResult<String> {
self.auth_service
.check_permission(user_id, authz::Permission::ViewMedia(id))
.await?;
let media = self
.repo
.find_by_id(id)
.await?
.ok_or(CoreError::NotFound("Media".to_string(), id))?;
media
.thumbnail_path
.ok_or(CoreError::NotFound("Thumbnail for Media".to_string(), id))
}
async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()> { async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()> {
self.auth_service self.auth_service
.check_permission(Some(user_id), authz::Permission::DeleteMedia(id)) .check_permission(Some(user_id), authz::Permission::DeleteMedia(id))

View File

@@ -24,6 +24,8 @@ pub trait MediaService: Send + Sync {
options: ListMediaOptions, options: ListMediaOptions,
) -> CoreResult<Vec<Media>>; ) -> CoreResult<Vec<Media>>;
async fn get_media_filepath(&self, id: Uuid, user_id: Option<Uuid>) -> CoreResult<String>; async fn get_media_filepath(&self, id: Uuid, user_id: Option<Uuid>) -> CoreResult<String>;
async fn get_media_thumbnail_path(&self, id: Uuid, user_id: Option<Uuid>)
-> CoreResult<String>;
async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()>; async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()>;
} }