nameback_core/
deps_check.rs

1use anyhow::Result;
2use std::path::Path;
3use walkdir::WalkDir;
4
5/// Represents a dependency that might be needed
6#[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/// Result of smart dependency detection
44#[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
60/// Smart detection: scan directory and determine which dependencies are actually needed
61pub fn detect_needed_dependencies(directory: &Path) -> Result<DependencyNeeds> {
62    let mut needs_tesseract = false;
63    let mut needs_ffmpeg = false;
64    let needs_imagemagick = false; // Reserved for future HEIC detection
65
66    // Quick scan of file types (just check extensions)
67    let mut file_count = 0;
68    for entry in WalkDir::new(directory)
69        .max_depth(3) // Don't scan too deep
70        .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            // Sampled enough files
80            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                // Images that might need OCR
89                "jpg" | "jpeg" | "png" | "gif" | "bmp" | "tiff" | "tif" | "webp" => {
90                    needs_tesseract = true;
91                }
92                // HEIC files need ImageMagick on Windows/Linux
93                "heic" | "heif" => {
94                    needs_tesseract = true;
95                    #[cfg(not(target_os = "macos"))]
96                    {
97                        needs_imagemagick = true;
98                    }
99                }
100                // Videos need FFmpeg for frame extraction
101                "mp4" | "mov" | "avi" | "mkv" | "webm" | "flv" | "wmv" | "m4v" => {
102                    needs_ffmpeg = true;
103                    needs_tesseract = true; // For OCR on extracted frames
104                }
105                _ => {}
106            }
107        }
108    }
109
110    // Check which dependencies are actually missing
111    let mut missing_required = Vec::new();
112    let mut missing_optional = Vec::new();
113
114    // ExifTool is always required
115    if !Dependency::ExifTool.is_available() {
116        missing_required.push(Dependency::ExifTool);
117    }
118
119    // Optional dependencies - only if needed
120    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
138// Helper functions to check if dependencies are available
139
140fn 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}