Extract id3 covers
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
use id3::Tag;
|
use id3::Tag;
|
||||||
|
use mime::Mime;
|
||||||
use pile_config::Label;
|
use pile_config::Label;
|
||||||
use pile_io::SyncReadBridge;
|
use pile_io::SyncReadBridge;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -10,13 +11,98 @@ use std::{
|
|||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
extract::traits::{ExtractState, ObjectExtractor},
|
extract::traits::{ExtractState, ListExtractor, ObjectExtractor},
|
||||||
value::{BinaryPileValue, PileValue},
|
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 {
|
pub struct Id3Extractor {
|
||||||
item: BinaryPileValue,
|
item: BinaryPileValue,
|
||||||
output: OnceLock<HashMap<Label, PileValue>>,
|
output: OnceLock<HashMap<Label, PileValue>>,
|
||||||
|
images: PileValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Id3Extractor {
|
impl Id3Extractor {
|
||||||
@@ -24,6 +110,7 @@ impl Id3Extractor {
|
|||||||
Self {
|
Self {
|
||||||
item: item.clone(),
|
item: item.clone(),
|
||||||
output: OnceLock::new(),
|
output: OnceLock::new(),
|
||||||
|
images: PileValue::ListExtractor(Arc::new(Id3ImagesExtractor::new(item))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,10 +221,21 @@ impl ObjectExtractor for Id3Extractor {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if name.as_str() == "images" {
|
||||||
|
return Ok(Some(self.images.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(self.get_inner().await?.get(name).cloned())
|
Ok(self.get_inner().await?.get(name).cloned())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::unwrap_used)]
|
||||||
async fn fields(&self) -> Result<Vec<Label>, std::io::Error> {
|
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