Files
k-photos/crates/domain/src/catalog/entities.rs
Gabriel Kaszewski 7b5bb66b37 feat: frontend-ready backend — pagination, auto-derivatives, list endpoints, bulk ops, OpenAPI
Pagination: count_by_owner + count_search on AssetRepository,
timeline/search return real total count (not page len).

Auto-derivatives: worker enqueues GenerateDerivative when
ExtractMetadata job completes, closing the upload→thumbnail gap.

List endpoints: GET /albums, GET /stacks with user scoping.
ListAlbumsHandler, ListStacksHandler, find_by_owner on AssetStackRepository.

Tag filtering: tag_name field on AssetFilters, JOIN asset_tags+tags
in postgres search/count queries.

Bulk operations: POST /assets/bulk-delete, POST /assets/bulk-tag.

Album update: PUT /albums/{id} with UpdateAlbumHandler (title, description).

OpenAPI: utoipa annotations on all 47 endpoints + all request/response
schemas registered. Scalar UI at /scalar covers full API.
2026-05-31 23:06:25 +02:00

280 lines
7.1 KiB
Rust

use crate::common::errors::DomainError;
use crate::common::value_objects::{Checksum, DateTimeStamp, StructuredData, SystemId};
// --- Asset ---
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum AssetType {
Image,
Video,
LivePhoto,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct SourceReference {
pub volume_id: SystemId,
pub relative_path: String,
pub checksum: Checksum,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Asset {
pub asset_id: SystemId,
pub source_reference: SourceReference,
pub asset_type: AssetType,
pub mime_type: String,
pub file_size: u64,
pub is_processed: bool,
pub owner_user_id: SystemId,
pub created_at: DateTimeStamp,
}
impl Asset {
pub fn new(
source_reference: SourceReference,
asset_type: AssetType,
mime_type: impl Into<String>,
file_size: u64,
owner: SystemId,
) -> Self {
Self {
asset_id: SystemId::new(),
source_reference,
asset_type,
mime_type: mime_type.into(),
file_size,
is_processed: false,
owner_user_id: owner,
created_at: DateTimeStamp::now(),
}
}
pub fn mark_processed(&mut self) {
self.is_processed = true;
}
}
// --- AssetFilters ---
#[derive(Default)]
pub struct AssetFilters {
pub asset_type: Option<AssetType>,
pub mime_type: Option<String>,
pub date_from: Option<DateTimeStamp>,
pub date_to: Option<DateTimeStamp>,
pub is_processed: Option<bool>,
pub tag_name: Option<String>,
}
// --- AssetMetadata ---
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub enum MetadataSource {
ExifExtracted,
AiGenerated,
UserEdited,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AssetMetadata {
pub asset_id: SystemId,
pub metadata_source: MetadataSource,
pub data: StructuredData,
pub updated_at: DateTimeStamp,
}
impl AssetMetadata {
pub fn new(asset_id: SystemId, source: MetadataSource, data: StructuredData) -> Self {
Self {
asset_id,
metadata_source: source,
data,
updated_at: DateTimeStamp::now(),
}
}
}
// --- AssetStack ---
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum StackType {
LivePhoto,
FormatPair,
BurstSequence,
ExposureBracket,
ManualGroup,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum StackMemberRole {
PrimaryDisplay,
HighResSource,
MotionClip,
AlternateFrame,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AssetStackMember {
pub asset_id: SystemId,
pub role: StackMemberRole,
pub sort_order: u32,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AssetStack {
pub stack_id: SystemId,
pub stack_type: StackType,
pub primary_asset_id: SystemId,
pub owner_user_id: SystemId,
pub members: Vec<AssetStackMember>,
}
impl AssetStack {
pub fn new(stack_type: StackType, primary_asset_id: SystemId, owner: SystemId) -> Self {
let primary_member = AssetStackMember {
asset_id: primary_asset_id,
role: StackMemberRole::PrimaryDisplay,
sort_order: 0,
};
Self {
stack_id: SystemId::new(),
stack_type,
primary_asset_id,
owner_user_id: owner,
members: vec![primary_member],
}
}
pub fn add_member(
&mut self,
asset_id: SystemId,
role: StackMemberRole,
) -> Result<(), DomainError> {
if self.members.iter().any(|m| m.asset_id == asset_id) {
return Err(DomainError::Conflict(
"Asset already exists in stack".to_string(),
));
}
let next_order = self.members.iter().map(|m| m.sort_order).max().unwrap_or(0) + 1;
self.members.push(AssetStackMember {
asset_id,
role,
sort_order: next_order,
});
Ok(())
}
}
// --- DerivativeAsset ---
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum DerivativeProfile {
ThumbnailSquare,
ThumbnailLarge,
WebOptimized,
VideoSd,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum GenerationStatus {
Pending,
Ready,
Failed,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DerivativeAsset {
pub derivative_id: SystemId,
pub parent_asset_id: SystemId,
pub profile_type: DerivativeProfile,
pub storage_path: String,
pub mime_type: String,
pub file_size: u64,
pub dimensions: (u32, u32),
pub generation_status: GenerationStatus,
}
impl DerivativeAsset {
pub fn new_pending(
parent: SystemId,
profile: DerivativeProfile,
path: impl Into<String>,
) -> Self {
Self {
derivative_id: SystemId::new(),
parent_asset_id: parent,
profile_type: profile,
storage_path: path.into(),
mime_type: String::new(),
file_size: 0,
dimensions: (0, 0),
generation_status: GenerationStatus::Pending,
}
}
pub fn mark_ready(&mut self, mime_type: impl Into<String>, size: u64, dims: (u32, u32)) {
self.mime_type = mime_type.into();
self.file_size = size;
self.dimensions = dims;
self.generation_status = GenerationStatus::Ready;
}
pub fn mark_failed(&mut self) {
self.generation_status = GenerationStatus::Failed;
}
}
// --- Duplicate ---
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum DetectionMethod {
ExactHash,
PerceptualHash,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum DuplicateStatus {
Unresolved,
Resolved,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DuplicateCandidate {
pub asset_id: SystemId,
pub similarity_score: f64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DuplicateGroup {
pub group_id: SystemId,
pub detection_method: DetectionMethod,
pub status: DuplicateStatus,
pub candidates: Vec<DuplicateCandidate>,
}
impl DuplicateGroup {
pub fn new_exact(asset_a: SystemId, asset_b: SystemId) -> Self {
Self {
group_id: SystemId::new(),
detection_method: DetectionMethod::ExactHash,
status: DuplicateStatus::Unresolved,
candidates: vec![
DuplicateCandidate {
asset_id: asset_a,
similarity_score: 1.0,
},
DuplicateCandidate {
asset_id: asset_b,
similarity_score: 1.0,
},
],
}
}
pub fn resolve(&mut self) {
self.status = DuplicateStatus::Resolved;
}
}