audiobook_forge/models/
config.rs

1//! Configuration model
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6/// Main configuration structure
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Config {
9    #[serde(default)]
10    pub directories: DirectoryConfig,
11    #[serde(default)]
12    pub performance: PerformanceConfig,
13    #[serde(default)]
14    pub processing: ProcessingConfig,
15    #[serde(default)]
16    pub quality: QualityConfig,
17    #[serde(default)]
18    pub metadata: MetadataConfig,
19    #[serde(default)]
20    pub organization: OrganizationConfig,
21    #[serde(default)]
22    pub logging: LoggingConfig,
23    #[serde(default)]
24    pub advanced: AdvancedConfig,
25}
26
27impl Default for Config {
28    fn default() -> Self {
29        Self {
30            directories: DirectoryConfig::default(),
31            performance: PerformanceConfig::default(),
32            processing: ProcessingConfig::default(),
33            quality: QualityConfig::default(),
34            metadata: MetadataConfig::default(),
35            organization: OrganizationConfig::default(),
36            logging: LoggingConfig::default(),
37            advanced: AdvancedConfig::default(),
38        }
39    }
40}
41
42/// Directory configuration
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct DirectoryConfig {
45    /// Source directory for audiobooks (overrides --root)
46    pub source: Option<PathBuf>,
47    /// Output directory ("same_as_source" or custom path)
48    #[serde(default = "default_output")]
49    pub output: String,
50}
51
52impl Default for DirectoryConfig {
53    fn default() -> Self {
54        Self {
55            source: None,
56            output: "same_as_source".to_string(),
57        }
58    }
59}
60
61fn default_output() -> String {
62    "same_as_source".to_string()
63}
64
65/// Performance configuration
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct PerformanceConfig {
68    /// Maximum number of files to encode in parallel
69    /// "auto" = use all CPU cores, or specify a number
70    #[serde(default = "default_max_concurrent_encodes")]
71    pub max_concurrent_encodes: String,
72    /// Enable parallel file encoding (faster but more CPU/memory)
73    #[serde(default = "default_true")]
74    pub enable_parallel_encoding: bool,
75    /// Encoding quality preset: "fast", "balanced", "high"
76    #[serde(default = "default_encoding_preset")]
77    pub encoding_preset: String,
78}
79
80impl Default for PerformanceConfig {
81    fn default() -> Self {
82        Self {
83            max_concurrent_encodes: "auto".to_string(),
84            enable_parallel_encoding: true,
85            encoding_preset: "balanced".to_string(),
86        }
87    }
88}
89
90fn default_max_concurrent_encodes() -> String {
91    "auto".to_string()
92}
93
94fn default_encoding_preset() -> String {
95    "balanced".to_string()
96}
97
98/// Processing configuration
99#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct ProcessingConfig {
101    /// Number of parallel workers (1-8)
102    #[serde(default = "default_parallel_workers")]
103    pub parallel_workers: u8,
104    /// Skip folders with existing M4B files
105    #[serde(default = "default_true")]
106    pub skip_existing: bool,
107    /// Always reprocess, overwriting existing files
108    #[serde(default)]
109    pub force_reprocess: bool,
110    /// Normalize existing M4B files (fix metadata)
111    #[serde(default)]
112    pub normalize_existing: bool,
113    /// Keep temporary files for debugging
114    #[serde(default)]
115    pub keep_temp_files: bool,
116    /// Maximum number of retry attempts
117    #[serde(default = "default_max_retries")]
118    pub max_retries: u8,
119    /// Initial retry delay in seconds
120    #[serde(default = "default_retry_delay")]
121    pub retry_delay: u64,
122}
123
124impl Default for ProcessingConfig {
125    fn default() -> Self {
126        Self {
127            parallel_workers: 2,
128            skip_existing: true,
129            force_reprocess: false,
130            normalize_existing: false,
131            keep_temp_files: false,
132            max_retries: 2,
133            retry_delay: 1,
134        }
135    }
136}
137
138fn default_max_retries() -> u8 {
139    2
140}
141
142fn default_retry_delay() -> u64 {
143    1
144}
145
146fn default_parallel_workers() -> u8 {
147    2
148}
149
150fn default_true() -> bool {
151    true
152}
153
154/// Quality configuration
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct QualityConfig {
157    /// Prefer stereo over mono when quality is equal
158    #[serde(default = "default_true")]
159    pub prefer_stereo: bool,
160    /// Chapter source priority ("auto", "files", "cue", etc.)
161    #[serde(default = "default_chapter_source")]
162    pub chapter_source: String,
163    /// Default bitrate in kbps ("auto" or specific: 64, 128, 256)
164    #[serde(default = "default_bitrate")]
165    pub default_bitrate: String,
166    /// Default sample rate in Hz ("auto" or specific: 44100, 48000)
167    #[serde(default = "default_sample_rate")]
168    pub default_sample_rate: String,
169}
170
171impl Default for QualityConfig {
172    fn default() -> Self {
173        Self {
174            prefer_stereo: true,
175            chapter_source: "auto".to_string(),
176            default_bitrate: "auto".to_string(),
177            default_sample_rate: "auto".to_string(),
178        }
179    }
180}
181
182fn default_chapter_source() -> String {
183    "auto".to_string()
184}
185
186fn default_bitrate() -> String {
187    "auto".to_string()
188}
189
190fn default_sample_rate() -> String {
191    "auto".to_string()
192}
193
194/// Metadata configuration
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct MetadataConfig {
197    /// Default language for metadata (ISO 639-1)
198    #[serde(default = "default_language")]
199    pub default_language: String,
200    /// Cover art filenames to search for
201    #[serde(default = "default_cover_filenames")]
202    pub cover_filenames: Vec<String>,
203    /// Audible metadata integration
204    #[serde(default)]
205    pub audible: AudibleConfig,
206    /// Matching mode for build command
207    #[serde(default)]
208    pub match_mode: MatchMode,
209}
210
211impl Default for MetadataConfig {
212    fn default() -> Self {
213        Self {
214            default_language: "es".to_string(),
215            cover_filenames: vec![
216                "cover.jpg".to_string(),
217                "folder.jpg".to_string(),
218                "cover.png".to_string(),
219                "folder.png".to_string(),
220            ],
221            audible: AudibleConfig::default(),
222            match_mode: MatchMode::default(),
223        }
224    }
225}
226
227fn default_language() -> String {
228    "es".to_string()
229}
230
231fn default_cover_filenames() -> Vec<String> {
232    vec![
233        "cover.jpg".to_string(),
234        "folder.jpg".to_string(),
235        "cover.png".to_string(),
236        "folder.png".to_string(),
237    ]
238}
239
240/// Matching mode for interactive metadata matching during build
241#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
242#[serde(rename_all = "lowercase")]
243pub enum MatchMode {
244    /// Disabled - don't match during build
245    Disabled,
246    /// Auto - automatically select best match (non-interactive)
247    Auto,
248    /// Interactive - prompt user for each file
249    Interactive,
250}
251
252impl Default for MatchMode {
253    fn default() -> Self {
254        MatchMode::Disabled
255    }
256}
257
258/// Audible metadata integration configuration
259#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct AudibleConfig {
261    /// Enable Audible metadata fetching
262    #[serde(default)]
263    pub enabled: bool,
264    /// Default Audible region for queries
265    #[serde(default = "default_audible_region")]
266    pub region: String,
267    /// Auto-match books by folder name during build
268    #[serde(default)]
269    pub auto_match: bool,
270    /// Download and embed cover art from Audible
271    #[serde(default = "default_true")]
272    pub download_covers: bool,
273    /// Cache metadata locally (hours, 0 = no cache)
274    #[serde(default = "default_cache_duration")]
275    pub cache_duration_hours: u64,
276    /// Rate limit (requests per minute)
277    #[serde(default = "default_rate_limit")]
278    pub rate_limit_per_minute: u32,
279}
280
281impl Default for AudibleConfig {
282    fn default() -> Self {
283        Self {
284            enabled: false,
285            region: "us".to_string(),
286            auto_match: false,
287            download_covers: true,
288            cache_duration_hours: 168, // 7 days
289            rate_limit_per_minute: 100,
290        }
291    }
292}
293
294fn default_audible_region() -> String {
295    "us".to_string()
296}
297
298fn default_cache_duration() -> u64 {
299    168 // 7 days
300}
301
302fn default_rate_limit() -> u32 {
303    100
304}
305
306/// Organization configuration
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct OrganizationConfig {
309    /// Name for completed audiobooks folder
310    #[serde(default = "default_m4b_folder")]
311    pub m4b_folder: String,
312    /// Name for conversion queue folder
313    #[serde(default = "default_convert_folder")]
314    pub convert_folder: String,
315}
316
317impl Default for OrganizationConfig {
318    fn default() -> Self {
319        Self {
320            m4b_folder: "M4B".to_string(),
321            convert_folder: "To_Convert".to_string(),
322        }
323    }
324}
325
326fn default_m4b_folder() -> String {
327    "M4B".to_string()
328}
329
330fn default_convert_folder() -> String {
331    "To_Convert".to_string()
332}
333
334/// Logging configuration
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct LoggingConfig {
337    /// Enable automatic log file creation
338    #[serde(default)]
339    pub log_to_file: bool,
340    /// Custom log file path
341    pub log_file: Option<PathBuf>,
342    /// Log level ("INFO", "DEBUG", "WARNING", "ERROR")
343    #[serde(default = "default_log_level")]
344    pub log_level: String,
345}
346
347impl Default for LoggingConfig {
348    fn default() -> Self {
349        Self {
350            log_to_file: false,
351            log_file: None,
352            log_level: "INFO".to_string(),
353        }
354    }
355}
356
357fn default_log_level() -> String {
358    "INFO".to_string()
359}
360
361/// Advanced configuration
362#[derive(Debug, Clone, Serialize, Deserialize)]
363pub struct AdvancedConfig {
364    /// Custom FFmpeg binary path
365    pub ffmpeg_path: Option<PathBuf>,
366    /// Custom AtomicParsley binary path
367    pub atomic_parsley_path: Option<PathBuf>,
368    /// Custom MP4Box binary path
369    pub mp4box_path: Option<PathBuf>,
370    /// Custom temporary files location
371    pub temp_directory: Option<PathBuf>,
372    /// Use Apple Silicon hardware encoder (aac_at)
373    pub use_apple_silicon_encoder: Option<bool>,
374}
375
376impl Default for AdvancedConfig {
377    fn default() -> Self {
378        Self {
379            ffmpeg_path: None,
380            atomic_parsley_path: None,
381            mp4box_path: None,
382            temp_directory: None,
383            use_apple_silicon_encoder: None,
384        }
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn test_default_config() {
394        let config = Config::default();
395        assert_eq!(config.processing.parallel_workers, 2);
396        assert_eq!(config.quality.prefer_stereo, true);
397        assert_eq!(config.metadata.default_language, "es");
398    }
399
400    #[test]
401    fn test_config_serialization() {
402        let config = Config::default();
403        let yaml = serde_yaml::to_string(&config).unwrap();
404        let deserialized: Config = serde_yaml::from_str(&yaml).unwrap();
405        assert_eq!(deserialized.processing.parallel_workers, 2);
406    }
407}