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 mut needs_imagemagick = false;
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    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}