Extractor rewrite
This commit is contained in:
135
crates/pile-value/src/extract/item/id3.rs
Normal file
135
crates/pile-value/src/extract/item/id3.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use id3::Tag;
|
||||
use pile_config::Label;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
io::BufReader,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
extract::traits::ObjectExtractor,
|
||||
value::{Item, PileValue, SyncReadBridge},
|
||||
};
|
||||
|
||||
pub struct Id3Extractor {
|
||||
item: Item,
|
||||
output: OnceLock<HashMap<Label, PileValue>>,
|
||||
}
|
||||
|
||||
impl Id3Extractor {
|
||||
pub fn new(item: &Item) -> Self {
|
||||
Self {
|
||||
item: item.clone(),
|
||||
output: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_inner(&self) -> Result<&HashMap<Label, PileValue>, std::io::Error> {
|
||||
if let Some(x) = self.output.get() {
|
||||
return Ok(x);
|
||||
}
|
||||
|
||||
let key = self.item.key();
|
||||
let ext = key.as_str().rsplit('.').next();
|
||||
if !matches!(ext, Some("mp3") | Some("aiff") | Some("aif") | Some("wav")) {
|
||||
return Ok(self.output.get_or_init(HashMap::new));
|
||||
}
|
||||
|
||||
let reader = SyncReadBridge::new_current(self.item.read().await?);
|
||||
let tag = match tokio::task::spawn_blocking(move || Tag::read_from2(BufReader::new(reader)))
|
||||
.await
|
||||
{
|
||||
Ok(Ok(tag)) => tag,
|
||||
|
||||
Ok(Err(id3::Error {
|
||||
kind: id3::ErrorKind::NoTag,
|
||||
..
|
||||
})) => {
|
||||
return Ok(self.output.get_or_init(HashMap::new));
|
||||
}
|
||||
|
||||
Ok(Err(id3::Error {
|
||||
kind: id3::ErrorKind::Io(e),
|
||||
..
|
||||
})) => return Err(e),
|
||||
|
||||
Ok(Err(e)) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)),
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
let mut output: HashMap<Label, Vec<PileValue>> = HashMap::new();
|
||||
for frame in tag.frames() {
|
||||
if let Some(texts) = frame.content().text_values() {
|
||||
let name = frame_id_to_field(frame.id());
|
||||
if let Some(key) = Label::new(name) {
|
||||
for text in texts {
|
||||
output
|
||||
.entry(key.clone())
|
||||
.or_default()
|
||||
.push(PileValue::String(Arc::new(text.into())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = output
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, PileValue::Array(Arc::new(v))))
|
||||
.collect();
|
||||
|
||||
return Ok(self.output.get_or_init(|| output));
|
||||
}
|
||||
}
|
||||
|
||||
/// Map an ID3 frame ID to the equivalent Vorbis Comment field name.
|
||||
/// Falls back to the lowercased frame ID if no mapping exists.
|
||||
fn frame_id_to_field(id: &str) -> Cow<'static, str> {
|
||||
match id {
|
||||
// spell:off
|
||||
"TIT2" => Cow::Borrowed("title"),
|
||||
"TIT1" => Cow::Borrowed("grouping"),
|
||||
"TIT3" => Cow::Borrowed("subtitle"),
|
||||
"TPE1" => Cow::Borrowed("artist"),
|
||||
"TPE2" => Cow::Borrowed("albumartist"),
|
||||
"TPE3" => Cow::Borrowed("conductor"),
|
||||
"TOPE" => Cow::Borrowed("originalartist"),
|
||||
"TALB" => Cow::Borrowed("album"),
|
||||
"TOAL" => Cow::Borrowed("originalalbum"),
|
||||
"TRCK" => Cow::Borrowed("tracknumber"),
|
||||
"TPOS" => Cow::Borrowed("discnumber"),
|
||||
"TSST" => Cow::Borrowed("discsubtitle"),
|
||||
"TDRC" | "TYER" => Cow::Borrowed("date"),
|
||||
"TDOR" | "TORY" => Cow::Borrowed("originaldate"),
|
||||
"TCON" => Cow::Borrowed("genre"),
|
||||
"TCOM" => Cow::Borrowed("composer"),
|
||||
"TEXT" => Cow::Borrowed("lyricist"),
|
||||
"TPUB" => Cow::Borrowed("label"),
|
||||
"TSRC" => Cow::Borrowed("isrc"),
|
||||
"TBPM" => Cow::Borrowed("bpm"),
|
||||
"TLAN" => Cow::Borrowed("language"),
|
||||
"TMED" => Cow::Borrowed("media"),
|
||||
"TMOO" => Cow::Borrowed("mood"),
|
||||
"TCOP" => Cow::Borrowed("copyright"),
|
||||
"TENC" => Cow::Borrowed("encodedby"),
|
||||
"TSSE" => Cow::Borrowed("encodersettings"),
|
||||
"TSOA" => Cow::Borrowed("albumsort"),
|
||||
"TSOP" => Cow::Borrowed("artistsort"),
|
||||
"TSOT" => Cow::Borrowed("titlesort"),
|
||||
"MVNM" => Cow::Borrowed("movement"),
|
||||
"MVIN" => Cow::Borrowed("movementnumber"),
|
||||
_ => Cow::Owned(id.to_lowercase()),
|
||||
// spell:on
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ObjectExtractor for Id3Extractor {
|
||||
async fn field(&self, name: &Label) -> Result<Option<PileValue>, std::io::Error> {
|
||||
Ok(self.get_inner().await?.get(name).cloned())
|
||||
}
|
||||
|
||||
async fn fields(&self) -> Result<Vec<Label>, std::io::Error> {
|
||||
Ok(self.get_inner().await?.keys().cloned().collect())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user