nameback_core/
deps_check.rs1use anyhow::Result;
2use std::path::Path;
3use walkdir::WalkDir;
4
5#[derive(Debug, Clone, PartialEq)]
7pub enum Dependency {
8 ExifTool,
9 Tesseract,
10 FFmpeg,
11 ImageMagick,
12}
13
14impl Dependency {
15 pub fn name(&self) -> &str {
16 match self {
17 Dependency::ExifTool => "exiftool",
18 Dependency::Tesseract => "tesseract",
19 Dependency::FFmpeg => "ffmpeg",
20 Dependency::ImageMagick => "imagemagick",
21 }
22 }
23
24 pub fn description(&self) -> &str {
25 match self {
26 Dependency::ExifTool => "Core metadata extraction (required)",
27 Dependency::Tesseract => "OCR for images and videos",
28 Dependency::FFmpeg => "Video frame extraction",
29 Dependency::ImageMagick => "HEIC/HEIF support",
30 }
31 }
32
33 pub fn is_available(&self) -> bool {
34 match self {
35 Dependency::ExifTool => check_exiftool(),
36 Dependency::Tesseract => check_tesseract(),
37 Dependency::FFmpeg => check_ffmpeg(),
38 Dependency::ImageMagick => check_imagemagick(),
39 }
40 }
41}
42
43#[derive(Debug)]
45pub struct DependencyNeeds {
46 pub missing_required: Vec<Dependency>,
47 pub missing_optional: Vec<Dependency>,
48}
49
50impl DependencyNeeds {
51 pub fn is_empty(&self) -> bool {
52 self.missing_required.is_empty() && self.missing_optional.is_empty()
53 }
54
55 pub fn has_required_missing(&self) -> bool {
56 !self.missing_required.is_empty()
57 }
58}
59
60pub fn detect_needed_dependencies(directory: &Path) -> Result<DependencyNeeds> {
62 let mut needs_tesseract = false;
63 let mut needs_ffmpeg = false;
64 let mut needs_imagemagick = false;
65
66 let mut file_count = 0;
68 for entry in WalkDir::new(directory)
69 .max_depth(3) .into_iter()
71 .filter_map(|e| e.ok())
72 {
73 if !entry.file_type().is_file() {
74 continue;
75 }
76
77 file_count += 1;
78 if file_count > 1000 {
79 break;
81 }
82
83 let path = entry.path();
84 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
85 let ext_lower = ext.to_lowercase();
86
87 match ext_lower.as_str() {
88 "jpg" | "jpeg" | "png" | "gif" | "bmp" | "tiff" | "tif" | "webp" => {
90 needs_tesseract = true;
91 }
92 "heic" | "heif" => {
94 needs_tesseract = true;
95 #[cfg(not(target_os = "macos"))]
96 {
97 needs_imagemagick = true;
98 }
99 }
100 "mp4" | "mov" | "avi" | "mkv" | "webm" | "flv" | "wmv" | "m4v" => {
102 needs_ffmpeg = true;
103 needs_tesseract = true; }
105 _ => {}
106 }
107 }
108 }
109
110 let mut missing_required = Vec::new();
112 let mut missing_optional = Vec::new();
113
114 if !Dependency::ExifTool.is_available() {
116 missing_required.push(Dependency::ExifTool);
117 }
118
119 if needs_tesseract && !Dependency::Tesseract.is_available() {
121 missing_optional.push(Dependency::Tesseract);
122 }
123
124 if needs_ffmpeg && !Dependency::FFmpeg.is_available() {
125 missing_optional.push(Dependency::FFmpeg);
126 }
127
128 if needs_imagemagick && !Dependency::ImageMagick.is_available() {
129 missing_optional.push(Dependency::ImageMagick);
130 }
131
132 Ok(DependencyNeeds {
133 missing_required,
134 missing_optional,
135 })
136}
137
138fn check_exiftool() -> bool {
141 let result = std::process::Command::new("exiftool")
142 .arg("-ver")
143 .output()
144 .map(|o| o.status.success())
145 .unwrap_or(false);
146
147 log::debug!("Dependency check - exiftool: {}", if result { "available" } else { "missing" });
148 result
149}
150
151fn check_tesseract() -> bool {
152 let result = std::process::Command::new("tesseract")
153 .arg("--version")
154 .output()
155 .map(|o| o.status.success())
156 .unwrap_or(false);
157
158 log::debug!("Dependency check - tesseract: {}", if result { "available" } else { "missing" });
159 result
160}
161
162fn check_ffmpeg() -> bool {
163 let result = std::process::Command::new("ffmpeg")
164 .arg("-version")
165 .output()
166 .map(|o| o.status.success())
167 .unwrap_or(false);
168
169 log::debug!("Dependency check - ffmpeg: {}", if result { "available" } else { "missing" });
170 result
171}
172
173fn check_imagemagick() -> bool {
174 let result = std::process::Command::new("magick")
175 .arg("-version")
176 .output()
177 .or_else(|_| {
178 std::process::Command::new("convert")
179 .arg("-version")
180 .output()
181 })
182 .map(|o| o.status.success())
183 .unwrap_or(false);
184
185 log::debug!("Dependency check - imagemagick: {}", if result { "available" } else { "missing" });
186 result
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_dependency_names() {
195 assert_eq!(Dependency::ExifTool.name(), "exiftool");
196 assert_eq!(Dependency::Tesseract.name(), "tesseract");
197 assert_eq!(Dependency::FFmpeg.name(), "ffmpeg");
198 assert_eq!(Dependency::ImageMagick.name(), "imagemagick");
199 }
200
201 #[test]
202 fn test_dependency_needs_empty() {
203 let needs = DependencyNeeds {
204 missing_required: vec![],
205 missing_optional: vec![],
206 };
207 assert!(needs.is_empty());
208 assert!(!needs.has_required_missing());
209 }
210
211 #[test]
212 fn test_dependency_needs_with_required() {
213 let needs = DependencyNeeds {
214 missing_required: vec![Dependency::ExifTool],
215 missing_optional: vec![],
216 };
217 assert!(!needs.is_empty());
218 assert!(needs.has_required_missing());
219 }
220
221 #[test]
222 fn test_dependency_needs_with_optional() {
223 let needs = DependencyNeeds {
224 missing_required: vec![],
225 missing_optional: vec![Dependency::Tesseract],
226 };
227 assert!(!needs.is_empty());
228 assert!(!needs.has_required_missing());
229 }
230}