1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6#[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#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct DirectoryConfig {
45 pub source: Option<PathBuf>,
47 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct PerformanceConfig {
68 #[serde(default = "default_max_concurrent_encodes")]
71 pub max_concurrent_encodes: String,
72 #[serde(default = "default_true")]
74 pub enable_parallel_encoding: bool,
75 #[serde(default = "default_encoding_preset")]
77 pub encoding_preset: String,
78 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ProcessingConfig {
109 #[serde(default = "default_parallel_workers")]
111 pub parallel_workers: u8,
112 #[serde(default = "default_true")]
114 pub skip_existing: bool,
115 #[serde(default)]
117 pub force_reprocess: bool,
118 #[serde(default)]
120 pub normalize_existing: bool,
121 #[serde(default)]
123 pub keep_temp_files: bool,
124 #[serde(default = "default_max_retries")]
126 pub max_retries: u8,
127 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct QualityConfig {
165 #[serde(default = "default_true")]
167 pub prefer_stereo: bool,
168 #[serde(default = "default_chapter_source")]
170 pub chapter_source: String,
171 #[serde(default = "default_bitrate")]
173 pub default_bitrate: String,
174 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct MetadataConfig {
205 #[serde(default = "default_language")]
207 pub default_language: String,
208 #[serde(default = "default_cover_filenames")]
210 pub cover_filenames: Vec<String>,
211 #[serde(default = "default_auto_extract_cover")]
213 pub auto_extract_cover: bool,
214 #[serde(default)]
216 pub audible: AudibleConfig,
217 #[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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
258#[serde(rename_all = "lowercase")]
259pub enum MatchMode {
260 Disabled,
262 Auto,
264 Interactive,
266}
267
268impl Default for MatchMode {
269 fn default() -> Self {
270 MatchMode::Disabled
271 }
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct AudibleConfig {
277 #[serde(default)]
279 pub enabled: bool,
280 #[serde(default = "default_audible_region")]
282 pub region: String,
283 #[serde(default)]
285 pub auto_match: bool,
286 #[serde(default = "default_true")]
288 pub download_covers: bool,
289 #[serde(default = "default_cache_duration")]
291 pub cache_duration_hours: u64,
292 #[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, 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 }
317
318fn default_rate_limit() -> u32 {
319 100
320}
321
322#[derive(Debug, Clone, Serialize, Deserialize)]
324pub struct OrganizationConfig {
325 #[serde(default = "default_m4b_folder")]
327 pub m4b_folder: String,
328 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct LoggingConfig {
353 #[serde(default)]
355 pub log_to_file: bool,
356 pub log_file: Option<PathBuf>,
358 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
383pub struct AdvancedConfig {
384 pub ffmpeg_path: Option<PathBuf>,
386 pub atomic_parsley_path: Option<PathBuf>,
388 pub mp4box_path: Option<PathBuf>,
390 pub temp_directory: Option<PathBuf>,
392 #[serde(skip_serializing_if = "Option::is_none")]
395 pub use_apple_silicon_encoder: Option<bool>,
396 #[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}