From 60dc755561e6e94d71d90d1e066264e166bedfc6 Mon Sep 17 00:00:00 2001 From: rm-dr <96270320+rm-dr@users.noreply.github.com> Date: Sat, 28 Mar 2026 11:25:30 -0700 Subject: [PATCH] Extract id3 covers --- crates/pile-value/src/extract/blob/id3.rs | 104 +++++++++++++++++++++- 1 file changed, 101 insertions(+), 3 deletions(-) diff --git a/crates/pile-value/src/extract/blob/id3.rs b/crates/pile-value/src/extract/blob/id3.rs index eab6b2f..684508b 100644 --- a/crates/pile-value/src/extract/blob/id3.rs +++ b/crates/pile-value/src/extract/blob/id3.rs @@ -1,4 +1,5 @@ use id3::Tag; +use mime::Mime; use pile_config::Label; use pile_io::SyncReadBridge; use std::{ @@ -10,13 +11,98 @@ use std::{ use tracing::trace; use crate::{ - extract::traits::{ExtractState, ObjectExtractor}, - value::{BinaryPileValue, PileValue}, + extract::traits::{ExtractState, ListExtractor, ObjectExtractor}, + value::{ArcBytes, BinaryPileValue, PileValue}, }; +pub struct Id3ImagesExtractor { + item: BinaryPileValue, + cached_count: OnceLock, +} + +impl Id3ImagesExtractor { + pub fn new(item: &BinaryPileValue) -> Self { + Self { + item: item.clone(), + cached_count: OnceLock::new(), + } + } + + async fn read_tag(&self) -> Result, std::io::Error> { + let item = self.item.clone(); + let reader = SyncReadBridge::new_current(self.item.read().await?); + tokio::task::spawn_blocking(move || match Tag::read_from2(BufReader::new(reader)) { + Ok(tag) => Ok(Some(tag)), + Err(id3::Error { + kind: id3::ErrorKind::Io(e), + .. + }) => Err(e), + Err(error) => { + trace!(message = "Could not parse id3 tags", ?item, ?error); + Ok(None) + } + }) + .await + .map_err(std::io::Error::other)? + } + + fn mime_ok(&self, state: &ExtractState) -> bool { + state.ignore_mime || self.item.mime().essence_str() == "audio/mpeg" + } +} + +#[async_trait::async_trait] +impl ListExtractor for Id3ImagesExtractor { + async fn get( + &self, + state: &ExtractState, + idx: usize, + ) -> Result, std::io::Error> { + if !self.mime_ok(state) { + return Ok(None); + } + + let Some(tag) = self.read_tag().await? else { + return Ok(None); + }; + + let Some(picture) = tag.pictures().nth(idx) else { + return Ok(None); + }; + + let mime: Mime = picture + .mime_type + .parse() + .unwrap_or(mime::APPLICATION_OCTET_STREAM); + let data = picture.data.clone(); + + Ok(Some(PileValue::Binary(BinaryPileValue::Blob { + mime, + bytes: ArcBytes(Arc::new(data)), + }))) + } + + async fn len(&self, state: &ExtractState) -> Result { + if !self.mime_ok(state) { + return Ok(0); + } + + if let Some(x) = self.cached_count.get() { + return Ok(*x); + } + + let count = match self.read_tag().await? { + Some(tag) => tag.pictures().count(), + None => 0, + }; + Ok(*self.cached_count.get_or_init(|| count)) + } +} + pub struct Id3Extractor { item: BinaryPileValue, output: OnceLock>, + images: PileValue, } impl Id3Extractor { @@ -24,6 +110,7 @@ impl Id3Extractor { Self { item: item.clone(), output: OnceLock::new(), + images: PileValue::ListExtractor(Arc::new(Id3ImagesExtractor::new(item))), } } @@ -134,10 +221,21 @@ impl ObjectExtractor for Id3Extractor { return Ok(None); } + if name.as_str() == "images" { + return Ok(Some(self.images.clone())); + } + Ok(self.get_inner().await?.get(name).cloned()) } + #[expect(clippy::unwrap_used)] async fn fields(&self) -> Result, std::io::Error> { - Ok(self.get_inner().await?.keys().cloned().collect()) + Ok(self + .get_inner() + .await? + .keys() + .cloned() + .chain([Label::new("images").unwrap()]) + .collect::>()) } }