1use crate::{Result, error::SubXError};
8use log::debug;
9use serde::{Deserialize, Serialize};
10use std::path::PathBuf;
11
12use config::{Config as ConfigBuilder, Environment, File};
14
15#[derive(Debug, Serialize, Deserialize, Clone, Default)]
20pub struct Config {
21 pub ai: AIConfig,
23 pub formats: FormatsConfig,
25 pub sync: SyncConfig,
27 pub general: GeneralConfig,
29 pub parallel: ParallelConfig,
31 pub loaded_from: Option<PathBuf>,
33}
34
35impl Config {
36 pub fn config_file_path() -> Result<PathBuf> {
49 if let Ok(custom) = std::env::var("SUBX_CONFIG_PATH") {
51 let path = PathBuf::from(custom);
52 return Ok(path);
53 }
54
55 let config_dir = dirs::config_dir()
57 .ok_or_else(|| SubXError::config("Unable to determine config directory"))?;
58 let default_path = config_dir.join("subx").join("config.toml");
59 Ok(default_path)
60 }
61
62 pub fn save_to_file(&self, path: &PathBuf) -> Result<()> {
72 let toml_content = toml::to_string_pretty(self)
73 .map_err(|e| SubXError::config(format!("TOML serialization error: {}", e)))?;
74
75 if let Some(parent) = path.parent() {
77 std::fs::create_dir_all(parent).map_err(|e| {
78 SubXError::config(format!("Failed to create config directory: {}", e))
79 })?;
80 }
81
82 std::fs::write(path, toml_content)
83 .map_err(|e| SubXError::config(format!("Failed to write config file: {}", e)))?;
84
85 Ok(())
86 }
87
88 pub fn save(&self) -> Result<()> {
94 let config_path = Self::config_file_path()?;
95 self.save_to_file(&config_path)
96 }
97
98 pub fn get_value(&self, key: &str) -> Result<String> {
108 let parts: Vec<&str> = key.split('.').collect();
109 match parts.as_slice() {
110 ["ai", "provider"] => Ok(self.ai.provider.clone()),
111 ["ai", "model"] => Ok(self.ai.model.clone()),
112 ["ai", "api_key"] => Ok(self.ai.api_key.clone().unwrap_or_default()),
113 ["formats", "default_output"] => Ok(self.formats.default_output.clone()),
114 ["sync", "max_offset_seconds"] => Ok(self.sync.max_offset_seconds.to_string()),
115 ["sync", "correlation_threshold"] => Ok(self.sync.correlation_threshold.to_string()),
116 ["general", "backup_enabled"] => Ok(self.general.backup_enabled.to_string()),
117 ["parallel", "max_workers"] => Ok(self.parallel.max_workers.to_string()),
118 _ => Err(SubXError::config(format!(
119 "Unknown configuration key: {}",
120 key
121 ))),
122 }
123 }
124
125 pub fn create_config_from_sources() -> Result<Self> {
139 let config_path = Self::config_file_path()?;
140
141 let settings = ConfigBuilder::builder()
142 .add_source(ConfigBuilder::try_from(&Self::default())?)
144 .add_source(File::from(config_path).required(false))
146 .add_source(Environment::with_prefix("SUBX").separator("_"))
148 .build()
149 .map_err(|e| {
150 debug!("create_config_from_sources: Config build failed: {}", e);
151 SubXError::config(format!("Configuration build failed: {}", e))
152 })?;
153
154 let config: Self = settings.try_deserialize().map_err(|e| {
155 debug!("create_config_from_sources: Deserialization failed: {}", e);
156 SubXError::config(format!("Configuration deserialization failed: {}", e))
157 })?;
158
159 debug!("create_config_from_sources: Configuration loaded successfully");
160 Ok(config)
161 }
162}
163
164#[derive(Debug, Serialize, Deserialize, Clone)]
166pub struct AIConfig {
167 pub provider: String,
169 pub api_key: Option<String>,
171 pub model: String,
173 pub base_url: String,
175 pub max_sample_length: usize,
177 pub temperature: f32,
179 pub retry_attempts: u32,
181 pub retry_delay_ms: u64,
183}
184
185impl Default for AIConfig {
186 fn default() -> Self {
187 Self {
188 provider: "openai".to_string(),
189 api_key: None,
190 model: "gpt-4o-mini".to_string(),
191 base_url: "https://api.openai.com/v1".to_string(),
192 max_sample_length: 3000,
193 temperature: 0.3,
194 retry_attempts: 3,
195 retry_delay_ms: 1000,
196 }
197 }
198}
199
200#[derive(Debug, Serialize, Deserialize, Clone)]
202pub struct FormatsConfig {
203 pub default_output: String,
205 pub preserve_styling: bool,
207 pub default_encoding: String,
209 pub encoding_detection_confidence: f32,
211}
212
213impl Default for FormatsConfig {
214 fn default() -> Self {
215 Self {
216 default_output: "srt".to_string(),
217 preserve_styling: false,
218 default_encoding: "utf-8".to_string(),
219 encoding_detection_confidence: 0.8,
220 }
221 }
222}
223
224#[derive(Debug, Serialize, Deserialize, Clone)]
226pub struct SyncConfig {
227 pub max_offset_seconds: f32,
229 pub audio_sample_rate: u32,
231 pub correlation_threshold: f32,
233 pub dialogue_detection_threshold: f32,
235 pub min_dialogue_duration_ms: u32,
237 pub dialogue_merge_gap_ms: u32,
239 pub enable_dialogue_detection: bool,
241 pub auto_detect_sample_rate: bool,
243}
244
245impl Default for SyncConfig {
246 fn default() -> Self {
247 Self {
248 max_offset_seconds: 10.0,
249 audio_sample_rate: 44100,
250 correlation_threshold: 0.8,
251 dialogue_detection_threshold: 0.6,
252 min_dialogue_duration_ms: 500,
253 dialogue_merge_gap_ms: 200,
254 enable_dialogue_detection: true,
255 auto_detect_sample_rate: true,
256 }
257 }
258}
259
260#[derive(Debug, Serialize, Deserialize, Clone)]
262pub struct GeneralConfig {
263 pub backup_enabled: bool,
265 pub max_concurrent_jobs: usize,
267 pub temp_dir: Option<PathBuf>,
269 pub log_level: String,
271 pub cache_dir: Option<PathBuf>,
273 pub task_timeout_seconds: u64,
275 pub enable_progress_bar: bool,
277 pub worker_idle_timeout_seconds: u64,
279}
280
281impl Default for GeneralConfig {
282 fn default() -> Self {
283 Self {
284 backup_enabled: false,
285 max_concurrent_jobs: 4,
286 temp_dir: None,
287 log_level: "info".to_string(),
288 cache_dir: None,
289 task_timeout_seconds: 300,
290 enable_progress_bar: true,
291 worker_idle_timeout_seconds: 60,
292 }
293 }
294}
295
296#[derive(Debug, Serialize, Deserialize, Clone)]
298pub struct ParallelConfig {
299 pub max_workers: usize,
301 pub chunk_size: usize,
303 pub overflow_strategy: OverflowStrategy,
305 pub enable_work_stealing: bool,
307 pub task_queue_size: usize,
309 pub enable_task_priorities: bool,
311 pub auto_balance_workers: bool,
313}
314
315impl Default for ParallelConfig {
316 fn default() -> Self {
317 Self {
318 max_workers: num_cpus::get(),
319 chunk_size: 1000,
320 overflow_strategy: OverflowStrategy::Block,
321 enable_work_stealing: true,
322 task_queue_size: 1000,
323 enable_task_priorities: false,
324 auto_balance_workers: true,
325 }
326 }
327}
328
329#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
331pub enum OverflowStrategy {
332 Block,
334 Drop,
336 Expand,
338 DropOldest,
340 Reject,
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 #[test]
349 fn test_default_config_creation() {
350 let config = Config::default();
351 assert_eq!(config.ai.provider, "openai");
352 assert_eq!(config.ai.model, "gpt-4o-mini");
353 assert_eq!(config.formats.default_output, "srt");
354 assert!(!config.general.backup_enabled);
355 assert_eq!(config.general.max_concurrent_jobs, 4);
356 }
357
358 #[test]
359 fn test_ai_config_defaults() {
360 let ai_config = AIConfig::default();
361 assert_eq!(ai_config.provider, "openai");
362 assert_eq!(ai_config.model, "gpt-4o-mini");
363 assert_eq!(ai_config.temperature, 0.3);
364 assert_eq!(ai_config.max_sample_length, 3000);
365 }
366
367 #[test]
368 fn test_sync_config_defaults() {
369 let sync_config = SyncConfig::default();
370 assert_eq!(sync_config.max_offset_seconds, 10.0);
371 assert_eq!(sync_config.correlation_threshold, 0.8);
372 assert_eq!(sync_config.audio_sample_rate, 44100);
373 assert!(sync_config.enable_dialogue_detection);
374 }
375
376 #[test]
377 fn test_config_serialization() {
378 let config = Config::default();
379 let toml_str = toml::to_string(&config).unwrap();
380 assert!(toml_str.contains("[ai]"));
381 assert!(toml_str.contains("[sync]"));
382 assert!(toml_str.contains("[general]"));
383 assert!(toml_str.contains("[parallel]"));
384 }
385}