use std::collections::HashMap; use std::fmt; use std::path::PathBuf; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::media_type::MediaType; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct MediaId(pub Uuid); impl MediaId { pub fn new() -> Self { Self(Uuid::now_v7()) } } impl fmt::Display for MediaId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } impl Default for MediaId { fn default() -> Self { Self::new() } } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ContentHash(pub String); impl ContentHash { pub fn new(hex: String) -> Self { Self(hex) } } impl fmt::Display for ContentHash { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MediaItem { pub id: MediaId, pub path: PathBuf, pub file_name: String, pub media_type: MediaType, pub content_hash: ContentHash, pub file_size: u64, pub title: Option, pub artist: Option, pub album: Option, pub genre: Option, pub year: Option, pub duration_secs: Option, pub description: Option, pub thumbnail_path: Option, pub custom_fields: HashMap, /// File modification time (Unix timestamp in seconds), used for incremental scanning pub file_mtime: Option, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CustomField { pub field_type: CustomFieldType, pub value: String, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum CustomFieldType { Text, Number, Date, Boolean, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Tag { pub id: Uuid, pub name: String, pub parent_id: Option, pub created_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Collection { pub id: Uuid, pub name: String, pub description: Option, pub kind: CollectionKind, pub filter_query: Option, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum CollectionKind { Manual, Virtual, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CollectionMember { pub collection_id: Uuid, pub media_id: MediaId, pub position: i32, pub added_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuditEntry { pub id: Uuid, pub media_id: Option, pub action: AuditAction, pub details: Option, pub timestamp: DateTime, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum AuditAction { // Media actions Imported, Updated, Deleted, Tagged, Untagged, AddedToCollection, RemovedFromCollection, Opened, Scanned, // Authentication actions LoginSuccess, LoginFailed, Logout, SessionExpired, // Authorization actions PermissionDenied, RoleChanged, LibraryAccessGranted, LibraryAccessRevoked, // User management UserCreated, UserUpdated, UserDeleted, // Plugin actions PluginInstalled, PluginUninstalled, PluginEnabled, PluginDisabled, // Configuration actions ConfigChanged, RootDirectoryAdded, RootDirectoryRemoved, // Social/Sharing actions ShareLinkCreated, ShareLinkAccessed, // System actions DatabaseVacuumed, DatabaseCleared, ExportCompleted, IntegrityCheckCompleted, } impl fmt::Display for AuditAction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { // Media actions Self::Imported => "imported", Self::Updated => "updated", Self::Deleted => "deleted", Self::Tagged => "tagged", Self::Untagged => "untagged", Self::AddedToCollection => "added_to_collection", Self::RemovedFromCollection => "removed_from_collection", Self::Opened => "opened", Self::Scanned => "scanned", // Authentication actions Self::LoginSuccess => "login_success", Self::LoginFailed => "login_failed", Self::Logout => "logout", Self::SessionExpired => "session_expired", // Authorization actions Self::PermissionDenied => "permission_denied", Self::RoleChanged => "role_changed", Self::LibraryAccessGranted => "library_access_granted", Self::LibraryAccessRevoked => "library_access_revoked", // User management Self::UserCreated => "user_created", Self::UserUpdated => "user_updated", Self::UserDeleted => "user_deleted", // Plugin actions Self::PluginInstalled => "plugin_installed", Self::PluginUninstalled => "plugin_uninstalled", Self::PluginEnabled => "plugin_enabled", Self::PluginDisabled => "plugin_disabled", // Configuration actions Self::ConfigChanged => "config_changed", Self::RootDirectoryAdded => "root_directory_added", Self::RootDirectoryRemoved => "root_directory_removed", // Social/Sharing actions Self::ShareLinkCreated => "share_link_created", Self::ShareLinkAccessed => "share_link_accessed", // System actions Self::DatabaseVacuumed => "database_vacuumed", Self::DatabaseCleared => "database_cleared", Self::ExportCompleted => "export_completed", Self::IntegrityCheckCompleted => "integrity_check_completed", }; write!(f, "{s}") } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Pagination { pub offset: u64, pub limit: u64, pub sort: Option, } impl Pagination { pub fn new(offset: u64, limit: u64, sort: Option) -> Self { Self { offset, limit, sort, } } } impl Default for Pagination { fn default() -> Self { Self { offset: 0, limit: 50, sort: None, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SavedSearch { pub id: Uuid, pub name: String, pub query: String, pub sort_order: Option, pub created_at: DateTime, } // Book Management Types #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BookMetadata { pub media_id: MediaId, pub isbn: Option, pub isbn13: Option, pub publisher: Option, pub language: Option, pub page_count: Option, pub publication_date: Option, pub series_name: Option, pub series_index: Option, pub format: Option, pub authors: Vec, pub identifiers: HashMap>, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct AuthorInfo { pub name: String, pub role: String, pub file_as: Option, pub position: i32, } impl AuthorInfo { pub fn new(name: String) -> Self { Self { name, role: "author".to_string(), file_as: None, position: 0, } } pub fn with_role(mut self, role: String) -> Self { self.role = role; self } pub fn with_file_as(mut self, file_as: String) -> Self { self.file_as = Some(file_as); self } pub fn with_position(mut self, position: i32) -> Self { self.position = position; self } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReadingProgress { pub media_id: MediaId, pub user_id: Uuid, pub current_page: i32, pub total_pages: Option, pub progress_percent: f64, pub last_read_at: DateTime, } impl ReadingProgress { pub fn new( media_id: MediaId, user_id: Uuid, current_page: i32, total_pages: Option, ) -> Self { let progress_percent = if let Some(total) = total_pages { if total > 0 { (current_page as f64 / total as f64 * 100.0).min(100.0) } else { 0.0 } } else { 0.0 }; Self { media_id, user_id, current_page, total_pages, progress_percent, last_read_at: Utc::now(), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ReadingStatus { ToRead, Reading, Completed, Abandoned, } impl fmt::Display for ReadingStatus { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::ToRead => write!(f, "to_read"), Self::Reading => write!(f, "reading"), Self::Completed => write!(f, "completed"), Self::Abandoned => write!(f, "abandoned"), } } }