audiobook_forge/utils/
config.rs1use crate::models::Config;
4use anyhow::{Context, Result};
5use std::fs;
6use std::path::PathBuf;
7
8pub struct ConfigManager;
10
11impl ConfigManager {
12 pub fn default_config_path() -> Result<PathBuf> {
14 let config_dir = dirs::config_dir()
15 .context("Cannot determine config directory")?
16 .join("audiobook-forge");
17
18 Ok(config_dir.join("config.yaml"))
19 }
20
21 pub fn ensure_config_dir() -> Result<PathBuf> {
23 let config_dir = dirs::config_dir()
24 .context("Cannot determine config directory")?
25 .join("audiobook-forge");
26
27 if !config_dir.exists() {
28 fs::create_dir_all(&config_dir)
29 .context("Failed to create config directory")?;
30 }
31
32 Ok(config_dir)
33 }
34
35 pub fn load_or_default(path: Option<&PathBuf>) -> Result<Config> {
37 let config_path = match path {
38 Some(p) => p.clone(),
39 None => Self::default_config_path()?,
40 };
41
42 if config_path.exists() {
43 Self::load(&config_path)
44 } else {
45 Ok(Config::default())
46 }
47 }
48
49 pub fn load(path: &PathBuf) -> Result<Config> {
51 let contents = fs::read_to_string(path)
52 .with_context(|| format!("Failed to read config file: {}", path.display()))?;
53
54 let config: Config = serde_yaml::from_str(&contents)
55 .with_context(|| format!("Failed to parse config file: {}", path.display()))?;
56
57 Ok(config)
58 }
59
60 pub fn save(config: &Config, path: Option<&PathBuf>) -> Result<()> {
62 let config_path = match path {
63 Some(p) => p.clone(),
64 None => Self::default_config_path()?,
65 };
66
67 if let Some(parent) = config_path.parent() {
69 if !parent.exists() {
70 fs::create_dir_all(parent)
71 .context("Failed to create config directory")?;
72 }
73 }
74
75 let yaml = serde_yaml::to_string(config)
76 .context("Failed to serialize config to YAML")?;
77
78 fs::write(&config_path, yaml)
79 .with_context(|| format!("Failed to write config file: {}", config_path.display()))?;
80
81 Ok(())
82 }
83
84 pub fn init(force: bool) -> Result<PathBuf> {
86 let config_path = Self::default_config_path()?;
87
88 if config_path.exists() && !force {
89 anyhow::bail!(
90 "Config file already exists at: {}\nUse --force to overwrite",
91 config_path.display()
92 );
93 }
94
95 let config_content = include_str!("../../templates/config.yaml");
97
98 if let Some(parent) = config_path.parent() {
100 if !parent.exists() {
101 fs::create_dir_all(parent)?;
102 }
103 }
104
105 fs::write(&config_path, config_content)
106 .with_context(|| format!("Failed to write config file: {}", config_path.display()))?;
107
108 Ok(config_path)
109 }
110
111 pub fn validate(config: &Config) -> Result<Vec<String>> {
113 let mut warnings = Vec::new();
114
115 if config.processing.parallel_workers < 1 || config.processing.parallel_workers > 8 {
117 warnings.push(format!(
118 "parallel_workers ({}) should be between 1 and 8",
119 config.processing.parallel_workers
120 ));
121 }
122
123 let valid_chapter_sources = ["auto", "files", "cue", "id3", "none"];
125 if !valid_chapter_sources.contains(&config.quality.chapter_source.as_str()) {
126 warnings.push(format!(
127 "chapter_source '{}' is not recognized. Valid options: {}",
128 config.quality.chapter_source,
129 valid_chapter_sources.join(", ")
130 ));
131 }
132
133 let valid_log_levels = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"];
135 if !valid_log_levels.contains(&config.logging.log_level.to_uppercase().as_str()) {
136 warnings.push(format!(
137 "log_level '{}' is not recognized. Valid options: {}",
138 config.logging.log_level,
139 valid_log_levels.join(", ")
140 ));
141 }
142
143 if let Some(ref path) = config.directories.source {
145 if !path.exists() {
146 warnings.push(format!(
147 "source directory does not exist: {}",
148 path.display()
149 ));
150 }
151 }
152
153 Ok(warnings)
154 }
155
156 pub fn show(path: Option<&PathBuf>) -> Result<String> {
158 let config = Self::load_or_default(path)?;
159 let yaml = serde_yaml::to_string(&config)
160 .context("Failed to serialize config")?;
161 Ok(yaml)
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use tempfile::tempdir;
169
170 #[test]
171 fn test_load_save_config() {
172 let dir = tempdir().unwrap();
173 let config_path = dir.path().join("config.yaml");
174
175 let config = Config::default();
176 ConfigManager::save(&config, Some(&config_path)).unwrap();
177
178 let loaded = ConfigManager::load(&config_path).unwrap();
179 assert_eq!(loaded.processing.parallel_workers, 2);
180 }
181
182 #[test]
183 fn test_validate_config() {
184 let mut config = Config::default();
185 config.processing.parallel_workers = 10; let warnings = ConfigManager::validate(&config).unwrap();
188 assert!(!warnings.is_empty());
189 }
190}