147 lines
3.8 KiB
Rust
147 lines
3.8 KiB
Rust
use id3::Tag;
|
|
use pile_config::Label;
|
|
use std::{
|
|
borrow::Cow,
|
|
collections::HashMap,
|
|
io::BufReader,
|
|
sync::{Arc, OnceLock},
|
|
};
|
|
use tracing::trace;
|
|
|
|
use crate::{
|
|
extract::traits::{ExtractState, 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);
|
|
}
|
|
|
|
trace!(message = "Reading id3 tags", key = self.item.key().as_str());
|
|
|
|
let key = self.item.key();
|
|
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,
|
|
|
|
Err(e) => return Err(e.into()),
|
|
Ok(Err(id3::Error {
|
|
kind: id3::ErrorKind::Io(e),
|
|
..
|
|
})) => return Err(e),
|
|
|
|
Ok(Err(error)) => {
|
|
trace!(
|
|
message = "Could not parse id3 tags",
|
|
key = key.as_str(),
|
|
?error
|
|
);
|
|
return Ok(self.output.get_or_init(HashMap::new));
|
|
}
|
|
};
|
|
|
|
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,
|
|
state: &ExtractState,
|
|
name: &Label,
|
|
args: Option<&str>,
|
|
) -> Result<Option<PileValue>, std::io::Error> {
|
|
if args.is_some() {
|
|
return Ok(None);
|
|
}
|
|
|
|
if !state.ignore_mime && self.item.mime().essence_str() != "audio/mpeg" {
|
|
return Ok(None);
|
|
}
|
|
|
|
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())
|
|
}
|
|
}
|