diff --git a/src/face_watch.rs b/src/face_watch.rs index 272ed8f..42c6128 100644 --- a/src/face_watch.rs +++ b/src/face_watch.rs @@ -415,22 +415,36 @@ fn try_auto_bind( /// Pulled out for unit testing — the same `PathExcluder` /memories uses, /// just applied at the face-detect candidate set instead of the memories /// listing. Skip @eaDir / .thumbnails / user-defined paths before we burn -/// a detect call (and Apollo's GPU memory) on junk. +/// a detect call (and Apollo's GPU memory) on junk. Also drops anything +/// that isn't an image file — the backlog drain pulls every hashed row in +/// `image_exif`, which includes videos; sending those to Apollo just +/// produces `failed` markers and inflates the FAILED stat. pub(crate) fn filter_excluded( base: &Path, excluded_dirs: &[String], candidates: Vec, library_name: Option<&str>, ) -> Vec { - if excluded_dirs.is_empty() { - return candidates; - } - let excluder = PathExcluder::new(base, excluded_dirs); + let excluder = if excluded_dirs.is_empty() { + None + } else { + Some(PathExcluder::new(base, excluded_dirs)) + }; candidates .into_iter() .filter(|c| { let abs = base.join(&c.rel_path); - if excluder.is_excluded(&abs) { + if !file_types::is_image_file(&abs) { + debug!( + "face_watch: skipping non-image path {} (library {})", + c.rel_path, + library_name.unwrap_or("") + ); + return false; + } + if let Some(ex) = excluder.as_ref() + && ex.is_excluded(&abs) + { debug!( "face_watch: skipping excluded path {} (library {})", c.rel_path, @@ -507,8 +521,8 @@ mod tests { #[test] fn filter_excluded_empty_rules_passes_all() { - // Skip the PathExcluder build entirely on the common path where - // EXCLUDED_DIRS is unset — saves an allocation per pass. + // EXCLUDED_DIRS unset still lets every image through — only the + // PathExcluder is skipped, the image-extension gate still runs. let tmp = tempfile::tempdir().unwrap(); let base = tmp.path(); let candidates = vec![cand("a.jpg"), cand("b.jpg")]; @@ -516,6 +530,25 @@ mod tests { assert_eq!(kept.len(), 2); } + #[test] + fn filter_excluded_drops_videos_and_non_media() { + // Backlog drain pulls every hashed row in image_exif (videos + // included). Videos must never reach Apollo — opencv can't + // decode them, every call would 422 and write a `failed` marker. + let tmp = tempfile::tempdir().unwrap(); + let base = tmp.path(); + let candidates = vec![ + cand("photos/a.jpg"), + cand("photos/clip.mp4"), + cand("photos/clip.MOV"), + cand("photos/notes.txt"), + cand("photos/b.heic"), + ]; + let kept = filter_excluded(base, &[], candidates, Some("test")); + let kept_paths: Vec<_> = kept.iter().map(|c| c.rel_path.as_str()).collect(); + assert_eq!(kept_paths, vec!["photos/a.jpg", "photos/b.heic"]); + } + #[test] fn read_bytes_passes_through_for_jpeg() { // JPEG goes through plain read — we DON'T want to lose orientation