Three recurring issues on every full scan: 1. Video playlist scans re-enqueued every file only to reject it as AlreadyExists. Pre-filter in ScanDirectoryMessage and QueueVideosMessage so we skip videos whose .m3u8 already exists, and demote the leaked AlreadyExists log to debug. 2. image crate was built with only jpeg/png features, so webp/tiff/avif files logged "format not supported" every scan. Enable those features. 3. RAW (ARW/NEF/CR2/...) and HEIC thumbnails weren't generated, so the scan kept retrying them. Try the file's embedded JPEG preview via kamadak-exif first (fast, pure-Rust, works on Sony ARW where ffmpeg's TIFF decoder fails). Fall back to ffmpeg for HEIC/HEIF and RAWs with no preview. Anything still undecodable gets a <thumb>.unsupported sentinel so future scans skip it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
102 lines
3.2 KiB
Rust
102 lines
3.2 KiB
Rust
use std::path::Path;
|
|
use walkdir::DirEntry;
|
|
|
|
/// Supported image file extensions
|
|
pub const IMAGE_EXTENSIONS: &[&str] = &[
|
|
"jpg", "jpeg", "png", "webp", "tiff", "tif", "heif", "heic", "avif", "nef", "arw",
|
|
];
|
|
|
|
/// Extensions the `image` crate cannot decode — we fall back to ffmpeg to
|
|
/// extract an embedded preview or decode the frame.
|
|
pub const FFMPEG_THUMBNAIL_EXTENSIONS: &[&str] = &["heif", "heic", "nef", "arw"];
|
|
|
|
/// Returns true if thumbnail generation should go through ffmpeg instead of
|
|
/// the `image` crate (RAW formats, HEIF/HEIC).
|
|
pub fn needs_ffmpeg_thumbnail(path: &Path) -> bool {
|
|
match path.extension().and_then(|e| e.to_str()) {
|
|
Some(ext) => FFMPEG_THUMBNAIL_EXTENSIONS.contains(&ext.to_lowercase().as_str()),
|
|
None => false,
|
|
}
|
|
}
|
|
|
|
/// Supported video file extensions
|
|
pub const VIDEO_EXTENSIONS: &[&str] = &["mp4", "mov", "avi", "mkv"];
|
|
|
|
/// Check if a path has an image extension
|
|
pub fn is_image_file(path: &Path) -> bool {
|
|
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
|
let ext_lower = ext.to_lowercase();
|
|
IMAGE_EXTENSIONS.contains(&ext_lower.as_str())
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Check if a path has a video extension
|
|
pub fn is_video_file(path: &Path) -> bool {
|
|
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
|
let ext_lower = ext.to_lowercase();
|
|
VIDEO_EXTENSIONS.contains(&ext_lower.as_str())
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Check if a path has a supported media extension (image or video)
|
|
pub fn is_media_file(path: &Path) -> bool {
|
|
is_image_file(path) || is_video_file(path)
|
|
}
|
|
|
|
/// Check if a DirEntry is an image file (for walkdir usage)
|
|
#[allow(dead_code)]
|
|
pub fn direntry_is_image(entry: &DirEntry) -> bool {
|
|
is_image_file(entry.path())
|
|
}
|
|
|
|
/// Check if a DirEntry is a video file (for walkdir usage)
|
|
#[allow(dead_code)]
|
|
pub fn direntry_is_video(entry: &DirEntry) -> bool {
|
|
is_video_file(entry.path())
|
|
}
|
|
|
|
/// Check if a DirEntry is a media file (for walkdir usage)
|
|
#[allow(dead_code)]
|
|
pub fn direntry_is_media(entry: &DirEntry) -> bool {
|
|
is_media_file(entry.path())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::path::Path;
|
|
|
|
#[test]
|
|
fn test_is_image_file() {
|
|
assert!(is_image_file(Path::new("photo.jpg")));
|
|
assert!(is_image_file(Path::new("photo.JPG")));
|
|
assert!(is_image_file(Path::new("photo.png")));
|
|
assert!(is_image_file(Path::new("photo.nef")));
|
|
assert!(!is_image_file(Path::new("video.mp4")));
|
|
assert!(!is_image_file(Path::new("document.txt")));
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_video_file() {
|
|
assert!(is_video_file(Path::new("video.mp4")));
|
|
assert!(is_video_file(Path::new("video.MP4")));
|
|
assert!(is_video_file(Path::new("video.mov")));
|
|
assert!(is_video_file(Path::new("video.avi")));
|
|
assert!(!is_video_file(Path::new("photo.jpg")));
|
|
assert!(!is_video_file(Path::new("document.txt")));
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_media_file() {
|
|
assert!(is_media_file(Path::new("photo.jpg")));
|
|
assert!(is_media_file(Path::new("video.mp4")));
|
|
assert!(is_media_file(Path::new("photo.PNG")));
|
|
assert!(!is_media_file(Path::new("document.txt")));
|
|
assert!(!is_media_file(Path::new("no_extension")));
|
|
}
|
|
}
|