//! Configuration endpoints. use crate::config::{ AlignmentConfig, BrightnessConfig, FaceResolutionConfig, OutputConfig, ProcessingConfig, VideoConfig, }; use crate::web::state::AppState; use axum::{extract::State, http::StatusCode, response::Json}; use serde::{Deserialize, Serialize}; /// Configuration response (excludes sensitive API credentials). #[derive(Serialize)] pub struct ConfigResponse { pub processing: ProcessingConfig, pub video: VideoConfig, } /// Get current configuration (excluding sensitive data). pub async fn get_config(State(state): State) -> Json { let config = state.config.read().await; Json(ConfigResponse { processing: config.processing.clone(), video: config.video.clone(), }) } /// Configuration update request. /// /// Uses the same nested structure as the config itself for consistency. #[derive(Deserialize)] pub struct ConfigUpdateRequest { pub processing: Option, pub video: Option, } /// Processing configuration update fields. #[derive(Deserialize)] pub struct ProcessingConfigUpdate { pub max_workers: Option, pub face_resolution: Option, pub brightness: Option, pub output: Option, pub alignment: Option, } /// Video configuration update fields. #[derive(Deserialize)] pub struct VideoConfigUpdate { pub enabled: Option, pub framerate: Option, pub codec: Option, pub crf: Option, } /// Validation error with field name and message. struct ValidationError { field: &'static str, message: String, } impl ValidationError { fn new(field: &'static str, message: impl Into) -> Self { Self { field, message: message.into(), } } } /// Validate processing configuration update values. fn validate_processing_config(proc: &ProcessingConfigUpdate) -> Result<(), ValidationError> { if let Some(v) = proc.max_workers { if v == 0 || v > 64 { return Err(ValidationError::new( "processing.max_workers", format!("must be between 1 and 64, got {}", v), )); } } if let Some(ref fr) = proc.face_resolution { if fr.enabled && fr.min_size == 0 { return Err(ValidationError::new( "processing.face_resolution.min_size", "must be greater than 0 when enabled", )); } if fr.min_size > 1000 { return Err(ValidationError::new( "processing.face_resolution.min_size", format!("must be at most 1000, got {}", fr.min_size), )); } } if let Some(ref br) = proc.brightness { if br.enabled { if br.min_brightness < 0.0 || br.min_brightness > 1.0 { return Err(ValidationError::new( "processing.brightness.min_brightness", format!("must be between 0.0 and 1.0, got {}", br.min_brightness), )); } if br.max_brightness < 0.0 || br.max_brightness > 1.0 { return Err(ValidationError::new( "processing.brightness.max_brightness", format!("must be between 0.0 and 1.0, got {}", br.max_brightness), )); } if br.min_brightness >= br.max_brightness { return Err(ValidationError::new( "processing.brightness", "min_brightness must be less than max_brightness", )); } } } if let Some(ref out) = proc.output { if out.size < 64 { return Err(ValidationError::new( "processing.output.size", format!("must be at least 64, got {}", out.size), )); } if out.size > 4096 { return Err(ValidationError::new( "processing.output.size", format!("must be at most 4096, got {}", out.size), )); } } Ok(()) } /// Validate video configuration update values. fn validate_video_config(vid: &VideoConfigUpdate) -> Result<(), ValidationError> { if let Some(v) = vid.framerate { if v == 0 || v > 120 { return Err(ValidationError::new( "video.framerate", format!("must be between 1 and 120, got {}", v), )); } } if let Some(v) = vid.crf { if v > 51 { return Err(ValidationError::new( "video.crf", format!("must be between 0 and 51, got {}", v), )); } } if let Some(ref v) = vid.codec { let valid_codecs = ["libx264", "libx265", "libvpx", "libvpx-vp9", "libaom-av1"]; if !valid_codecs.contains(&v.as_str()) { return Err(ValidationError::new( "video.codec", format!( "must be one of: {}, got '{}'", valid_codecs.join(", "), v ), )); } } Ok(()) } /// Update configuration. pub async fn update_config( State(state): State, Json(update): Json, ) -> Result, (StatusCode, String)> { state .ensure_no_job_running() .await .map_err(|e| (StatusCode::CONFLICT, e.to_string()))?; // Validate input before updating if let Some(ref proc) = update.processing { validate_processing_config(proc).map_err(|e| { ( StatusCode::BAD_REQUEST, format!("Invalid {}: {}", e.field, e.message), ) })?; } if let Some(ref vid) = update.video { validate_video_config(vid).map_err(|e| { ( StatusCode::BAD_REQUEST, format!("Invalid {}: {}", e.field, e.message), ) })?; } // Update config { let mut config = state.config.write().await; if let Some(proc) = update.processing { if let Some(v) = proc.max_workers { config.processing.max_workers = v; } if let Some(v) = proc.face_resolution { config.processing.face_resolution = v; } if let Some(v) = proc.brightness { config.processing.brightness = v; } if let Some(v) = proc.output { config.processing.output = v; } if let Some(v) = proc.alignment { config.processing.alignment = v; } } if let Some(vid) = update.video { if let Some(v) = vid.enabled { config.video.enabled = v; } if let Some(v) = vid.framerate { config.video.framerate = v; } if let Some(v) = vid.codec { config.video.codec = v; } if let Some(v) = vid.crf { config.video.crf = v; } } // Persist to file (non-blocking, best-effort) if let Err(e) = config.save_to_file("config.toml") { tracing::warn!("Failed to persist config to file: {}", e); } else { tracing::info!("Configuration updated and persisted to config.toml"); } } // Return updated config Ok(get_config(State(state)).await) }