Extract id3 covers

This commit is contained in:
2026-03-28 11:25:30 -07:00
parent 5527b61d39
commit 60dc755561

View File

@@ -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<usize>,
}
impl Id3ImagesExtractor {
pub fn new(item: &BinaryPileValue) -> Self {
Self {
item: item.clone(),
cached_count: OnceLock::new(),
}
}
async fn read_tag(&self) -> Result<Option<Tag>, 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<Option<PileValue>, 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<usize, std::io::Error> {
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<HashMap<Label, PileValue>>,
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<Vec<Label>, std::io::Error> {
Ok(self.get_inner().await?.keys().cloned().collect())
Ok(self
.get_inner()
.await?
.keys()
.cloned()
.chain([Label::new("images").unwrap()])
.collect::<Vec<_>>())
}
}