use pile_config::Label; use std::{ collections::HashMap, io::BufReader, sync::{Arc, OnceLock}, }; use tracing::trace; use crate::{ extract::traits::ObjectExtractor, value::{Item, PileValue, SyncReadBridge}, }; pub struct ExifExtractor { item: Item, output: OnceLock>, } impl ExifExtractor { pub fn new(item: &Item) -> Self { Self { item: item.clone(), output: OnceLock::new(), } } async fn get_inner(&self) -> Result<&HashMap, std::io::Error> { if let Some(x) = self.output.get() { return Ok(x); } let reader = SyncReadBridge::new_current(self.item.read().await?); let raw_fields = tokio::task::spawn_blocking(move || { let mut br = BufReader::new(reader); let exif = exif::Reader::new() .read_from_container(&mut br) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?; let fields: Vec<(String, String)> = exif .fields() .map(|f| { ( f.tag.to_string(), f.display_value().with_unit(&exif).to_string(), ) }) .collect(); Ok::<_, std::io::Error>(fields) }) .await .map_err(std::io::Error::other)?; let raw_fields = match raw_fields { Ok(x) => x, Err(error) => { trace!(message = "Could not process exif", ?error, key = ?self.item.key()); return Ok(self.output.get_or_init(HashMap::new)); } }; let mut output: HashMap = HashMap::new(); for (tag_name, value) in raw_fields { let Some(label) = tag_to_label(&tag_name) else { continue; }; // First occurrence wins (PRIMARY IFD comes before THUMBNAIL) output .entry(label) .or_insert_with(|| PileValue::String(Arc::new(value.into()))); } return Ok(self.output.get_or_init(|| output)); } } fn tag_to_label(tag: &str) -> Option