subx_cli/config/
config_legacy.rs

1//! Configuration data structures for the SubX CLI application.
2//!
3//! This module provides configuration type definitions used throughout
4//! the application. These types are now instantiated through the new
5//! configuration service system rather than global configuration managers.
6
7use crate::{Result, error::SubXError};
8use log::debug;
9use serde::{Deserialize, Serialize};
10use std::path::PathBuf;
11
12// config crate imports for new configuration system
13use config::{Config as ConfigBuilder, Environment, File};
14
15/// Full application configuration.
16///
17/// This struct aggregates all settings for AI integration, subtitle format
18/// conversion, synchronization, general options, and parallel execution.
19#[derive(Debug, Serialize, Deserialize, Clone, Default)]
20pub struct Config {
21    /// AI service configuration parameters.
22    pub ai: AIConfig,
23    /// Subtitle format conversion settings.
24    pub formats: FormatsConfig,
25    /// Audio-subtitle synchronization options.
26    pub sync: SyncConfig,
27    /// General runtime options (e.g., backup enabled, job limits).
28    pub general: GeneralConfig,
29    /// Parallel processing parameters.
30    pub parallel: ParallelConfig,
31    /// Optional file path from which the configuration was loaded.
32    pub loaded_from: Option<PathBuf>,
33}
34
35impl Config {
36    /// Get the user configuration file path.
37    ///
38    /// This method determines the appropriate configuration file path based on
39    /// environment variables and user directories.
40    ///
41    /// # Returns
42    ///
43    /// Returns the path to the user's configuration file.
44    ///
45    /// # Errors
46    ///
47    /// Returns an error if the configuration directory cannot be determined.
48    pub fn config_file_path() -> Result<PathBuf> {
49        // Check for custom config path from environment
50        if let Ok(custom) = std::env::var("SUBX_CONFIG_PATH") {
51            let path = PathBuf::from(custom);
52            return Ok(path);
53        }
54
55        // Use default user config directory
56        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    /// Save configuration to TOML file.
63    ///
64    /// # Arguments
65    ///
66    /// * `path` - Path where to save the configuration
67    ///
68    /// # Errors
69    ///
70    /// Returns an error if serialization or file writing fails.
71    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        // Create parent directory if needed
76        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    /// Save configuration to default file path.
89    ///
90    /// # Errors
91    ///
92    /// Returns an error if saving fails.
93    pub fn save(&self) -> Result<()> {
94        let config_path = Self::config_file_path()?;
95        self.save_to_file(&config_path)
96    }
97
98    /// Get a configuration value by key path.
99    ///
100    /// # Arguments
101    ///
102    /// * `key` - Dot-separated key path (e.g., "ai.provider")
103    ///
104    /// # Errors
105    ///
106    /// Returns an error if the key is not found.
107    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    /// Create configuration from sources.
126    ///
127    /// This method builds a configuration by merging settings from multiple sources
128    /// in order of precedence: defaults, config file, environment variables, and
129    /// command-line overrides.
130    ///
131    /// # Returns
132    ///
133    /// Returns a complete configuration loaded from all available sources.
134    ///
135    /// # Errors
136    ///
137    /// Returns an error if configuration loading or parsing fails.
138    pub fn create_config_from_sources() -> Result<Self> {
139        let config_path = Self::config_file_path()?;
140
141        let settings = ConfigBuilder::builder()
142            // Start with defaults
143            .add_source(ConfigBuilder::try_from(&Self::default())?)
144            // Add file source if it exists
145            .add_source(File::from(config_path).required(false))
146            // Add environment variables with SUBX_ prefix
147            .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/// AI service provider configuration.
165#[derive(Debug, Serialize, Deserialize, Clone)]
166pub struct AIConfig {
167    /// AI provider name (e.g. "openai", "anthropic")
168    pub provider: String,
169    /// API key for authentication
170    pub api_key: Option<String>,
171    /// AI model name to use
172    pub model: String,
173    /// API base URL
174    pub base_url: String,
175    /// Maximum sample length per request
176    pub max_sample_length: usize,
177    /// AI generation creativity parameter (0.0-1.0)
178    pub temperature: f32,
179    /// Number of retries on request failure
180    pub retry_attempts: u32,
181    /// Retry interval in milliseconds
182    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/// Subtitle format related configuration.
201#[derive(Debug, Serialize, Deserialize, Clone)]
202pub struct FormatsConfig {
203    /// Default output format (e.g. "srt", "ass", "vtt")
204    pub default_output: String,
205    /// Whether to preserve style information during format conversion
206    pub preserve_styling: bool,
207    /// Default character encoding (e.g. "utf-8", "gbk")
208    pub default_encoding: String,
209    /// Encoding detection confidence threshold (0.0-1.0)
210    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/// Audio synchronization related configuration.
225#[derive(Debug, Serialize, Deserialize, Clone)]
226pub struct SyncConfig {
227    /// Maximum offset in seconds for synchronization
228    pub max_offset_seconds: f32,
229    /// Audio sample rate for processing
230    pub audio_sample_rate: u32,
231    /// Correlation threshold for sync quality (0.0-1.0)
232    pub correlation_threshold: f32,
233    /// Dialogue detection threshold (0.0-1.0)
234    pub dialogue_detection_threshold: f32,
235    /// Minimum dialogue duration in milliseconds
236    pub min_dialogue_duration_ms: u32,
237    /// Gap between dialogues for merging (milliseconds)
238    pub dialogue_merge_gap_ms: u32,
239    /// Enable dialogue detection
240    pub enable_dialogue_detection: bool,
241    /// Auto-detect sample rate from audio files
242    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/// General application configuration.
261#[derive(Debug, Serialize, Deserialize, Clone)]
262pub struct GeneralConfig {
263    /// Enable automatic backup of original files
264    pub backup_enabled: bool,
265    /// Maximum number of concurrent processing jobs
266    pub max_concurrent_jobs: usize,
267    /// Temporary directory for processing
268    pub temp_dir: Option<PathBuf>,
269    /// Log level for application output
270    pub log_level: String,
271    /// Cache directory for storing processed data
272    pub cache_dir: Option<PathBuf>,
273    /// Task timeout in seconds
274    pub task_timeout_seconds: u64,
275    /// Enable progress bar display
276    pub enable_progress_bar: bool,
277    /// Worker idle timeout in seconds
278    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/// Parallel processing configuration.
297#[derive(Debug, Serialize, Deserialize, Clone)]
298pub struct ParallelConfig {
299    /// Maximum number of worker threads
300    pub max_workers: usize,
301    /// Chunk size for parallel processing
302    pub chunk_size: usize,
303    /// Overflow strategy when workers are busy
304    pub overflow_strategy: OverflowStrategy,
305    /// Enable work stealing between workers
306    pub enable_work_stealing: bool,
307    /// Task queue size
308    pub task_queue_size: usize,
309    /// Enable task priorities
310    pub enable_task_priorities: bool,
311    /// Auto-balance workers
312    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/// Strategy for handling overflow when all workers are busy.
330#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
331pub enum OverflowStrategy {
332    /// Block until a worker becomes available
333    Block,
334    /// Drop new tasks when all workers are busy
335    Drop,
336    /// Create additional temporary workers
337    Expand,
338    /// Drop oldest tasks in queue
339    DropOldest,
340    /// Reject new tasks
341    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}