use std::collections::HashMap; use std::path::PathBuf; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; // Media #[derive(Debug, Serialize)] pub struct MediaResponse { pub id: String, pub path: String, pub file_name: String, pub media_type: String, pub content_hash: String, 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 has_thumbnail: bool, pub custom_fields: HashMap, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize)] pub struct CustomFieldResponse { pub field_type: String, pub value: String, } #[derive(Debug, Deserialize)] pub struct ImportRequest { pub path: PathBuf, } #[derive(Debug, Serialize)] pub struct ImportResponse { pub media_id: String, pub was_duplicate: bool, } #[derive(Debug, Deserialize)] pub struct UpdateMediaRequest { pub title: Option, pub artist: Option, pub album: Option, pub genre: Option, pub year: Option, pub description: Option, } // Tags #[derive(Debug, Serialize)] pub struct TagResponse { pub id: String, pub name: String, pub parent_id: Option, pub created_at: DateTime, } #[derive(Debug, Deserialize)] pub struct CreateTagRequest { pub name: String, pub parent_id: Option, } #[derive(Debug, Deserialize)] pub struct TagMediaRequest { pub tag_id: Uuid, } // Collections #[derive(Debug, Serialize)] pub struct CollectionResponse { pub id: String, pub name: String, pub description: Option, pub kind: String, pub filter_query: Option, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Deserialize)] pub struct CreateCollectionRequest { pub name: String, pub kind: String, pub description: Option, pub filter_query: Option, } #[derive(Debug, Deserialize)] pub struct AddMemberRequest { pub media_id: Uuid, pub position: Option, } // Search #[derive(Debug, Deserialize)] pub struct SearchParams { pub q: String, pub sort: Option, pub offset: Option, pub limit: Option, } #[derive(Debug, Serialize)] pub struct SearchResponse { pub items: Vec, pub total_count: u64, } // Audit #[derive(Debug, Serialize)] pub struct AuditEntryResponse { pub id: String, pub media_id: Option, pub action: String, pub details: Option, pub timestamp: DateTime, } // Search (POST body) #[derive(Debug, Deserialize)] pub struct SearchRequestBody { pub q: String, pub sort: Option, pub offset: Option, pub limit: Option, } // Scan #[derive(Debug, Deserialize)] pub struct ScanRequest { pub path: Option, } #[derive(Debug, Serialize)] pub struct ScanResponse { pub files_found: usize, pub files_processed: usize, pub errors: Vec, } #[derive(Debug, Serialize)] pub struct ScanJobResponse { pub job_id: String, } #[derive(Debug, Serialize)] pub struct ScanStatusResponse { pub scanning: bool, pub files_found: usize, pub files_processed: usize, pub error_count: usize, pub errors: Vec, } // Pagination #[derive(Debug, Deserialize)] pub struct PaginationParams { pub offset: Option, pub limit: Option, pub sort: Option, } // Open #[derive(Debug, Deserialize)] pub struct OpenRequest { pub media_id: Uuid, } // Config #[derive(Debug, Serialize)] pub struct ConfigResponse { pub backend: String, pub database_path: Option, pub roots: Vec, pub scanning: ScanningConfigResponse, pub server: ServerConfigResponse, pub ui: UiConfigResponse, pub config_path: Option, pub config_writable: bool, } #[derive(Debug, Serialize)] pub struct ScanningConfigResponse { pub watch: bool, pub poll_interval_secs: u64, pub ignore_patterns: Vec, } #[derive(Debug, Serialize)] pub struct ServerConfigResponse { pub host: String, pub port: u16, } #[derive(Debug, Deserialize)] pub struct UpdateScanningRequest { pub watch: Option, pub poll_interval_secs: Option, pub ignore_patterns: Option>, } #[derive(Debug, Deserialize)] pub struct RootDirRequest { pub path: String, } // Enhanced Import #[derive(Debug, Deserialize)] pub struct ImportWithOptionsRequest { pub path: PathBuf, pub tag_ids: Option>, pub new_tags: Option>, pub collection_id: Option, } #[derive(Debug, Deserialize)] pub struct BatchImportRequest { pub paths: Vec, pub tag_ids: Option>, pub new_tags: Option>, pub collection_id: Option, } #[derive(Debug, Serialize)] pub struct BatchImportResponse { pub results: Vec, pub total: usize, pub imported: usize, pub duplicates: usize, pub errors: usize, } #[derive(Debug, Serialize)] pub struct BatchImportItemResult { pub path: String, pub media_id: Option, pub was_duplicate: bool, pub error: Option, } #[derive(Debug, Deserialize)] pub struct DirectoryImportRequest { pub path: PathBuf, pub tag_ids: Option>, pub new_tags: Option>, pub collection_id: Option, } #[derive(Debug, Serialize)] pub struct DirectoryPreviewResponse { pub files: Vec, pub total_count: usize, pub total_size: u64, } #[derive(Debug, Serialize)] pub struct DirectoryPreviewFile { pub path: String, pub file_name: String, pub media_type: String, pub file_size: u64, } // Custom Fields #[derive(Debug, Deserialize)] pub struct SetCustomFieldRequest { pub name: String, pub field_type: String, pub value: String, } // Media update extended #[derive(Debug, Deserialize)] pub struct UpdateMediaFullRequest { pub title: Option, pub artist: Option, pub album: Option, pub genre: Option, pub year: Option, pub description: Option, } // Batch operations #[derive(Debug, Deserialize)] pub struct BatchTagRequest { pub media_ids: Vec, pub tag_ids: Vec, } #[derive(Debug, Deserialize)] pub struct BatchCollectionRequest { pub media_ids: Vec, pub collection_id: Uuid, } #[derive(Debug, Deserialize)] pub struct BatchDeleteRequest { pub media_ids: Vec, } #[derive(Debug, Deserialize)] pub struct BatchUpdateRequest { pub media_ids: Vec, pub title: Option, pub artist: Option, pub album: Option, pub genre: Option, pub year: Option, pub description: Option, } #[derive(Debug, Serialize)] pub struct BatchOperationResponse { pub processed: usize, pub errors: Vec, } // Search with sort #[derive(Debug, Serialize)] pub struct MediaCountResponse { pub count: u64, } // Database management #[derive(Debug, Serialize)] pub struct DatabaseStatsResponse { pub media_count: u64, pub tag_count: u64, pub collection_count: u64, pub audit_count: u64, pub database_size_bytes: u64, pub backend_name: String, } // UI Config #[derive(Debug, Serialize, Deserialize, Clone)] pub struct UiConfigResponse { pub theme: String, pub default_view: String, pub default_page_size: usize, pub default_view_mode: String, pub auto_play_media: bool, pub show_thumbnails: bool, pub sidebar_collapsed: bool, } #[derive(Debug, Deserialize)] pub struct UpdateUiConfigRequest { pub theme: Option, pub default_view: Option, pub default_page_size: Option, pub default_view_mode: Option, pub auto_play_media: Option, pub show_thumbnails: Option, pub sidebar_collapsed: Option, } impl From<&pinakes_core::config::UiConfig> for UiConfigResponse { fn from(ui: &pinakes_core::config::UiConfig) -> Self { Self { theme: ui.theme.clone(), default_view: ui.default_view.clone(), default_page_size: ui.default_page_size, default_view_mode: ui.default_view_mode.clone(), auto_play_media: ui.auto_play_media, show_thumbnails: ui.show_thumbnails, sidebar_collapsed: ui.sidebar_collapsed, } } } // Library Statistics #[derive(Debug, Serialize)] pub struct LibraryStatisticsResponse { pub total_media: u64, pub total_size_bytes: u64, pub avg_file_size_bytes: u64, pub media_by_type: Vec, pub storage_by_type: Vec, pub newest_item: Option, pub oldest_item: Option, pub top_tags: Vec, pub top_collections: Vec, pub total_tags: u64, pub total_collections: u64, pub total_duplicates: u64, } #[derive(Debug, Serialize)] pub struct TypeCountResponse { pub name: String, pub count: u64, } impl From for LibraryStatisticsResponse { fn from(stats: pinakes_core::storage::LibraryStatistics) -> Self { Self { total_media: stats.total_media, total_size_bytes: stats.total_size_bytes, avg_file_size_bytes: stats.avg_file_size_bytes, media_by_type: stats .media_by_type .into_iter() .map(|(name, count)| TypeCountResponse { name, count }) .collect(), storage_by_type: stats .storage_by_type .into_iter() .map(|(name, count)| TypeCountResponse { name, count }) .collect(), newest_item: stats.newest_item, oldest_item: stats.oldest_item, top_tags: stats .top_tags .into_iter() .map(|(name, count)| TypeCountResponse { name, count }) .collect(), top_collections: stats .top_collections .into_iter() .map(|(name, count)| TypeCountResponse { name, count }) .collect(), total_tags: stats.total_tags, total_collections: stats.total_collections, total_duplicates: stats.total_duplicates, } } } // Scheduled Tasks #[derive(Debug, Serialize)] pub struct ScheduledTaskResponse { pub id: String, pub name: String, pub schedule: String, pub enabled: bool, pub last_run: Option, pub next_run: Option, pub last_status: Option, } // Duplicates #[derive(Debug, Serialize)] pub struct DuplicateGroupResponse { pub content_hash: String, pub items: Vec, } // Auth #[derive(Debug, Deserialize)] pub struct LoginRequest { pub username: String, pub password: String, } #[derive(Debug, Serialize)] pub struct LoginResponse { pub token: String, pub username: String, pub role: String, } #[derive(Debug, Serialize)] pub struct UserInfoResponse { pub username: String, pub role: String, } // Conversion helpers impl From for MediaResponse { fn from(item: pinakes_core::model::MediaItem) -> Self { Self { id: item.id.0.to_string(), path: item.path.to_string_lossy().to_string(), file_name: item.file_name, media_type: serde_json::to_value(item.media_type) .ok() .and_then(|v| v.as_str().map(String::from)) .unwrap_or_default(), content_hash: item.content_hash.0, file_size: item.file_size, title: item.title, artist: item.artist, album: item.album, genre: item.genre, year: item.year, duration_secs: item.duration_secs, description: item.description, has_thumbnail: item.thumbnail_path.is_some(), custom_fields: item .custom_fields .into_iter() .map(|(k, v)| { ( k, CustomFieldResponse { field_type: format!("{:?}", v.field_type).to_lowercase(), value: v.value, }, ) }) .collect(), created_at: item.created_at, updated_at: item.updated_at, } } } impl From for TagResponse { fn from(tag: pinakes_core::model::Tag) -> Self { Self { id: tag.id.to_string(), name: tag.name, parent_id: tag.parent_id.map(|id| id.to_string()), created_at: tag.created_at, } } } impl From for CollectionResponse { fn from(col: pinakes_core::model::Collection) -> Self { Self { id: col.id.to_string(), name: col.name, description: col.description, kind: format!("{:?}", col.kind).to_lowercase(), filter_query: col.filter_query, created_at: col.created_at, updated_at: col.updated_at, } } } impl From for AuditEntryResponse { fn from(entry: pinakes_core::model::AuditEntry) -> Self { Self { id: entry.id.to_string(), media_id: entry.media_id.map(|id| id.0.to_string()), action: entry.action.to_string(), details: entry.details, timestamp: entry.timestamp, } } } // Plugins #[derive(Debug, Serialize)] pub struct PluginResponse { pub id: String, pub name: String, pub version: String, pub author: String, pub description: String, pub api_version: String, pub enabled: bool, } #[derive(Debug, Deserialize)] pub struct InstallPluginRequest { pub source: String, // URL or file path } #[derive(Debug, Deserialize)] pub struct TogglePluginRequest { pub enabled: bool, } impl PluginResponse { pub fn new(meta: pinakes_plugin_api::PluginMetadata, enabled: bool) -> Self { Self { id: meta.id, name: meta.name, version: meta.version, author: meta.author, description: meta.description, api_version: meta.api_version, enabled, } } } // Users #[derive(Debug, Serialize)] pub struct UserResponse { pub id: String, pub username: String, pub role: String, pub profile: UserProfileResponse, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize)] pub struct UserProfileResponse { pub avatar_path: Option, pub bio: Option, pub preferences: UserPreferencesResponse, } #[derive(Debug, Serialize)] pub struct UserPreferencesResponse { pub theme: Option, pub language: Option, pub default_video_quality: Option, pub auto_play: bool, } #[derive(Debug, Serialize)] pub struct UserLibraryResponse { pub user_id: String, pub root_path: String, pub permission: String, pub granted_at: DateTime, } #[derive(Debug, Deserialize)] pub struct GrantLibraryAccessRequest { pub root_path: String, pub permission: pinakes_core::users::LibraryPermission, } #[derive(Debug, Deserialize)] pub struct RevokeLibraryAccessRequest { pub root_path: String, } impl From for UserResponse { fn from(user: pinakes_core::users::User) -> Self { Self { id: user.id.0.to_string(), username: user.username, role: user.role.to_string(), profile: UserProfileResponse { avatar_path: user.profile.avatar_path, bio: user.profile.bio, preferences: UserPreferencesResponse { theme: user.profile.preferences.theme, language: user.profile.preferences.language, default_video_quality: user.profile.preferences.default_video_quality, auto_play: user.profile.preferences.auto_play, }, }, created_at: user.created_at, updated_at: user.updated_at, } } } impl From for UserLibraryResponse { fn from(access: pinakes_core::users::UserLibraryAccess) -> Self { Self { user_id: access.user_id.0.to_string(), root_path: access.root_path, permission: format!("{:?}", access.permission).to_lowercase(), granted_at: access.granted_at, } } } // ===== Social (Ratings, Comments, Favorites, Shares) ===== #[derive(Debug, Serialize)] pub struct RatingResponse { pub id: String, pub user_id: String, pub media_id: String, pub stars: u8, pub review_text: Option, pub created_at: DateTime, } impl From for RatingResponse { fn from(r: pinakes_core::social::Rating) -> Self { Self { id: r.id.to_string(), user_id: r.user_id.0.to_string(), media_id: r.media_id.0.to_string(), stars: r.stars, review_text: r.review_text, created_at: r.created_at, } } } #[derive(Debug, Deserialize)] pub struct CreateRatingRequest { pub stars: u8, pub review_text: Option, } #[derive(Debug, Serialize)] pub struct CommentResponse { pub id: String, pub user_id: String, pub media_id: String, pub parent_comment_id: Option, pub text: String, pub created_at: DateTime, } impl From for CommentResponse { fn from(c: pinakes_core::social::Comment) -> Self { Self { id: c.id.to_string(), user_id: c.user_id.0.to_string(), media_id: c.media_id.0.to_string(), parent_comment_id: c.parent_comment_id.map(|id| id.to_string()), text: c.text, created_at: c.created_at, } } } #[derive(Debug, Deserialize)] pub struct CreateCommentRequest { pub text: String, pub parent_id: Option, } #[derive(Debug, Deserialize)] pub struct FavoriteRequest { pub media_id: Uuid, } #[derive(Debug, Deserialize)] pub struct CreateShareLinkRequest { pub media_id: Uuid, pub password: Option, pub expires_in_hours: Option, } #[derive(Debug, Serialize)] pub struct ShareLinkResponse { pub id: String, pub media_id: String, pub token: String, pub expires_at: Option>, pub view_count: u64, pub created_at: DateTime, } impl From for ShareLinkResponse { fn from(s: pinakes_core::social::ShareLink) -> Self { Self { id: s.id.to_string(), media_id: s.media_id.0.to_string(), token: s.token, expires_at: s.expires_at, view_count: s.view_count, created_at: s.created_at, } } } // ===== Playlists ===== #[derive(Debug, Serialize)] pub struct PlaylistResponse { pub id: String, pub owner_id: String, pub name: String, pub description: Option, pub is_public: bool, pub is_smart: bool, pub filter_query: Option, pub created_at: DateTime, pub updated_at: DateTime, } impl From for PlaylistResponse { fn from(p: pinakes_core::playlists::Playlist) -> Self { Self { id: p.id.to_string(), owner_id: p.owner_id.0.to_string(), name: p.name, description: p.description, is_public: p.is_public, is_smart: p.is_smart, filter_query: p.filter_query, created_at: p.created_at, updated_at: p.updated_at, } } } #[derive(Debug, Deserialize)] pub struct CreatePlaylistRequest { pub name: String, pub description: Option, pub is_public: Option, pub is_smart: Option, pub filter_query: Option, } #[derive(Debug, Deserialize)] pub struct UpdatePlaylistRequest { pub name: Option, pub description: Option, pub is_public: Option, } #[derive(Debug, Deserialize)] pub struct PlaylistItemRequest { pub media_id: Uuid, pub position: Option, } #[derive(Debug, Deserialize)] pub struct ReorderPlaylistRequest { pub media_id: Uuid, pub new_position: i32, } // ===== Analytics ===== #[derive(Debug, Serialize)] pub struct UsageEventResponse { pub id: String, pub media_id: Option, pub user_id: Option, pub event_type: String, pub timestamp: DateTime, pub duration_secs: Option, } impl From for UsageEventResponse { fn from(e: pinakes_core::analytics::UsageEvent) -> Self { Self { id: e.id.to_string(), media_id: e.media_id.map(|m| m.0.to_string()), user_id: e.user_id.map(|u| u.0.to_string()), event_type: e.event_type.to_string(), timestamp: e.timestamp, duration_secs: e.duration_secs, } } } #[derive(Debug, Deserialize)] pub struct RecordUsageEventRequest { pub media_id: Option, pub event_type: String, pub duration_secs: Option, pub context: Option, } #[derive(Debug, Serialize)] pub struct MostViewedResponse { pub media: MediaResponse, pub view_count: u64, } #[derive(Debug, Deserialize)] pub struct WatchProgressRequest { pub progress_secs: f64, } #[derive(Debug, Serialize)] pub struct WatchProgressResponse { pub progress_secs: f64, } // ===== Subtitles ===== #[derive(Debug, Serialize)] pub struct SubtitleResponse { pub id: String, pub media_id: String, pub language: Option, pub format: String, pub is_embedded: bool, pub track_index: Option, pub offset_ms: i64, pub created_at: DateTime, } impl From for SubtitleResponse { fn from(s: pinakes_core::subtitles::Subtitle) -> Self { Self { id: s.id.to_string(), media_id: s.media_id.0.to_string(), language: s.language, format: s.format.to_string(), is_embedded: s.is_embedded, track_index: s.track_index, offset_ms: s.offset_ms, created_at: s.created_at, } } } #[derive(Debug, Deserialize)] pub struct AddSubtitleRequest { pub language: Option, pub format: String, pub file_path: Option, pub is_embedded: Option, pub track_index: Option, pub offset_ms: Option, } #[derive(Debug, Deserialize)] pub struct UpdateSubtitleOffsetRequest { pub offset_ms: i64, } // ===== Enrichment ===== #[derive(Debug, Serialize)] pub struct ExternalMetadataResponse { pub id: String, pub media_id: String, pub source: String, pub external_id: Option, pub metadata: serde_json::Value, pub confidence: f64, pub last_updated: DateTime, } impl From for ExternalMetadataResponse { fn from(m: pinakes_core::enrichment::ExternalMetadata) -> Self { let metadata = serde_json::from_str(&m.metadata_json).unwrap_or_else(|e| { tracing::warn!( "failed to deserialize external metadata JSON for media {}: {}", m.media_id.0, e ); serde_json::Value::Null }); Self { id: m.id.to_string(), media_id: m.media_id.0.to_string(), source: m.source.to_string(), external_id: m.external_id, metadata, confidence: m.confidence, last_updated: m.last_updated, } } } // ===== Transcode ===== #[derive(Debug, Serialize)] pub struct TranscodeSessionResponse { pub id: String, pub media_id: String, pub profile: String, pub status: String, pub progress: f32, pub created_at: DateTime, pub expires_at: Option>, } impl From for TranscodeSessionResponse { fn from(s: pinakes_core::transcode::TranscodeSession) -> Self { Self { id: s.id.to_string(), media_id: s.media_id.0.to_string(), profile: s.profile, status: s.status.as_str().to_string(), progress: s.progress, created_at: s.created_at, expires_at: s.expires_at, } } } #[derive(Debug, Deserialize)] pub struct CreateTranscodeRequest { pub profile: String, }