feat: Add thumbnail management for albums and people, implement face embedding functionality

This commit is contained in:
2025-11-15 22:50:53 +01:00
parent 98f56e4f1e
commit 0f3e098d6d
28 changed files with 560 additions and 26 deletions

View File

@@ -0,0 +1,2 @@
ALTER TABLE albums
ADD COLUMN thumbnail_media_id UUID REFERENCES media(id) ON DELETE SET NULL;

View File

@@ -0,0 +1,2 @@
ALTER TABLE people
ADD COLUMN thumbnail_media_id UUID REFERENCES media(id) ON DELETE SET NULL;

View File

@@ -0,0 +1,7 @@
CREATE TABLE face_embeddings (
id UUID PRIMARY KEY,
face_region_id UUID NOT NULL REFERENCES face_regions(id) ON DELETE CASCADE,
model_id SMALLINT NOT NULL,
embedding BYTEA NOT NULL
);
CREATE UNIQUE INDEX idx_face_embeddings_region_id ON face_embeddings (face_region_id);

View File

@@ -2,12 +2,22 @@ use axum::{
Json, Router,
extract::{Path, State},
http::StatusCode,
routing::{get, post},
routing::{get, post, put},
};
use libertas_core::schema::{
AddMediaToAlbumData, CreateAlbumData, ShareAlbumData, UpdateAlbumData,
};
use libertas_core::schema::{AddMediaToAlbumData, CreateAlbumData, ShareAlbumData, UpdateAlbumData};
use uuid::Uuid;
use crate::{error::ApiError, middleware::auth::UserId, schema::{AddMediaToAlbumRequest, AlbumResponse, CreateAlbumRequest, ShareAlbumRequest, UpdateAlbumRequest}, state::AppState};
use crate::{
error::ApiError,
middleware::auth::UserId,
schema::{
AddMediaToAlbumRequest, AlbumResponse, CreateAlbumRequest, SetThumbnailRequest,
ShareAlbumRequest, UpdateAlbumRequest,
},
state::AppState,
};
async fn create_album(
State(state): State<AppState>,
@@ -110,6 +120,19 @@ async fn delete_album(
Ok(StatusCode::NO_CONTENT)
}
async fn set_album_thumbnail(
State(state): State<AppState>,
UserId(user_id): UserId,
Path(album_id): Path<Uuid>,
Json(payload): Json<SetThumbnailRequest>,
) -> Result<StatusCode, ApiError> {
state
.album_service
.set_album_thumbnail(album_id, payload.media_id, user_id)
.await?;
Ok(StatusCode::OK)
}
pub fn album_routes() -> Router<AppState> {
Router::new()
.route("/", post(create_album).get(list_user_albums))
@@ -119,6 +142,7 @@ pub fn album_routes() -> Router<AppState> {
.put(update_album)
.delete(delete_album),
)
.route("/{id}/thumbnail", put(set_album_thumbnail))
.route("/{id}/media", post(add_media_to_album))
.route("/{id}/share", post(share_album))
}

View File

@@ -13,7 +13,7 @@ use crate::{
middleware::auth::UserId,
schema::{
AssignFaceRequest, CreatePersonRequest, FaceRegionResponse, MergePersonRequest,
PersonResponse, SharePersonRequest, UpdatePersonRequest,
PersonResponse, SetPersonThumbnailRequest, SharePersonRequest, UpdatePersonRequest,
},
state::AppState,
};
@@ -30,6 +30,7 @@ pub fn people_routes() -> Router<AppState> {
post(share_person).delete(unshare_person),
)
.route("/{person_id}/merge", post(merge_person))
.route("/{person_id}/thumbnail", put(set_person_thumbnail))
}
pub fn face_routes() -> Router<AppState> {
@@ -162,3 +163,16 @@ async fn merge_person(
.await?;
Ok(StatusCode::NO_CONTENT)
}
async fn set_person_thumbnail(
State(state): State<AppState>,
UserId(user_id): UserId,
Path(person_id): Path<Uuid>,
Json(payload): Json<SetPersonThumbnailRequest>,
) -> Result<StatusCode, ApiError> {
state
.person_service
.set_person_thumbnail(person_id, payload.face_region_id, user_id)
.await?;
Ok(StatusCode::OK)
}

View File

@@ -279,3 +279,13 @@ pub struct ServeFileQuery {
#[serde(default)]
pub strip: bool,
}
#[derive(Deserialize)]
pub struct SetThumbnailRequest {
pub media_id: Uuid,
}
#[derive(Deserialize)]
pub struct SetPersonThumbnailRequest {
pub face_region_id: Uuid,
}

View File

@@ -170,4 +170,22 @@ impl AlbumService for AlbumServiceImpl {
let media = self.album_repo.list_media_by_album_id(album_id).await?;
Ok(PublicAlbumBundle { album, media })
}
async fn set_album_thumbnail(
&self,
album_id: Uuid,
media_id: Uuid,
user_id: Uuid,
) -> CoreResult<()> {
self.auth_service
.check_permission(Some(user_id), Permission::EditAlbum(album_id))
.await?;
self.auth_service
.check_permission(Some(user_id), Permission::ViewMedia(media_id))
.await?;
self.album_repo
.set_thumbnail_media_id(album_id, media_id)
.await
}
}

View File

@@ -120,7 +120,6 @@ impl AuthorizationService for AuthorizationServiceImpl {
if let Some(ref user) = user {
if authz::is_admin(user) {
// [cite: 115]
return Ok(());
}
}
@@ -135,7 +134,6 @@ impl AuthorizationService for AuthorizationServiceImpl {
if let Some(id) = user_id {
if authz::is_owner(id, &media) {
// [cite: 117]
return Ok(());
}
@@ -144,7 +142,6 @@ impl AuthorizationService for AuthorizationServiceImpl {
.is_media_in_shared_album(media_id, id)
.await?
{
// [cite: 118-119]
return Ok(());
}
}

View File

@@ -215,4 +215,34 @@ impl PersonService for PersonServiceImpl {
self.person_repo.delete(source_person_id).await
}
async fn set_person_thumbnail(
&self,
person_id: Uuid,
face_region_id: Uuid,
user_id: Uuid,
) -> CoreResult<()> {
self.auth_service
.check_permission(Some(user_id), authz::Permission::EditPerson(person_id))
.await?;
let face_region =
self.face_repo
.find_by_id(face_region_id)
.await?
.ok_or(CoreError::NotFound(
"FaceRegion".to_string(),
face_region_id,
))?;
if face_region.person_id != Some(person_id) {
return Err(CoreError::Validation(
"FaceRegion does not belong to the specified person".to_string(),
));
}
self.person_repo
.set_thumbnail_media_id(person_id, face_region.media_id)
.await
}
}