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
156impl std::fmt::Display for DependencyStatus {
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        if self.found {
159            write!(f, "✓ {}", self.name)?;
160            if let Some(ref version) = self.version {
161                write!(f, " ({})", version)?;
162            }
163            if let Some(ref path) = self.path {
164                write!(f, "\n  Path: {}", path)?;
165            }
166            Ok(())
167        } else {
168            write!(f, "✗ {} - NOT FOUND", self.name)
169        }
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[test]
178    fn test_check_dependencies() {
179        let deps = DependencyChecker::check_all();
180        assert_eq!(deps.len(), 3);
181
182        // At least ffmpeg should be found for tests to pass
183        let ffmpeg = deps.iter().find(|d| d.name == "ffmpeg");
184        assert!(ffmpeg.is_some());
185    }
186}