Extractor rewrite

This commit is contained in:
2026-03-11 10:12:36 -07:00
parent b789255ea9
commit 078801be40
51 changed files with 676 additions and 693 deletions

View 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())
}
}