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    /// Audible metadata integration
212    #[serde(default)]
213    pub audible: AudibleConfig,
214    /// Matching mode for build command
215    #[serde(default)]
216    pub match_mode: MatchMode,
217}
218
219impl Default for MetadataConfig {
220    fn default() -> Self {
221        Self {
222            default_language: "es".to_string(),
223            cover_filenames: vec![
224                "cover.jpg".to_string(),
225                "folder.jpg".to_string(),
226                "cover.png".to_string(),
227                "folder.png".to_string(),
228            ],
229            audible: AudibleConfig::default(),
230            match_mode: MatchMode::default(),
231        }
232    }
233}
234
235fn default_language() -> String {
236    "es".to_string()
237}
238
239fn default_cover_filenames() -> Vec<String> {
240    vec![
241        "cover.jpg".to_string(),
242        "folder.jpg".to_string(),
243        "cover.png".to_string(),
244        "folder.png".to_string(),
245    ]
246}
247
248/// Matching mode for interactive metadata matching during build
249#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
250#[serde(rename_all = "lowercase")]
251pub enum MatchMode {
252    /// Disabled - don't match during build
253    Disabled,
254    /// Auto - automatically select best match (non-interactive)
255    Auto,
256    /// Interactive - prompt user for each file
257    Interactive,
258}
259
260impl Default for MatchMode {
261    fn default() -> Self {
262        MatchMode::Disabled
263    }
264}
265
266/// Audible metadata integration configuration
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct AudibleConfig {
269    /// Enable Audible metadata fetching
270    #[serde(default)]
271    pub enabled: bool,
272    /// Default Audible region for queries
273    #[serde(default = "default_audible_region")]
274    pub region: String,
275    /// Auto-match books by folder name during build
276    #[serde(default)]
277    pub auto_match: bool,
278    /// Download and embed cover art from Audible
279    #[serde(default = "default_true")]
280    pub download_covers: bool,
281    /// Cache metadata locally (hours, 0 = no cache)
282    #[serde(default = "default_cache_duration")]
283    pub cache_duration_hours: u64,
284    /// Rate limit (requests per minute)
285    #[serde(default = "default_rate_limit")]
286    pub rate_limit_per_minute: u32,
287}
288
289impl Default for AudibleConfig {
290    fn default() -> Self {
291        Self {
292            enabled: false,
293            region: "us".to_string(),
294            auto_match: false,
295            download_covers: true,
296            cache_duration_hours: 168, // 7 days
297            rate_limit_per_minute: 100,
298        }
299    }
300}
301
302fn default_audible_region() -> String {
303    "us".to_string()
304}
305
306fn default_cache_duration() -> u64 {
307    168 // 7 days
308}
309
310fn default_rate_limit() -> u32 {
311    100
312}
313
314/// Organization configuration
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct OrganizationConfig {
317    /// Name for completed audiobooks folder
318    #[serde(default = "default_m4b_folder")]
319    pub m4b_folder: String,
320    /// Name for conversion queue folder
321    #[serde(default = "default_convert_folder")]
322    pub convert_folder: String,
323}
324
325impl Default for OrganizationConfig {
326    fn default() -> Self {
327        Self {
328            m4b_folder: "M4B".to_string(),
329            convert_folder: "To_Convert".to_string(),
330        }
331    }
332}
333
334fn default_m4b_folder() -> String {
335    "M4B".to_string()
336}
337
338fn default_convert_folder() -> String {
339    "To_Convert".to_string()
340}
341
342/// Logging configuration
343#[derive(Debug, Clone, Serialize, Deserialize)]
344pub struct LoggingConfig {
345    /// Enable automatic log file creation
346    #[serde(default)]
347    pub log_to_file: bool,
348    /// Custom log file path
349    pub log_file: Option<PathBuf>,
350    /// Log level ("INFO", "DEBUG", "WARNING", "ERROR")
351    #[serde(default = "default_log_level")]
352    pub log_level: String,
353}
354
355impl Default for LoggingConfig {
356    fn default() -> Self {
357        Self {
358            log_to_file: false,
359            log_file: None,
360            log_level: "INFO".to_string(),
361        }
362    }
363}
364
365fn default_log_level() -> String {
366    "INFO".to_string()
367}
368
369fn default_aac_encoder() -> String {
370    "auto".to_string()
371}
372
373/// Advanced configuration
374#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct AdvancedConfig {
376    /// Custom FFmpeg binary path
377    pub ffmpeg_path: Option<PathBuf>,
378    /// Custom AtomicParsley binary path
379    pub atomic_parsley_path: Option<PathBuf>,
380    /// Custom MP4Box binary path
381    pub mp4box_path: Option<PathBuf>,
382    /// Custom temporary files location
383    pub temp_directory: Option<PathBuf>,
384    /// DEPRECATED: Use aac_encoder instead
385    /// Use Apple Silicon hardware encoder (aac_at)
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub use_apple_silicon_encoder: Option<bool>,
388    /// AAC encoder preference: "auto", "aac_at", "libfdk_aac", "aac"
389    #[serde(default = "default_aac_encoder")]
390    pub aac_encoder: String,
391}
392
393impl Default for AdvancedConfig {
394    fn default() -> Self {
395        Self {
396            ffmpeg_path: None,
397            atomic_parsley_path: None,
398            mp4box_path: None,
399            temp_directory: None,
400            use_apple_silicon_encoder: None,
401            aac_encoder: default_aac_encoder(),
402        }
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    #[test]
411    fn test_default_config() {
412        let config = Config::default();
413        assert_eq!(config.processing.parallel_workers, 2);
414        assert_eq!(config.quality.prefer_stereo, true);
415        assert_eq!(config.metadata.default_language, "es");
416    }
417
418    #[test]
419    fn test_config_serialization() {
420        let config = Config::default();
421        let yaml = serde_yaml::to_string(&config).unwrap();
422        let deserialized: Config = serde_yaml::from_str(&yaml).unwrap();
423        assert_eq!(deserialized.processing.parallel_workers, 2);
424    }
425}