Extract id3 covers
This commit is contained in:
@@ -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<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user