use id3::Tag; use pile_config::Label; use std::{borrow::Cow, collections::HashMap, sync::OnceLock}; use crate::{FileItem, PileValue, extract::Extractor}; pub struct Id3Extractor<'a> { item: &'a FileItem, output: OnceLock>>, } impl<'a> Id3Extractor<'a> { pub fn new(item: &'a FileItem) -> Self { Self { item, output: OnceLock::new(), } } fn get_inner(&self) -> Result<&HashMap>, std::io::Error> { if let Some(x) = self.output.get() { return Ok(x); } let ext = self.item.path.extension().and_then(|x| x.to_str()); if !matches!(ext, Some("mp3") | Some("aiff") | Some("aif") | Some("wav")) { return Ok(self.output.get_or_init(HashMap::new)); } let tag = match Tag::read_from_path(&self.item.path) { Ok(tag) => tag, Err(id3::Error { kind: id3::ErrorKind::NoTag, .. }) => return Ok(self.output.get_or_init(HashMap::new)), Err(id3::Error { kind: id3::ErrorKind::Io(e), .. }) => return Err(e), Err(e) => return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, e)), }; let mut output: HashMap>> = HashMap::new(); for frame in tag.frames() { if let Some(text) = frame.content().text() { let name = frame_id_to_field(frame.id()); if let Some(key) = Label::new(name) { output .entry(key) .or_default() .push(PileValue::String(text.into())); } } } let output = output .into_iter() .map(|(k, v)| (k, PileValue::Array(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 { "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()), } } impl Extractor for Id3Extractor<'_> { fn field<'a>( &'a self, name: &Label, ) -> Result>, std::io::Error> { Ok(self.get_inner()?.get(name)) } fn fields(&self) -> Result, std::io::Error> { Ok(self.get_inner()?.keys().cloned().collect()) } }