fix(scan): quiet startup scans & thumbnail RAW/HEIC
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>
This commit is contained in:
56
src/exif.rs
56
src/exif.rs
@@ -1,5 +1,5 @@
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use std::io::{BufReader, Read, Seek, SeekFrom};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
@@ -25,6 +25,60 @@ pub struct ExifData {
|
||||
pub date_taken: Option<i64>,
|
||||
}
|
||||
|
||||
/// TIFF-based RAW formats where `JPEGInterchangeFormat` offsets are
|
||||
/// absolute file offsets (the file itself is a TIFF container).
|
||||
fn is_tiff_raw(path: &Path) -> bool {
|
||||
matches!(
|
||||
path.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|s| s.to_lowercase())
|
||||
.as_deref(),
|
||||
Some(
|
||||
"tiff" | "tif" | "nef" | "cr2" | "arw" | "dng" | "raf" | "orf" | "rw2" | "pef" | "srw"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the bytes of the embedded JPEG thumbnail in a TIFF-based RAW or
|
||||
/// TIFF file. Used to thumbnail formats whose RAW pixel data can't be decoded
|
||||
/// by our normal tools (e.g. Sony ARW). Returns `None` if no preview is
|
||||
/// present, the file isn't a TIFF container, or the data doesn't look like
|
||||
/// a valid JPEG.
|
||||
pub fn extract_embedded_jpeg_preview(path: &Path) -> Option<Vec<u8>> {
|
||||
if !is_tiff_raw(path) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let file = File::open(path).ok()?;
|
||||
let mut bufreader = BufReader::new(file);
|
||||
let exif = Reader::new().read_from_container(&mut bufreader).ok()?;
|
||||
|
||||
let offset = exif
|
||||
.get_field(Tag::JPEGInterchangeFormat, In::THUMBNAIL)?
|
||||
.value
|
||||
.get_uint(0)?;
|
||||
let length = exif
|
||||
.get_field(Tag::JPEGInterchangeFormatLength, In::THUMBNAIL)?
|
||||
.value
|
||||
.get_uint(0)?;
|
||||
if length == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut file = File::open(path).ok()?;
|
||||
file.seek(SeekFrom::Start(offset as u64)).ok()?;
|
||||
let mut buf = vec![0u8; length as usize];
|
||||
file.read_exact(&mut buf).ok()?;
|
||||
|
||||
// JPEG SOI marker sanity check — MakerNote offsets sometimes point at
|
||||
// TIFF-wrapped previews or other non-JPEG data.
|
||||
if buf.len() < 2 || buf[0] != 0xFF || buf[1] != 0xD8 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(buf)
|
||||
}
|
||||
|
||||
pub fn supports_exif(path: &Path) -> bool {
|
||||
if let Some(ext) = path.extension() {
|
||||
let ext_lower = ext.to_string_lossy().to_lowercase();
|
||||
|
||||
Reference in New Issue
Block a user