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 std::process::Command::new("exiftool")
142 .arg("-ver")
143 .output()
144 .map(|o| o.status.success())
145 .unwrap_or(false)
146}
147
148fn check_tesseract() -> bool {
149 std::process::Command::new("tesseract")
150 .arg("--version")
151 .output()
152 .map(|o| o.status.success())
153 .unwrap_or(false)
154}
155
156fn check_ffmpeg() -> bool {
157 std::process::Command::new("ffmpeg")
158 .arg("-version")
159 .output()
160 .map(|o| o.status.success())
161 .unwrap_or(false)
162}
163
164fn check_imagemagick() -> bool {
165 std::process::Command::new("magick")
166 .arg("-version")
167 .output()
168 .or_else(|_| {
169 std::process::Command::new("convert")
170 .arg("-version")
171 .output()
172 })
173 .map(|o| o.status.success())
174 .unwrap_or(false)
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn test_dependency_names() {
183 assert_eq!(Dependency::ExifTool.name(), "exiftool");
184 assert_eq!(Dependency::Tesseract.name(), "tesseract");
185 assert_eq!(Dependency::FFmpeg.name(), "ffmpeg");
186 assert_eq!(Dependency::ImageMagick.name(), "imagemagick");
187 }
188
189 #[test]
190 fn test_dependency_needs_empty() {
191 let needs = DependencyNeeds {
192 missing_required: vec![],
193 missing_optional: vec![],
194 };
195 assert!(needs.is_empty());
196 assert!(!needs.has_required_missing());
197 }
198
199 #[test]
200 fn test_dependency_needs_with_required() {
201 let needs = DependencyNeeds {
202 missing_required: vec![Dependency::ExifTool],
203 missing_optional: vec![],
204 };
205 assert!(!needs.is_empty());
206 assert!(needs.has_required_missing());
207 }
208
209 #[test]
210 fn test_dependency_needs_with_optional() {
211 let needs = DependencyNeeds {
212 missing_required: vec![],
213 missing_optional: vec![Dependency::Tesseract],
214 };
215 assert!(!needs.is_empty());
216 assert!(!needs.has_required_missing());
217 }
218}