use ape; use error_chain::bail; use id3; use lewton::inside_ogg::OggStreamReader; use metaflac; use mp3_duration; use regex::Regex; use std::fs; use std::path::Path; use crate::errors::*; use crate::utils; use crate::utils::AudioFormat; #[derive(Debug, Clone, PartialEq)] pub struct SongTags { pub disc_number: Option, pub track_number: Option, pub title: Option, pub duration: Option, pub artist: Option, pub album_artist: Option, pub album: Option, pub year: Option, } #[cfg_attr(feature = "profile-index", flame)] pub fn read(path: &Path) -> Result { match utils::get_audio_format(path) { Some(AudioFormat::FLAC) => read_flac(path), Some(AudioFormat::MP3) => read_id3(path), Some(AudioFormat::MPC) => read_ape(path), Some(AudioFormat::OGG) => read_vorbis(path), _ => bail!("Unsupported file format for reading metadata"), } } #[cfg_attr(feature = "profile-index", flame)] fn read_id3(path: &Path) -> Result { let tag = id3::Tag::read_from_path(&path)?; let duration = mp3_duration::from_path(&path) .map(|d| d.as_secs() as u32) .ok(); let artist = tag.artist().map(|s| s.to_string()); let album_artist = tag.album_artist().map(|s| s.to_string()); let album = tag.album().map(|s| s.to_string()); let title = tag.title().map(|s| s.to_string()); let disc_number = tag.disc(); let track_number = tag.track(); let year = tag .year() .map(|y| y as i32) .or_else(|| tag.date_released().and_then(|d| Some(d.year))) .or_else(|| tag.date_recorded().and_then(|d| Some(d.year))); Ok(SongTags { artist, album_artist, album, title, duration, disc_number, track_number, year, }) } fn read_ape_string(item: &ape::Item) -> Option { match item.value { ape::ItemValue::Text(ref s) => Some(s.clone()), _ => None, } } fn read_ape_i32(item: &ape::Item) -> Option { match item.value { ape::ItemValue::Text(ref s) => s.parse::().ok(), _ => None, } } fn read_ape_x_of_y(item: &ape::Item) -> Option { match item.value { ape::ItemValue::Text(ref s) => { let format = Regex::new(r#"^\d+"#).unwrap(); if let Some(m) = format.find(s) { s[m.start()..m.end()].parse().ok() } else { None } } _ => None, } } #[cfg_attr(feature = "profile-index", flame)] fn read_ape(path: &Path) -> Result { let tag = ape::read(path)?; let artist = tag.item("Artist").and_then(read_ape_string); let album = tag.item("Album").and_then(read_ape_string); let album_artist = tag.item("Album artist").and_then(read_ape_string); let title = tag.item("Title").and_then(read_ape_string); let year = tag.item("Year").and_then(read_ape_i32); let disc_number = tag.item("Disc").and_then(read_ape_x_of_y); let track_number = tag.item("Track").and_then(read_ape_x_of_y); Ok(SongTags { artist, album_artist, album, title, duration: None, disc_number, track_number, year, }) } #[cfg_attr(feature = "profile-index", flame)] fn read_vorbis(path: &Path) -> Result { let file = fs::File::open(path)?; let source = OggStreamReader::new(file)?; let mut tags = SongTags { artist: None, album_artist: None, album: None, title: None, duration: None, disc_number: None, track_number: None, year: None, }; for (key, value) in source.comment_hdr.comment_list { match key.as_str() { "TITLE" => tags.title = Some(value), "ALBUM" => tags.album = Some(value), "ARTIST" => tags.artist = Some(value), "ALBUMARTIST" => tags.album_artist = Some(value), "TRACKNUMBER" => tags.track_number = value.parse::().ok(), "DISCNUMBER" => tags.disc_number = value.parse::().ok(), "DATE" => tags.year = value.parse::().ok(), _ => (), } } Ok(tags) } #[cfg_attr(feature = "profile-index", flame)] fn read_flac(path: &Path) -> Result { let tag = metaflac::Tag::read_from_path(path)?; let vorbis = tag.vorbis_comments().ok_or("Missing Vorbis comments")?; let disc_number = vorbis .get("DISCNUMBER") .and_then(|d| d[0].parse::().ok()); let year = vorbis.get("DATE").and_then(|d| d[0].parse::().ok()); let mut streaminfo = tag.get_blocks(metaflac::BlockType::StreamInfo); let duration = match streaminfo.next() { Some(&metaflac::Block::StreamInfo(ref s)) => { Some((s.total_samples as u32 / s.sample_rate) as u32) } _ => None, }; Ok(SongTags { artist: vorbis.artist().map(|v| v[0].clone()), album_artist: vorbis.album_artist().map(|v| v[0].clone()), album: vorbis.album().map(|v| v[0].clone()), title: vorbis.title().map(|v| v[0].clone()), duration, disc_number, track_number: vorbis.track(), year, }) } #[test] fn test_read_metadata() { let sample_tags = SongTags { disc_number: Some(3), track_number: Some(1), title: Some("TEST TITLE".into()), artist: Some("TEST ARTIST".into()), album_artist: Some("TEST ALBUM ARTIST".into()), album: Some("TEST ALBUM".into()), duration: None, year: Some(2016), }; let flac_sample_tag = SongTags { duration: Some(0), ..sample_tags.clone() }; let mp3_sample_tag = SongTags { duration: Some(0), ..sample_tags.clone() }; assert_eq!(read(Path::new("test/sample.mp3")).unwrap(), mp3_sample_tag); assert_eq!(read(Path::new("test/sample.ogg")).unwrap(), sample_tags); assert_eq!( read(Path::new("test/sample.flac")).unwrap(), flac_sample_tag ); }