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 #[serde(default = "default_api_max_retries")]
297 pub api_max_retries: u8,
298 #[serde(default = "default_api_retry_delay")]
300 pub api_retry_delay_secs: u64,
301 #[serde(default = "default_api_max_retry_delay")]
303 pub api_max_retry_delay_secs: u64,
304}
305
306impl Default for AudibleConfig {
307 fn default() -> Self {
308 Self {
309 enabled: false,
310 region: "us".to_string(),
311 auto_match: false,
312 download_covers: true,
313 cache_duration_hours: 168, rate_limit_per_minute: 100,
315 api_max_retries: 3,
316 api_retry_delay_secs: 1,
317 api_max_retry_delay_secs: 30,
318 }
319 }
320}
321
322fn default_audible_region() -> String {
323 "us".to_string()
324}
325
326fn default_cache_duration() -> u64 {
327 168 }
329
330fn default_rate_limit() -> u32 {
331 100
332}
333
334fn default_api_max_retries() -> u8 {
335 3
336}
337
338fn default_api_retry_delay() -> u64 {
339 1
340}
341
342fn default_api_max_retry_delay() -> u64 {
343 30
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
348pub struct OrganizationConfig {
349 #[serde(default = "default_m4b_folder")]
351 pub m4b_folder: String,
352 #[serde(default = "default_convert_folder")]
354 pub convert_folder: String,
355}
356
357impl Default for OrganizationConfig {
358 fn default() -> Self {
359 Self {
360 m4b_folder: "M4B".to_string(),
361 convert_folder: "To_Convert".to_string(),
362 }
363 }
364}
365
366fn default_m4b_folder() -> String {
367 "M4B".to_string()
368}
369
370fn default_convert_folder() -> String {
371 "To_Convert".to_string()
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct LoggingConfig {
377 #[serde(default)]
379 pub log_to_file: bool,
380 pub log_file: Option<PathBuf>,
382 #[serde(default = "default_log_level")]
384 pub log_level: String,
385}
386
387impl Default for LoggingConfig {
388 fn default() -> Self {
389 Self {
390 log_to_file: false,
391 log_file: None,
392 log_level: "INFO".to_string(),
393 }
394 }
395}
396
397fn default_log_level() -> String {
398 "INFO".to_string()
399}
400
401fn default_aac_encoder() -> String {
402 "auto".to_string()
403}
404
405#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct AdvancedConfig {
408 pub ffmpeg_path: Option<PathBuf>,
410 pub atomic_parsley_path: Option<PathBuf>,
412 pub mp4box_path: Option<PathBuf>,
414 pub temp_directory: Option<PathBuf>,
416 #[serde(skip_serializing_if = "Option::is_none")]
419 pub use_apple_silicon_encoder: Option<bool>,
420 #[serde(default = "default_aac_encoder")]
422 pub aac_encoder: String,
423}
424
425impl Default for AdvancedConfig {
426 fn default() -> Self {
427 Self {
428 ffmpeg_path: None,
429 atomic_parsley_path: None,
430 mp4box_path: None,
431 temp_directory: None,
432 use_apple_silicon_encoder: None,
433 aac_encoder: default_aac_encoder(),
434 }
435 }
436}
437
438#[cfg(test)]
439mod tests {
440 use super::*;
441
442 #[test]
443 fn test_default_config() {
444 let config = Config::default();
445 assert_eq!(config.processing.parallel_workers, 2);
446 assert_eq!(config.quality.prefer_stereo, true);
447 assert_eq!(config.metadata.default_language, "es");
448 }
449
450 #[test]
451 fn test_config_serialization() {
452 let config = Config::default();
453 let yaml = serde_yaml::to_string(&config).unwrap();
454 let deserialized: Config = serde_yaml::from_str(&yaml).unwrap();
455 assert_eq!(deserialized.processing.parallel_workers, 2);
456 }
457}