audiobook_forge/core/
analyzer.rs1use crate::audio::{extract_metadata, FFmpeg};
4use crate::models::{BookFolder, Track};
5use anyhow::Result;
6use futures::stream::{self, StreamExt};
7
8pub struct Analyzer {
10 ffmpeg: FFmpeg,
11 parallel_workers: usize,
12}
13
14impl Analyzer {
15 pub fn new() -> Result<Self> {
17 Ok(Self {
18 ffmpeg: FFmpeg::new()?,
19 parallel_workers: 8,
20 })
21 }
22
23 pub fn with_workers(workers: usize) -> Result<Self> {
25 Ok(Self {
26 ffmpeg: FFmpeg::new()?,
27 parallel_workers: workers.clamp(1, 16),
28 })
29 }
30
31 pub async fn analyze_book_folder(&self, book_folder: &mut BookFolder) -> Result<()> {
33 let results = stream::iter(&book_folder.mp3_files)
35 .map(|mp3_file| async {
36 let quality = self.ffmpeg.probe_audio_file(mp3_file).await?;
38
39 let mut track = Track::new(mp3_file.clone(), quality);
41
42 if let Err(e) = extract_metadata(&mut track) {
44 tracing::warn!(
45 "Failed to extract metadata from {}: {}",
46 mp3_file.display(),
47 e
48 );
49 }
50
51 Ok::<Track, anyhow::Error>(track)
52 })
53 .buffer_unordered(self.parallel_workers)
54 .collect::<Vec<_>>()
55 .await;
56
57 let mut tracks = Vec::new();
59 for result in results {
60 match result {
61 Ok(track) => tracks.push(track),
62 Err(e) => {
63 tracing::error!("Failed to analyze track: {}", e);
64 return Err(e);
65 }
66 }
67 }
68
69 tracks.sort_by(|a, b| a.file_path.cmp(&b.file_path));
72
73 book_folder.tracks = tracks;
74
75 Ok(())
76 }
77
78 pub fn get_total_duration(&self, book_folder: &BookFolder) -> f64 {
80 book_folder.get_total_duration()
81 }
82
83 pub fn can_use_copy_mode(&self, book_folder: &BookFolder) -> bool {
85 book_folder.can_use_concat_copy()
86 }
87}
88
89impl Default for Analyzer {
90 fn default() -> Self {
91 Self::new().expect("Failed to create analyzer")
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::models::QualityProfile;
99 use std::path::PathBuf;
100
101 #[test]
102 fn test_analyzer_creation() {
103 let analyzer = Analyzer::new();
104 assert!(analyzer.is_ok());
105 }
106
107 #[test]
108 fn test_analyzer_with_workers() {
109 let analyzer = Analyzer::with_workers(4).unwrap();
110 assert_eq!(analyzer.parallel_workers, 4);
111
112 let analyzer = Analyzer::with_workers(20).unwrap();
114 assert_eq!(analyzer.parallel_workers, 16);
115 }
116
117 #[test]
118 fn test_can_use_copy_mode() {
119 let analyzer = Analyzer::new().unwrap();
120 let mut book = BookFolder::new(PathBuf::from("/test"));
121
122 let aac_quality = QualityProfile::new(128, 44100, 2, "aac".to_string(), 3600.0).unwrap();
124 book.tracks = vec![
125 Track::new(PathBuf::from("1.m4a"), aac_quality.clone()),
126 Track::new(PathBuf::from("2.m4a"), aac_quality),
127 ];
128
129 assert!(analyzer.can_use_copy_mode(&book));
130
131 let mp3_quality = QualityProfile::new(128, 44100, 2, "mp3".to_string(), 3600.0).unwrap();
133 book.tracks = vec![
134 Track::new(PathBuf::from("1.mp3"), mp3_quality.clone()),
135 Track::new(PathBuf::from("2.mp3"), mp3_quality),
136 ];
137
138 assert!(!analyzer.can_use_copy_mode(&book));
139 }
140}