use crate::audio::{extract_metadata, FFmpeg};
use crate::models::{BookFolder, Track};
use anyhow::Result;
use futures::stream::{self, StreamExt};
pub struct Analyzer {
ffmpeg: FFmpeg,
parallel_workers: usize,
}
impl Analyzer {
pub fn new() -> Result<Self> {
Ok(Self {
ffmpeg: FFmpeg::new()?,
parallel_workers: 8,
})
}
pub fn with_workers(workers: usize) -> Result<Self> {
Ok(Self {
ffmpeg: FFmpeg::new()?,
parallel_workers: workers.clamp(1, 16),
})
}
pub async fn analyze_book_folder(&self, book_folder: &mut BookFolder) -> Result<()> {
let results = stream::iter(&book_folder.mp3_files)
.map(|mp3_file| async {
let quality = self.ffmpeg.probe_audio_file(mp3_file).await?;
let mut track = Track::new(mp3_file.clone(), quality);
if let Err(e) = extract_metadata(&mut track) {
tracing::warn!(
"Failed to extract metadata from {}: {}",
mp3_file.display(),
e
);
}
Ok::<Track, anyhow::Error>(track)
})
.buffer_unordered(self.parallel_workers)
.collect::<Vec<_>>()
.await;
let mut tracks = Vec::new();
for result in results {
match result {
Ok(track) => tracks.push(track),
Err(e) => {
tracing::error!("Failed to analyze track: {}", e);
return Err(e);
}
}
}
tracks.sort_by(|a, b| a.file_path.cmp(&b.file_path));
book_folder.tracks = tracks;
Ok(())
}
pub fn get_total_duration(&self, book_folder: &BookFolder) -> f64 {
book_folder.get_total_duration()
}
pub fn can_use_copy_mode(&self, book_folder: &BookFolder) -> bool {
book_folder.can_use_concat_copy()
}
}
impl Default for Analyzer {
fn default() -> Self {
Self::new().expect("Failed to create analyzer")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::QualityProfile;
use std::path::PathBuf;
#[test]
fn test_analyzer_creation() {
let analyzer = Analyzer::new();
assert!(analyzer.is_ok());
}
#[test]
fn test_analyzer_with_workers() {
let analyzer = Analyzer::with_workers(4).unwrap();
assert_eq!(analyzer.parallel_workers, 4);
let analyzer = Analyzer::with_workers(20).unwrap();
assert_eq!(analyzer.parallel_workers, 16);
}
#[test]
fn test_can_use_copy_mode() {
let analyzer = Analyzer::new().unwrap();
let mut book = BookFolder::new(PathBuf::from("/test"));
let aac_quality = QualityProfile::new(128, 44100, 2, "aac".to_string(), 3600.0).unwrap();
book.tracks = vec![
Track::new(PathBuf::from("1.m4a"), aac_quality.clone()),
Track::new(PathBuf::from("2.m4a"), aac_quality),
];
assert!(analyzer.can_use_copy_mode(&book));
let mp3_quality = QualityProfile::new(128, 44100, 2, "mp3".to_string(), 3600.0).unwrap();
book.tracks = vec![
Track::new(PathBuf::from("1.mp3"), mp3_quality.clone()),
Track::new(PathBuf::from("2.mp3"), mp3_quality),
];
assert!(!analyzer.can_use_copy_mode(&book));
}
}