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    /// Maximum concurrent file encodings per book (prevents resource exhaustion)
79    #[serde(default = "default_max_concurrent_files_per_book")]
80    pub max_concurrent_files_per_book: String,
81}
82
83impl Default for PerformanceConfig {
84    fn default() -> Self {
85        Self {
86            max_concurrent_encodes: "auto".to_string(),
87            enable_parallel_encoding: true,
88            encoding_preset: "balanced".to_string(),
89            max_concurrent_files_per_book: "8".to_string(),
90        }
91    }
92}
93
94fn default_max_concurrent_encodes() -> String {
95    "auto".to_string()
96}
97
98fn default_encoding_preset() -> String {
99    "balanced".to_string()
100}
101
102fn default_max_concurrent_files_per_book() -> String {
103    "8".to_string()
104}
105
106/// Processing configuration
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ProcessingConfig {
109    /// Number of parallel workers (1-8)
110    #[serde(default = "default_parallel_workers")]
111    pub parallel_workers: u8,
112    /// Skip folders with existing M4B files
113    #[serde(default = "default_true")]
114    pub skip_existing: bool,
115    /// Always reprocess, overwriting existing files
116    #[serde(default)]
117    pub force_reprocess: bool,
118    /// Normalize existing M4B files (fix metadata)
119    #[serde(default)]
120    pub normalize_existing: bool,
121    /// Keep temporary files for debugging
122    #[serde(default)]
123    pub keep_temp_files: bool,
124    /// Maximum number of retry attempts
125    #[serde(default = "default_max_retries")]
126    pub max_retries: u8,
127    /// Initial retry delay in seconds
128    #[serde(default = "default_retry_delay")]
129    pub retry_delay: u64,
130}
131
132impl Default for ProcessingConfig {
133    fn default() -> Self {
134        Self {
135            parallel_workers: 2,
136            skip_existing: true,
137            force_reprocess: false,
138            normalize_existing: false,
139            keep_temp_files: false,
140            max_retries: 2,
141            retry_delay: 1,
142        }
143    }
144}
145
146fn default_max_retries() -> u8 {
147    2
148}
149
150fn default_retry_delay() -> u64 {
151    1
152}
153
154fn default_parallel_workers() -> u8 {
155    2
156}
157
158fn default_true() -> bool {
159    true
160}
161
162/// Quality configuration
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct QualityConfig {
165    /// Prefer stereo over mono when quality is equal
166    #[serde(default = "default_true")]
167    pub prefer_stereo: bool,
168    /// Chapter source priority ("auto", "files", "cue", etc.)
169    #[serde(default = "default_chapter_source")]
170    pub chapter_source: String,
171    /// Default bitrate in kbps ("auto" or specific: 64, 128, 256)
172    #[serde(default = "default_bitrate")]
173    pub default_bitrate: String,
174    /// Default sample rate in Hz ("auto" or specific: 44100, 48000)
175    #[serde(default = "default_sample_rate")]
176    pub default_sample_rate: String,
177}
178
179impl Default for QualityConfig {
180    fn default() -> Self {
181        Self {
182            prefer_stereo: true,
183            chapter_source: "auto".to_string(),
184            default_bitrate: "auto".to_string(),
185            default_sample_rate: "auto".to_string(),
186        }
187    }
188}
189
190fn default_chapter_source() -> String {
191    "auto".to_string()
192}
193
194fn default_bitrate() -> String {
195    "auto".to_string()
196}
197
198fn default_sample_rate() -> String {
199    "auto".to_string()
200}
201
202/// Metadata configuration
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct MetadataConfig {
205    /// Default language for metadata (ISO 639-1)
206    #[serde(default = "default_language")]
207    pub default_language: String,
208    /// Cover art filenames to search for
209    #[serde(default = "default_cover_filenames")]
210    pub cover_filenames: Vec<String>,
211    /// Auto-extract embedded cover art from audio files as fallback
212    #[serde(default = "default_auto_extract_cover")]
213    pub auto_extract_cover: bool,
214    /// Audible metadata integration
215    #[serde(default)]
216    pub audible: AudibleConfig,
217    /// Matching mode for build command
218    #[serde(default)]
219    pub match_mode: MatchMode,
220}
221
222impl Default for MetadataConfig {
223    fn default() -> Self {
224        Self {
225            default_language: "es".to_string(),
226            cover_filenames: vec![
227                "cover.jpg".to_string(),
228                "folder.jpg".to_string(),
229                "cover.png".to_string(),
230                "folder.png".to_string(),
231            ],
232            auto_extract_cover: true,
233            audible: AudibleConfig::default(),
234            match_mode: MatchMode::default(),
235        }
236    }
237}
238
239fn default_language() -> String {
240    "es".to_string()
241}
242
243fn default_cover_filenames() -> Vec<String> {
244    vec![
245        "cover.jpg".to_string(),
246        "folder.jpg".to_string(),
247        "cover.png".to_string(),
248        "folder.png".to_string(),
249    ]
250}
251
252fn default_auto_extract_cover() -> bool {
253    true
254}
255
256/// Matching mode for interactive metadata matching during build
257#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
258#[serde(rename_all = "lowercase")]
259pub enum MatchMode {
260    /// Disabled - don't match during build
261    Disabled,
262    /// Auto - automatically select best match (non-interactive)
263    Auto,
264    /// Interactive - prompt user for each file
265    Interactive,
266}
267
268impl Default for MatchMode {
269    fn default() -> Self {
270        MatchMode::Disabled
271    }
272}
273
274/// Audible metadata integration configuration
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct AudibleConfig {
277    /// Enable Audible metadata fetching
278    #[serde(default)]
279    pub enabled: bool,
280    /// Default Audible region for queries
281    #[serde(default = "default_audible_region")]
282    pub region: String,
283    /// Auto-match books by folder name during build
284    #[serde(default)]
285    pub auto_match: bool,
286    /// Download and embed cover art from Audible
287    #[serde(default = "default_true")]
288    pub download_covers: bool,
289    /// Cache metadata locally (hours, 0 = no cache)
290    #[serde(default = "default_cache_duration")]
291    pub cache_duration_hours: u64,
292    /// Rate limit (requests per minute)
293    #[serde(default = "default_rate_limit")]
294    pub rate_limit_per_minute: u32,
295}
296
297impl Default for AudibleConfig {
298    fn default() -> Self {
299        Self {
300            enabled: false,
301            region: "us".to_string(),
302            auto_match: false,
303            download_covers: true,
304            cache_duration_hours: 168, // 7 days
305            rate_limit_per_minute: 100,
306        }
307    }
308}
309
310fn default_audible_region() -> String {
311    "us".to_string()
312}
313
314fn default_cache_duration() -> u64 {
315    168 // 7 days
316}
317
318fn default_rate_limit() -> u32 {
319    100
320}
321
322/// Organization configuration
323#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct OrganizationConfig {
325    /// Name for completed audiobooks folder
326    #[serde(default = "default_m4b_folder")]
327    pub m4b_folder: String,
328    /// Name for conversion queue folder
329    #[serde(default = "default_convert_folder")]
330    pub convert_folder: String,
331}
332
333impl Default for OrganizationConfig {
334    fn default() -> Self {
335        Self {
336            m4b_folder: "M4B".to_string(),
337            convert_folder: "To_Convert".to_string(),
338        }
339    }
340}
341
342fn default_m4b_folder() -> String {
343    "M4B".to_string()
344}
345
346fn default_convert_folder() -> String {
347    "To_Convert".to_string()
348}
349
350/// Logging configuration
351#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct LoggingConfig {
353    /// Enable automatic log file creation
354    #[serde(default)]
355    pub log_to_file: bool,
356    /// Custom log file path
357    pub log_file: Option<PathBuf>,
358    /// Log level ("INFO", "DEBUG", "WARNING", "ERROR")
359    #[serde(default = "default_log_level")]
360    pub log_level: String,
361}
362
363impl Default for LoggingConfig {
364    fn default() -> Self {
365        Self {
366            log_to_file: false,
367            log_file: None,
368            log_level: "INFO".to_string(),
369        }
370    }
371}
372
373fn default_log_level() -> String {
374    "INFO".to_string()
375}
376
377fn default_aac_encoder() -> String {
378    "auto".to_string()
379}
380
381/// Advanced configuration
382#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct AdvancedConfig {
384    /// Custom FFmpeg binary path
385    pub ffmpeg_path: Option<PathBuf>,
386    /// Custom AtomicParsley binary path
387    pub atomic_parsley_path: Option<PathBuf>,
388    /// Custom MP4Box binary path
389    pub mp4box_path: Option<PathBuf>,
390    /// Custom temporary files location
391    pub temp_directory: Option<PathBuf>,
392    /// DEPRECATED: Use aac_encoder instead
393    /// Use Apple Silicon hardware encoder (aac_at)
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub use_apple_silicon_encoder: Option<bool>,
396    /// AAC encoder preference: "auto", "aac_at", "libfdk_aac", "aac"
397    #[serde(default = "default_aac_encoder")]
398    pub aac_encoder: String,
399}
400
401impl Default for AdvancedConfig {
402    fn default() -> Self {
403        Self {
404            ffmpeg_path: None,
405            atomic_parsley_path: None,
406            mp4box_path: None,
407            temp_directory: None,
408            use_apple_silicon_encoder: None,
409            aac_encoder: default_aac_encoder(),
410        }
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    #[test]
419    fn test_default_config() {
420        let config = Config::default();
421        assert_eq!(config.processing.parallel_workers, 2);
422        assert_eq!(config.quality.prefer_stereo, true);
423        assert_eq!(config.metadata.default_language, "es");
424    }
425
426    #[test]
427    fn test_config_serialization() {
428        let config = Config::default();
429        let yaml = serde_yaml::to_string(&config).unwrap();
430        let deserialized: Config = serde_yaml::from_str(&yaml).unwrap();
431        assert_eq!(deserialized.processing.parallel_workers, 2);
432    }
433}