audiobook_forge/utils/
validation.rs

1//! Dependency validation utilities
2
3use std::process::Command;
4use which::which;
5
6/// Dependency checker for external tools
7pub struct DependencyChecker;
8
9#[derive(Debug, Clone)]
10pub struct DependencyStatus {
11    pub name: String,
12    pub found: bool,
13    pub version: Option<String>,
14    pub path: Option<String>,
15}
16
17impl DependencyChecker {
18    /// Check if FFmpeg is installed and get version
19    pub fn check_ffmpeg() -> DependencyStatus {
20        match which("ffmpeg") {
21            Ok(path) => {
22                let version = Self::get_ffmpeg_version();
23                DependencyStatus {
24                    name: "ffmpeg".to_string(),
25                    found: true,
26                    version,
27                    path: Some(path.display().to_string()),
28                }
29            }
30            Err(_) => DependencyStatus {
31                name: "ffmpeg".to_string(),
32                found: false,
33                version: None,
34                path: None,
35            },
36        }
37    }
38
39    /// Check if AtomicParsley is installed
40    pub fn check_atomic_parsley() -> DependencyStatus {
41        match which("AtomicParsley") {
42            Ok(path) => {
43                let version = Self::get_atomic_parsley_version();
44                DependencyStatus {
45                    name: "AtomicParsley".to_string(),
46                    found: true,
47                    version,
48                    path: Some(path.display().to_string()),
49                }
50            }
51            Err(_) => DependencyStatus {
52                name: "AtomicParsley".to_string(),
53                found: false,
54                version: None,
55                path: None,
56            },
57        }
58    }
59
60    /// Check if MP4Box is installed
61    pub fn check_mp4box() -> DependencyStatus {
62        match which("MP4Box") {
63            Ok(path) => {
64                let version = Self::get_mp4box_version();
65                DependencyStatus {
66                    name: "MP4Box".to_string(),
67                    found: true,
68                    version,
69                    path: Some(path.display().to_string()),
70                }
71            }
72            Err(_) => DependencyStatus {
73                name: "MP4Box".to_string(),
74                found: false,
75                version: None,
76                path: None,
77            },
78        }
79    }
80
81    /// Check all dependencies
82    pub fn check_all() -> Vec<DependencyStatus> {
83        vec![
84            Self::check_ffmpeg(),
85            Self::check_atomic_parsley(),
86            Self::check_mp4box(),
87        ]
88    }
89
90    /// Check if all dependencies are satisfied
91    pub fn all_dependencies_met() -> bool {
92        Self::check_all().iter().all(|dep| dep.found)
93    }
94
95    /// Get FFmpeg version
96    fn get_ffmpeg_version() -> Option<String> {
97        let output = Command::new("ffmpeg")
98            .arg("-version")
99            .output()
100            .ok()?;
101
102        let stdout = String::from_utf8_lossy(&output.stdout);
103        stdout
104            .lines()
105            .next()
106            .and_then(|line| line.split_whitespace().nth(2))
107            .map(|s| s.to_string())
108    }
109
110    /// Get AtomicParsley version
111    fn get_atomic_parsley_version() -> Option<String> {
112        let output = Command::new("AtomicParsley")
113            .arg("--version")
114            .output()
115            .ok()?;
116
117        let stdout = String::from_utf8_lossy(&output.stdout);
118        stdout
119            .lines()
120            .find(|line| line.contains("version"))
121            .and_then(|line| line.split_whitespace().last())
122            .map(|s| s.to_string())
123    }
124
125    /// Get MP4Box version
126    fn get_mp4box_version() -> Option<String> {
127        let output = Command::new("MP4Box")
128            .arg("-version")
129            .output()
130            .ok()?;
131
132        let stdout = String::from_utf8_lossy(&output.stdout);
133        stdout
134            .lines()
135            .next()
136            .and_then(|line| line.split_whitespace().find(|s| s.contains("version")))
137            .map(|s| s.to_string())
138    }
139
140    /// Check if Apple Silicon AAC encoder is available
141    pub fn check_aac_at_support() -> bool {
142        // Check if ffmpeg supports aac_at encoder
143        let output = Command::new("ffmpeg")
144            .args(&["-encoders"])
145            .output();
146
147        if let Ok(output) = output {
148            let stdout = String::from_utf8_lossy(&output.stdout);
149            stdout.contains("aac_at")
150        } else {
151            false
152        }
153    }
154
155    /// Get all available AAC encoders
156    pub fn get_available_encoders() -> Vec<String> {
157        crate::audio::EncoderDetector::get_available_encoders()
158            .into_iter()
159            .map(|e| e.name().to_string())
160            .collect()
161    }
162
163    /// Get the currently selected AAC encoder
164    pub fn get_selected_encoder() -> String {
165        crate::audio::get_encoder().name().to_string()
166    }
167}
168
169impl std::fmt::Display for DependencyStatus {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        if self.found {
172            write!(f, "✓ {}", self.name)?;
173            if let Some(ref version) = self.version {
174                write!(f, " ({})", version)?;
175            }
176            if let Some(ref path) = self.path {
177                write!(f, "\n  Path: {}", path)?;
178            }
179            Ok(())
180        } else {
181            write!(f, "✗ {} - NOT FOUND", self.name)
182        }
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_check_dependencies() {
192        let deps = DependencyChecker::check_all();
193        assert_eq!(deps.len(), 3);
194
195        // At least ffmpeg should be found for tests to pass
196        let ffmpeg = deps.iter().find(|d| d.name == "ffmpeg");
197        assert!(ffmpeg.is_some());
198    }
199}